JavaScript 数据类型的确定与转换

233 阅读11分钟

1. 有多少种数据类型?

JavaScript 中每个值都属于某一种数据类型,目前共有 8 种:

  1. 字符串(string):值不可变

  2. 数值(number):包括整数和小数,具有数值精度问题。

  3. 布尔值(boolean):只有 truefalse 两种值。

  4. 空值(null):常用来作为初始值,以及表示空对象,转换为数值时为 0

  5. 未定义(undefined):undefined 是未赋值的变量的默认值,也是函数返回值的默认值,转换为数值时为 NaN

  6. 对象(object):各种值的组合。

  7. 唯一标识(symbol):在 ES6 (即 ES2015 )中新增的,值具有唯一性,用于解决对象属性名冲突的问题。

  8. 大整数(bigint):在 ES11 (即 ES2020)中新增的,形式为数值后面跟上 n,如 123n,用来解决 js 中整数的精度问题(小数的还是没解决哈😂)。

2. 原始类型

JavaScript 中,通常将 stringnumberboolean 这三种数据类型成为 原始类型,是最基本的数据类型,不能再分了。

nullundefined,一般将它们看作两个特殊值。JavaScript 的设计者 Brendan Eich 最开始把 null 作为空对象,且能自动转为 0,所以认为没有定义的值应该用另外的来表示,于是有了 undefined,转为数值时为 NaN

undefined 不属于关键字,可以作为变量名,但值始终为 undefined。而 null 作为空值,只存在一个,存储空间全为 0,所有赋值为 null 的变量实际都指向同一个 null

随着 JavaScript 的标准 ECMAScript(简称 ES )不断更新,symbolbigint 也可看作 原始类型

所有的原始类型存储的都是

3. 复合类型

对象(object)是最复杂的数据类型,往往由多个原始类型组成,称为 复合类型

对象(object)可以细分为三个子类型:

  1. 狭义的对象(object):如 {},也是我们通常意思下的对象。

  2. 数组(array):如 [],与狭义的对象在数据的组织形式上不同。

  3. 函数(function):如 function(){},作为一种数据类型,可以把赋值给变量,为 JavaScript 的函数式编程奠定了基础。

复合类型存储的是 地址指针)。

4. 如何确定值的数据类型?

JavaScript 中有三种方式,可以用来确定一个值的数据类型:

  1. typeof 运算符

  2. instanceof 运算符

  3. Object.prototype.toString 方法

(1) typeof 运算符

typeof 运算符可以返回一个值的数据类型。

typeof '' // "string"
typeof 1 // "number"
typeof 1.1 // "number"
typeof true // "boolean"
typeof null // "object"
typeof undefined // "undefined"
typeof {} // "object"
typeof [] // "object"
typeof function(){} // "function"
typeof Symbol() // "symbol"
typeof 1n // "bigint"

从上面,我们可以看到 typeof 运算符返回数据类型时的几个特殊情况:

情况一:

typeof null // "object"

null 并不是对象类型,但 typeof null 得到的却是 object,这个是 JavaScript 发展的历史原因导致的,具体来说是以下两个原因:

  1. 1995 年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),把 null 作为了对象类型的一种特殊值,但后面独立出来作为了原始类型,为了兼容以前的代码就一直保留了下来。

  2. JavaScript 最开始使用的 32 位系统,为了性能考虑使用低位存储变量的类型信息,比如 000 开头就代表对象,而 null 全为 0,所以类型也就判断成了 object

所以假设定义了变量 v,判断是否为 null,最方便的就是:

if(v === null){

}

情况二:

typeof undefined // "undefined"

undefined 是只声明未赋值的变量的默认值,并且不是关键字,可以作为变量名。基于此,我们可以用 typeof 来判断变量是否声明,避免直接报错,比如:

v // ReferenceError: v is not defined
typeof v // "undefined"

if(typeof v === "undefined"){
    console.log("变量 v 未声明");
}

情况三:

typeof [] // "object"
typeof function(){} // "function"

这里 typeof 得到数组的类型为 object,主要是为了强调在 JavaScript 内部,数组本质上是一种特殊的对象。

而函数的类型为 function,则是函数与普通对象的区别比较大,所以没有返回 object

如果要区分狭义的对象和数组,用 typeof 也就不行了。

(2) instanceof 运算符

instanceof 会返回一个布尔值,表示对象是否为某个构造函数的实例。

也就是说,可以用 instanceof 运算符来判断对象的具体类型,比如数组的判断:

[] instanceof Array // true
// 等同于
Array.prototype.isPrototypeOf([])

表示数组对象 [] 是构造函数 Array 的实例,所以返回 true

由于 instanceof 会检查整个原型链,所以同一个实例对多个构造函数都会返回 true,比如:

var d = new Date();
d instanceof Date // true
d instanceof Object // true

instanceof 的原理是检查构造函数的 prototype 是否在对象的原型链上,一般所有的对象,比如 o,那么 o instanceof Object 都会为 true,因为 Objectprototype 是原型链的末尾。有种特殊情况:

var o = Object.create(null); // 对象 o 的原型链上只有 null
typeof o // "object"
o instanceof Object // false

当对象的原型链上有 null 时,instanceof Object 就会返回 false

另外 instanceof 运算符还可以在构造函数中判断是否用了 new 命令运行函数:

function MyObj(name){
    if(this instanceof MyObj){
        this.name = name;
    }else{
        return new MyObj(name);
    }
}

注意,instanceof 运算符只能用于对象,不适用原始类型的值。但如果非要用来判断原始类型,也可以通过自定义 instanceof 的行为来实现,如:

class MyString{
    static [Symbol.hasInstance](str){
        return typeof str === "string";
    }
}
"" instanceof MyString // true

这也能看出 instanceof 并不是百分百可信的。😂

(3) Object.prototype.toString 方法

Object.prototype.toString 方法的作用是返回对象的类型字符串,比如:

var a = {};
a.toString(); // "[object Object]"

toString 方法经常会被覆盖,比如字符串(String)、数组(Array)、函数(Function)、日期(Date)等对象都分别自定义了自己的 toString 方法,如下:

"123".toString(); // "123"
[1,2,3].toString(); // "1,2,3"
(function(){}).toString(); // "function(){}"
new Date('2020-02-02 02:02:02').toString(); // Sun Feb 02 2020 02:02:02 GMT+0800 (中国标准时间)

所以为了得到指定对象(比如 o)的类型字符串,得用 Object.prototype.toString 才行,为了操作指定对象,需要用函数的 call 或者 apply 方法,如下:

Object.prototype.toString.call("123"); // "[object String]"
Object.prototype.toString.call(123); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function(){}); // "[object Function]"
Object.prototype.toString.call(/123/); // "[object RegExp]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(Symbol()); // "[object Symbol]"
Object.prototype.toString.call(123n); // "[object BigInt]"

从上面可以发现,通过 Object.prototype.toString.call 方法能很好地得到每种数据类型对应的字符串,比 typeofinstanceof 更为准确。利用这个可以写出更好的类型判断函数:

var myType = function(o){
    var str = Object.prototype.toString.call(o);
    var type = str.match(/\[object (.*)?\]/)[1].toLowerCase();
    return type;
}

myType("123") // "string"
myType({}); // "object"
myType([]); // "array"
//...

注意,Object.prototype.toLocaleString 可不行,并不是返回对象的类型字符串,而是将对象按一定规则转换成字符串后输出。另外 Array.isArray() 方法能很好的判断是否是数组。

5. 数据类型的转换

JavaScript 是一种动态类型语言,变量没有类型限制,可以随时给变量赋于任何类型的值。

由于变量的类型是不确定的,我们有时需要手动强制转换,在一定情况下,也会发生数据类型的自动转换。

(1) 强制转换为字符串(string)

可以用 String 函数将任意类型的值转换为字符串。

当转换原始值为字符串时,得到的基本上就是字面量所对应的字符串,只有数值会进行标准化展示:

String('abc'); // "abc"
String(123); // "123"
String(12.00); // "12"
String(1e2); // "100"
String(NaN); // "NaN"
String(true); // "true"
String(false); // "false"
String(null); // "null"
String(undefined); // "undefined"
String(Symbol()); // "Symbol()"
String(123n); // "123"

当转换对象为字符串时,会先调用对象自身的 toString 方法,如果得到不是原始值,则调用原对象的 valueOf 方法,如果得到还不是原始值,则报错。只要得到原始值,则将原始值转换为字符串返回。

// Object.prototype.toString() 返回的对象的类型字符串
String({}); // "[object Object]"

// Array.prototype.toString() 会对数组的每个元素进行字符串转换,最后用逗号(,)拼接成字符串
String([1,"abc",{}]); // "1,abc,[object Object]"

注意,数组的 toString 方法会将 nullundefined 转为空字符串,和 String() 函数的结果不同。

(2) 强制转换为数值(number)

可以用 Number 函数将除了 Symbol 类型外的值转换为数值。

当转换原始值为数值时,转换结果如下:

Number(''); // 0
Number('123'); // 123
Number('123.45'); // 123.45
Number('123abc'); // NaN
Number(123); // 123
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN
Number(Symbol()); // TypeError: Cannot convert a Symbol value to a number
Number(123n); // 123

当转换对象为数值时,首先调用对象的 valueOf 方法,如果得到的不是原始值,则调用原对象的 toString 方法,如果得到的还不是原始值,则报错。只要得到原始值,则转换为数值并返回。

// 狭义对象的 valueOf 返回的对象本身,所以继续调用 toString,结果得到 "[object Object]",转换为数值得到 NaN
Number({}); // NaN

// 同样的,数组的 valueOf 返回的对象,只能继续调 toString,得到 "",转换为数值得到 0
Number([]); // 0
Number([5]); // 5
Number([5,6]); // NaN

(3) 强制转换为布尔值(boolean)

可以用 Boolean 函数将任意值转换为布尔值,规则比较简单。

以下值转换为布尔值时为 false

空字符串(""''
零(0+0-0
非数值(NaN
假值(false
null
undefined

其他的值转换为布尔值时都为 true

Boolean(''); // false
Boolean('abc'); // true
Boolean(0); // false
Boolean(NaN); // false
Boolean(false); // false
Boolean(true); // true
Boolean(null); // false
Boolean(undefined); // false
Boolean({}); // true
Boolean([]); // true
Boolean(new Boolean(false)); // true

(4) 自动转换

自动转换是以强制转换为基础的,遇到以下几种情况,JavaScript 会进行数据类型的自动转换,即自动进行强制转换。

情况一:加法运算(+

加法运算符(+)用来求两个数值的和,而 JavaScript 允许非数值的相加,会存在类型的自动转换。

当两个运算值全为数值时,进行正常的数值相加:

1 + 1 // 2

当两个运算值全为字符串时,加法运算符会变成连接运算符,进行字符串的拼接:

'123' + 'abc' // "123abc"

当一个运算值为字符串,而另一个为非字符串,则将非字符串自动转换为字符串,进行拼接:

'1' + 23; // "123"
23 + '1'; // "231"
'1' + true; // "1true"
'1' + false; // "1false"
'1' + null; // "1null"
'1' + undefined; // "1undefined"
'1' + {}; // "1[object Object]"
'1' + ['2',3,true,{}]; // "123true[object Object]"

当运算值中有对象时,会自动转换对象为原始值,再相加:

通常情况下,对象转原始值会得到字符串,所以最终会进行字符串的拼接

1 + {}; // "1[object Object]"
1 + ['2']; // "12"
情况二:除了情况一外的算术运算

除了情况一外,进行不同类型值运算时,包括一元运算符,都会把值自动转换为数值。

'5' - '2'; // 3
'5' * '2'; // 10
'5' - 2; // 3
'5abc' - 1; // NaN
true - 1; // 0
false - 1; // -1
'5' * []; // 0
null + 1; // 1
undefined + 1; // NaN
+'abc'; // NaN
+'123'; // 123
-'123'; // -123
+true; // 1
-true; // -1
+false; // 0
-false; // -0
情况三:条件判断(if、while)与布尔运算(!、&&、||、?:)

当预期为布尔值时,会将相应值自动转换为布尔值。

if(''&&![]){
    console('空字符串转换为布尔值后为 false,这里永远也不会执行');
}
情况四:比较运算(>、<、>=、<=、==、!=、===、!==)

当进行非相等比较(><>=<=)时,如果比较的两个运算值都是字符串,则按字符比较其 Unicode 码点:

'a' > 'A' // true
'ab' > 'a' // true

如果有运算值不是字符串,则将原始值转换为数值进行比较:

'2' > 1 // true
true > false // true
2 > true // true

如果有运算值是对象,则先将对象转换为原始值,再根据上述的情况进行判断:

5 > ['4'] // true
[2] > '11' // true

注意 NaN 与任何值比较都是 false

NaN > 0 // false
NaN >= NaN //false
1 > NaN // false

当进行相等比较(==!====!==)时,相等和严格相等不一样。

严格相等(===)会验证类型是否一致,不会进行类型转换,如果一致,则原始值比较值是否一致,对象比较地址是否一致:

1 === '1' // false
1 === 1 // true
+0 === -0 // true
{} === {} // false
var o1 = {};
var o2 = o1;
o1 === o2 // true

注意,NaN 和任何值比较都是 false

相等(==)如果两个运算值类型一致,则和严格相等(===)一样,否则原始值转换为数值,对象转换为原始值后再根据情况比较:

1 == '1' // true
1 == true // true
0 == false // true
0 == '' // true
[1,2] == '1,2' // true

注意,nullundefined 只有和自身,或者两者之间比较才是 true,其他为 false

null == null // true
undefined == undefined // true
null == undefined // true

(完)