JS隐式类型转换

308 阅读8分钟

对于类型转换,一直都是很模糊的概念。用到的时候不是现查,就是在浏览器上输出下结果,从没考虑过背后的原理。最近在网上搜了一些帖子,大都是只言片语,不成体系。今天决定自己总结一下,学习的同时,也能给大家分享一下成果。

先来简单测试一下:如果下面的输入结果都能回答正确,且知道其中缘由的话,您可以看点别的了,大佬,再见!

1 + 1 // 
1 + '1' // 
1 + true // 
1 + undefined // 

1 + null // 
1 + [] // 
1 + [1] //  

'1' == 1 // 
'1' === 1 //
true == 1 //
[] == 0 //
null == undefined //

null == 0 //
null > 0 //
null >= 0 //

1. JavaScript数据类型

在正式开始之前,先来回顾一下基础知识。

JavaScript的数据类型分为两类:原始类型(primitive type)和对象类型(object type)。原始类型包括数字、字符串和布尔值,除此之外,还有两个特殊的原始值:null(空)和undefined(未定义)。在JavaScript中除数字、字符串、布尔值、null和undefined之外的就是对象了。

2. 类型转换

JavaScript的取值类型非常灵活,当JavaScript期望使用某个类型的值的时候,你可以提供任意类型值,JavaScript将根据需要自行转换类型:当JavaScript期望使用一个布尔值时,它把给定的值转为布尔值;如果JavaScript期望使用一个字符串,它把给定的值转为字符串;如果JavaScript期望使用一个数字,它把给定的值转为数字。

以下简要列举了在JavaScript中不同类型值之间的转换关系:

字符串数字布尔值
undefined"undefined"NaNfalse
null"null"0false
true"true"1
false"false"0
""(空字符串)0false
"1.2"(非空,数字)1.2true
"one"(非空,非数字)NaNtrue
0"0"false
-0"0"false
NaN"NaN"false
Infinity"Infinity"true
-Infinity"-Infinity"true
1(非零数字)"1"true

2.1 对象转为布尔值

对象到布尔值的转换非常简单:所有的对象都转换为true。

2.2 对象转为字符串

JavaScript中对象转为字符串会经历以下过程:

  • 如果对象具有toString()方法,则调用这个方法。如果它返回一个原始值,再将这个值转换为字符串之后,作为返回结果(转换关系见上表);
  • 如果对象没有toString()方法,或者这个方法并没有返回一个原始值,那么会调用对象的valueOf()方法。如果存在这个方法,且返回了一个原始值,JavaScript将这个值转换为字符串返回;
  • 如果对象没有valueOf方法,或者这个方法并没有返回一个原始值,这是将抛出一个类型错误。
let obj = {
  toString() {
    console.log('1');
    return {};
  },
  valueOf() {
    console.log('2');
    return {};
  }
};

String(obj);

// 1
// 2
// TypeError: Cannot convert object to primitive value

2.3 对象转为数字

在对象转为数字的过程中,JavaScript做了同样的事情,只不过调用顺序有所变化:先调用valueOf()方法,再调用toString()方法。

这个过程解释了为什么空数组会转换为数字0,以及为什么具有单个数字元素的数组同样会转换为数字:

console.log(Number([])); // 0 过程: [] -> '' -> 0
console.log(Number([1])); // 1
console.log(Number([1, 2])); // NaN

数组继承了默认的valueOf()方法,这个方法返回一个对象,而不是原始值,因此调用toString()方法,再将字符串转换为数字。

注意,数组重写了toString()方法,返回结果与数组的join()方法一致:

let arr = [1, 2, 3];
Object.prototype.toString.call(arr); // '[object Array]'
arr.toString(); // '1,2,3'
arr.join(); // '1,2,3'

好了,以上是基础知识复习,接下来是我们的重头戏:隐式类型转换。

3. 隐式类型转换

上面说过,当JavaScript期望使用某个类型的值的时候,你可以提供任意类型值,JavaScript将根据需要自行转换类型,JavaScript中的某些运算符会做隐式的类型转。所以,弄清楚程序运行时JavaScript期望的数据类型,是解决隐式类型转换问题的关键。

下面,我们逐一分析一下JavaScript运行时的情况。

3.1 加法运算符"+"

加法运算符的转换规则优先考虑字符串连接,具体的行为表现为:

  • 如果其中一个操作数是对象,则先会将对象转换为原始值,对象到原始值的转换基本上是对象到数字的转换:先调用valueOf(),如果不存在这个方法或者返回的不是原始值,再调用toString()方法进行转换;
  • 在进行了对象的原始值转换后,如果其中一个操作数是字符串的话,那么另一个操作数也会转换为字符串,然后进行字符串连接;
  • 否则两个操作数都将转换为数字(或者NaN),然后进行加法操作。
1 + 1 // 2 两边都是数字
1 + '1' // '11' 有一边是字符串,则转为字符串
1 + true // 2 两边都没有字符串,则都转为数字,然后相加,下同
1 + undefined // NaN 
1 + null // 0
1 + [] // '1'
1 + [1] // '11'

最后两个数组先调用valueOf(),返回的还是数组,不是原始值。所以再调用toString(),返回字符串。上面提到空数组转为数字时是0,是因为使用了Number方法,显式的将数组转为数字。

最后,需要特别注意的是,当加号运算符有多个运算时,要考虑加法的结合性对运算顺序的影响:

1 + 2 + 'string' // '3string'
1 + (2 + 'string') // '12string'

3.2 相等和不等运算符( == != === !==)

先说“==”,其行为表现如下:

  • 如果一个值是null,另一个是undefined,则他们相等;
  • 如果一个是字符串,另一个是数字,则将字符串转换为数字再进行比较;
  • 如果其中一个是布尔值,则将其转换为数字再进行比较;
  • 如果一个是对象,另一个是原始值,则将对象转换为原始值,再依据上面规则进行比较;

简单总结一下:除了null 和 undefined之外,如果运算符两边的操作数都是字符串,则直接比较;如果不是,那么将其转换为为数字比较。

'1' == 1 // true 将'1'转换为1
true == 1 // true 将true转换为1
[] == 0 // true []先转换为空字符串'',再转为数字0

//{}先调用valueOf返回对象,然后调用toString() 返回'[object Object]'
({}) == '[object Object]' // true 

var obj = {
  valueOf() {
    return {};
  },
  toString() {
    return '1';
  }
}
//obj先调用valueOf返回对象,然后调用toString() 返回字符串'1',再转为数字1
console.log(obj == 1) // true

null 和 undefined 比较特殊,除了它们两个互相相等之外,与其他值都不相等

null == undefined // true
null == 0 // false
null == false  // false

另外,NaN是一个比较特殊的值,它和任何值都不相等,包括它自己:

 NaN == NaN // false

利用上面讲的最后一条规则,我们可以做一些有趣的事情,来看一道经典的笔试题:

var a = ???; // 如何完善a,使其正确打印'success'
if(a == 1 && a == 2 && a == 3){
    console.log('success');
}

//方案:利用类型转换,重写valueOf,返回一个自增的值
var a = {
    num: 1,
    valueOf(){
        return this.num++;
    }
};

严格相等运算符“===”首先计算操作数的值,然后进行比较,比较过程中不会有类型转换。如果类型不同,则不相等。

let obj = {
  valueOf() {
    conosle.log('1');
    return {};
  },
  toString() {
    conosle.log('2');
    return 'obj';
  }
}

//不进行类型转换,没有输出 1 和 2
console.log(obj === 'obj') // false

如果两边都是对象,比较的是对象的引用,而不是值的比较。两个对象即使属性数量和值相同也不相等。

let o1 = { x: 1 };
let o2 = { x: 2 };

o1 == o2 // false
o1 == o1 // true

“!=” 和 “!==” 是相等运算符结果的取反,这里就不赘述了。

3.3 比较运算符(> < >= <=)

比较运算符的操作数可以是任意类型。然而,只有数字和字符串才能进行真正的比较,不是字符串和数字的将进行以下类型转换:

  • 如果操作数是对象,则将对象转为原始值;
  • 如果转换后两个值都是字符串,那么依照字母表的顺序对两个字符串进行比较(字母表顺序是指组成这个字符串的16位Unicode字符的索引顺序,下面会说);
  • 如果转换后有一个操作数不是字符串,那么两个操作数都转换为数字进行比较;
  • 0 和 -0 是相等的;Infinity比任何数字都大,-Infinitiy比任何数字都小;如果其中一个操作数是NaN,那么结果总是返回false。
console.log('2' > 10) // false '2'转换为2,小于10

//使用字符串的charCodeAt()方法,可以查看字符的Unicode编码值
console.log('2'.charCodeAt()); // 50
console.log('10'.charCodeAt()); // 49
console.log('2' > '10') // true

//逐位依次比较
console.log('a'.charCodeAt()); // 97
console.log('b'.charCodeAt()); // 98
console.log('abc' > 'b') // false
console.log('abc' > 'aac') // true

3.4 逻辑运算符( ! )

“!”是一元运算符,目的是将操作数转为布尔值后求反。上面说过,所有的对象转为布尔值时都为true,所以(“!” + 对象)的值都为false。 需要注意的是,“!”的优先级高于“==”,进行比较时,先求反,再比较:

console.log([] == 0) // true
console.log(![] == 0) // true

console.log({} == 0) // false
console.log(!{} == 0) // true

console.log(!{} == {}) // false
console.log({} == {}) // false

3.5 一元操作数( + - )

和上面的二元运算符不同,这里要讲的“+”、“-",相当于调用Number,将操作数转换为数值类型:

console.log(+[]) // 0
console.log(+{}) // NaN
console.log(+null) // 0
console.log(+undefined) // NaN
console.log(+'') // 0
console.log(+'1') // 1
console.log(+'abc') // NaN

console.log(+'1' === 1) // true
console.log('1' === 1) // false

console.log(-'0' === 0) // true
console.log(-'' === 0) // true

其他算术运算符(- * / %)的行为与上面一致,也是将操作数转为数字。</font size=2>

3.6 if和while语句

if (expression) {
  statement
}

上面的表达式中,需要先计算expression的值,如果计算结果为真,那么就执行statement。expression需要是一个布尔值,所以不论传入什么类型,都会将它转为布尔值。

if ([]) {
  console.log(1)
}

if (!0) {
  console.log(2);
}

if (!'0') {
  console.log(3);
}

//输出 1 2

4. 结束

好了,到此为止,类型转换的问题基本上梳理的差不多了,希望对大家有一些帮助。如果还有不清楚地方,可以循环阅读,或者再参照一些其他资料。如果有错误或者遗漏的地方,欢迎指正。

备注:本文大部分内容都摘自《JavaScript权威指南》。