1 隐式强制类型转换
隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,你自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。
1.1 字符串和数字之间的隐式强制类型转换
首先尝试理解下面的代码:
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
a和b都不是字符串,但是它们都被强制转换为字符串然后进行拼接。原因何在?
扩展: 根据ES5规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作,该抽象操作再调用[[DefaultValue]],以数字作为上下文。
你或许注意到这与ToNumber抽象操作处理对象的方式一样(参见JavaScript类型和语法二)。因为数组的valueOf()操作无法得到简单基本类型值,于是它转而调用toString()。因此上例中的两个数组变成了"1,2"和"3,4"。+将它们拼接后返回"1,23,4"。
简单来说就是,如果+的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),则执行字符串拼接;否则执行数字加法。
案例2:
var a = {
valueOf: function() { return 42; },
toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"
a + ""(隐式)和String(a)(显式)之间有一个细微的差别需要注意。根据ToPrimitive抽象操作规则,a + ""会对a调用valueOf()方法,然后通过ToString抽象操作将返回值转换为字符串。而String(a)则是直接调用ToString()。
字符串转数字
var a = "3.14";
var b = a -0;
b; // 3.14
-是数字减法运算符,因此a -0会将a强制类型转换为数字。也可以使用a * 1和a /1,因为这两个运算符也只适用于数字,只不过这样的用法不太常见。
对象的-操作与+类似:
var a = [3];
var b = [1];
a - b; // 2
为了执行减法运算,a和b都需要被转换为数字,它们首先被转换为字符串(通过toString()),然后再转换为数字。
1.2 布尔值到数字的隐式强制类型转换
案例:
如果其中有且仅有一个参数为true,则onlyOne(..)返回true。
let a = true
let b = false
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
// 跳过假值,和处理0一样,但是避免了NaN
if (arguments[i]) {
sum += arguments[i];
}
}
return sum == 1;
}
var a = true;
var b = false;
onlyOne( b, a ); // true
onlyOne( b, a, b, b, b ); // true
onlyOne( b, b ); // false
onlyOne( b, a, b, b, b, a ); // false
同样的功能也可以通过显式强制类型转换来实现,如下:
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
sum += Number( ! ! arguments[i] );
}
return sum === 1;
}
!! arguments[i]首先将参数转换为true或false。 通过sum += arguments[i]中的隐式强制类型转换,将真值(true/truthy)转换为1并进行累加。 如果有且仅有一个参数为true,则结果为1;否则不等于1, sum == 1条件不成立。
1.3 隐式强制类型转换为布尔值
相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生布尔值隐式强制类型转换。
- if (..)语句中的条件判断表达式。
- for ( .. ; .. ; .. )语句中的条件判断表达式(第二个)。
- while (..)和do..while(..)循环中的条件判断表达式。
- ? :中的条件判断表达式。
- 逻辑运算符||(逻辑或)和&&(逻辑与)左边的操作数(作为条件判断表达式)。
以上情况中,非布尔值会被隐式强制类型转换为布尔值,遵循前面介绍过的ToBoolean抽象操作规则,例如:
var a = 42;
var b = "abc";
var c;
var d = null;
if (a) {
console.log( "yep" ); // yep
}
while (c) {
console.log( "nope, never runs" );
}
c = d ? a : b;
c; // "abc"
if ((a && d) || c) {
console.log( "yep" ); // yep
}
1.4 ||和&&
和其他语言不同,在JavaScript中它们返回的并不是布尔值。
它们的返回值是两个操作数中的一个(且仅一个)。即选择两个操作数中的一个,然后返回它的值。
引述ES5规范:&&和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
var a = 42;
var b = "abc";
var c = null;
a || b; // 42
a && b; // "abc"
c || b; // "abc"
c && b; // null
||和&&首先会对第一个操作数(a和c)执行条件判断,如果其不是布尔值(如上例)就先进行ToBoolean强制类型转换,然后再执行条件判断。
对于||来说,如果条件判断结果为true就返回第一个操作数(a和c)的值,如果为false就返回第二个操作数(b)的值。
&&则相反,如果条件判断结果为true就返回第二个操作数(b)的值,如果为false就返回第一个操作数(a和c)的值。
1.4.1 ||
下面是一个十分常见的||的用法,也许你已经用过但并未完全理解:
function foo(a, b) {
a = a || "hello";
b = b || "world";
console.log( a + " " + b );
}
foo(); // "hello world"
foo( "yeah", "yeah! " ); // "yeah yeah! "
a = a || "hello"(又称为C#的“空值合并运算符”的JavaScript版本)检查变量a,如果还未赋值(或者为假值),就赋予它一个默认值("hello")。
需要注意:
foo( "That's it! ", "" ); // "That's it! world" <-- 晕!
第二个参数""是一个假值(falsy value),因此b = b || "world"条件不成立,返回默认值"world"。
1.4.2 &&
有一种用法对开发人员不常见,然而JavaScript代码压缩工具常用。就是如果第一个操作数为真值,则&&运算符“选择”第二个操作数作为返回值,这也叫作“守护运算符”,即前面的表达式为后面的表达式“把关”:
function foo() {
console.log( a );
}
var a = 42;
a && foo(); // 42
foo()只有在条件判断a通过时才会被调用。如果条件判断未通过,a && foo()就会悄然终止(也叫作“短路”, short circuiting), foo()不会被调用。
1.5 符号的强制类型转换
ES6中引入了符号类型,它的强制类型转换有一个坑,在这里有必要提一下。ES6允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError
符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是true)。
2 宽松相等和严格相等
宽松相等(loose equals)==和严格相等(strict equals)===都用来判断两个值是否“相等”,但是它们之间有一个很重要的区别,特别是在判断条件上。
常见的误区是“==检查值是否相等,===检查值和类型是否相等”。听起来蛮有道理,然而还不够准确。 很多JavaScript的书籍和博客也是这样来解释的,但是很遗憾他们都错了。正确的解释是:“==允许在相等比较中进行强制类型转换,而===不允许。”
根据第一种解释(不准确的版本), ===似乎比==做的事情更多,因为它还要检查值的类型。第二种解释中==的工作量更大一些,因为如果值的类型不同还需要进行强制类型转换。
==和===都会检查操作数的类型。区别在于操作数类型不同时它们的处理方式不同。
2.1 抽象相等
ES5规范定义了对象(包括函数和数组)的宽松相等==。两个对象指向同一个值时即视为相等,不发生强制类型转换。在比较两个对象的时候; ==和===的工作原理是一样的。
ES5规范中还规定,==在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或两者都转换为相同的类型后再进行比较。
2.2 字符串和数字之间的相等比较
var a = 42;
var b = "42";
a === b; // false
a == b; // true
因为没有强制类型转换,所以a === b为false,42和"42"不相等。 而a == b是宽松相等,即如果两个值的类型不同,则对其中之一或两者都进行强制类型转换。
具体怎么转换?是a从42转换为字符串,还是b从"42"转换为数字?
ES5规范11.9.3.4-5这样定义: (1) 如果Type(x)是数字,Type(y)是字符串,则返回x == ToNumber(y)的结果。 (2) 如果Type(x)是字符串,Type(y)是数字,则返回ToNumber(x) == y的结果。
根据规范,"42"应该被强制类型转换为数字以便进行相等比较。
2.3 其他类型和布尔类型之间的相等比较
var a = "42";
var b = true;
a == b; // false
我们都知道"42"是一个真值(见本章前面部分),为什么==的结果不是true呢?
规范11.9.3.6-7是这样说的: (1) 如果Type(x)是布尔类型,则返回ToNumber(x) == y的结果; (2) 如果Type(y)是布尔类型,则返回x == ToNumber(y)的结果。
Type(x)是布尔值,所以ToNumber(x)将true强制类型转换为1,变成1 == "42",二者的类型仍然不同,"42"根据规则被强制类型转换为42,最后变成1 == 42,结果为false。
也就是说,字符串"42"既不等于true,也不等于false。一个值怎么可以既非真值也非假值,这也太奇怪了吧?
"42"是一个真值没错,但"42" == true中并没有发生布尔值的比较和强制类型转换。 这里不是"42"转换为布尔值(true),而是true转换为1, "42"转换为42。
这里并不涉及ToBoolean,所以"42"是真值还是假值与==本身没有关系!
重点是我们要搞清楚==对不同的类型组合怎样处理。==两边的布尔值会被强制类型转换为数字。很奇怪吧? 个人建议无论什么情况下都不要使用== true和== false。
2.4 null和undefined之间的相等比较
null和undefined之间的==也涉及隐式强制类型转换。
ES5规范11.9.3.2-3规定: (1) 如果x为null, y为undefined,则结果为true。 (2) 如果x为undefined, y为null,则结果为true。
在==中null和undefined相等(它们也与其自身相等),除此之外其他值都不存在这种情况。这也就是说,在==中null和undefined是一回事,可以相互进行隐式强制类型转换:
var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
总结: null和undefined可以进行隐性强制类型转换,但除此之外,与其他任何假值(falsey value)比较,都会返回false,如上例中的'', 0, false
null和undefined之间的强制类型转换是安全可靠的,上例中除null和undefined以外的其他值均无法得到假阳(false positive)结果。个人认为通过这种方式将null和undefined作为等价值来处理比较好。
再看下面的例子:
var a = doSomething();
if (a == null) {
// ..
}
条件判断a == null仅在doSomething()返回null和undefined时才成立,除此之外其他值都不成立,包括0、false和""这样的假值。
2.5 对象和非对象之间的相等比较
关于对象(对象/函数/数组)和标量基本类型(字符串/数字/布尔值)之间的相等比较, ES5规范11.9.3.8-9做如下规定: (1) 如果Type(x)是字符串或数字,Type(y)是对象,则返回x == ToPrimitive(y)的结果; (2) 如果Type(x)是对象,Type(y)是字符串或数字,则返回ToPrimitive(x) == y的结果。
这里只提到了字符串和数字,没有布尔值。原因是我们之前介绍过11.9.3.6-7中规定了布尔值会先被强制类型转换为数字。
即布尔值和任意类型的值比较时,都会先将布尔值强转为数字。
var a = 42;
var b = [ 42 ];
a == b; // true
[ 42 ]首先调用ToPrimitive抽象操作,返回"42",变成"42" == 42,然后又变成42== 42,最后二者相等。
var a = "abc";
var b = Object( a ); // 和new String( a )一样
a === b; // false
a == b; // true
a == b结果为true,因为b通过ToPrimitive进行强制类型转换(也称为“拆封”,英文为unboxed或者unwrapped),并返回标量基本类型值"abc",与a相等。
但有一些值不这样,原因是==算法中其他优先级更高的规则。例如:
var a = null;
var b = Object( a ); // 和Object()一样
a == b; // false
var c = undefined;
var d = Object( c ); // 和Object()一样
c == d; // false
var e = NaN;
var f = Object( e ); // 和new Number( e )一样
e == f; // false
因为没有对应的封装对象,所以null和undefined不能够被封装(boxed), Object(null)和Object()均返回一个常规对象。NaN能够被封装为数字封装对象,但拆封之后NaN == NaN返回false,因为NaN不等于NaN。
2.6 比较少见的情况
2.6.1 返回其他数字
Number.prototype.valueOf = function() {
return 3;
};
new Number( 2 ) == 3; // true
2 == 3不会有这种问题,因为2和3都是数字基本类型值,不会调用Number.prototype.valueOf()方法。 而Number(2)涉及ToPrimitive强制类型转换,因此会调用valueOf()。
还有更奇怪的现象:
if (a == 2 && a == 3) {
// ..
}
你也许觉得这不可能,因为a不会同时等于2和3。但“同时”一词并不准确,因为a ==2在a == 3之前执行。
如果让a.valueOf()每次调用都产生副作用,比如第一次返回2,第二次返回3,就会出现这样的情况。这实现起来很简单:
var i = 2;
Number.prototype.valueOf = function() {
return i++;
};
var a = new Number( 42 );
if (a == 2 && a == 3) {
console.log( "Yep, this happened." );
}
2.6.2 假值的相等比较
==中的隐式强制类型转换最为人诟病的地方是假值的相等比较。
下面分别列出了常规和非常规的情况:
"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- 晕! -> '0' == 0 -> 0 == 0
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 晕! -> 0 == 0
false == ""; // true -- 晕! -> 0 == '' -> 0 == 0(注意:Number('') = 0 )
false == []; // true -- 晕! -> 0 == '' -> 0 == 0( 注意:[].toString() = '' )
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- 晕! -> 0 == 0
"" == []; // true -- 晕! -> '' == ''
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 晕!-> 0 == '' -> 0 == 0
0 == {}; // false
2.6.3 极端情况
这还不算完,还有更极端的例子:
[] == ! [] // true
转换过程:
-
首先
[]调用toString()转为'',![]会将[]强转为布尔值,[]是对象,所以![]时false变成了:'' == false -
接着
false会转成数字0(布尔值只能转数字) 变成了:'' == 0 -
最后
''转为数字0, 变成了:0 == 0
2 == [2]; // true
"" == [null]; // true
[2]会先调用数组的valueOf()方法,发现返回的时数组[2],并非基本数据类型,进而调用数组的toString()方法,返回'2','2' == 2 自然返回true
[null]同样调用valueOf()未果后,尝试调用toString(),将[null]转为'', '' == '' 也自然为true
0 == "\n"; // true
""、"\n"(或者" "等其他空格组合)等空字符串被ToNumber强制类型转换为0,所以返回true
2.6.4 安全运用隐式强制类型转换
我们要对==两边的值认真推敲,以下两个原则可以让我们有效地避免出错。
- 如果两边的值中有true或者false,千万不要使用==。
- 如果两边的值中有[]、""或者0,尽量不要使用==。
这时最好用===来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制类型转换的坑。
3 抽象关系比较
a < b中涉及的隐式强制类型转换不太引人注意,不过还是很有必要深入了解一下。
ES5规范11.8.5节定义了“抽象关系比较”(abstract relational comparison),分为两个部分: 比较双方都是字符串(后半部分)和其他情况(前半部分)。
比较双方首先调用ToPrimitive,如果结果出现非字符串,就根据ToNumber规则将双方强制类型转换为数字来进行比较。
var a = [ 42 ];
var b = [ "43" ];
a < b; // true
b < a; // false
如果比较双方都是字符串,则按字母顺序来进行比较:
var a = [ "42" ];
var b = [ "043" ];
a < b; // false
a和b并没有被转换为数字,因为ToPrimitive返回的是字符串,所以这里比较的是"42"和"043"两个字符串,它们分别以"4"和"0"开头。因为"0"在字母顺序上小于"4",所以最后结果为false。
同理:
var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];
a < b; // false
a转换为"4, 2", b转换为"0, 4, 3",同样是按字母顺序进行比较。
再比如:
var a = { b: 42 };
var b = { b: 43 };
a < b; // ? ?
结果还是false,因为a是[object Object], b也是[object Object],所以按照字母顺序a < b并不成立。
下面的例子就有些奇怪了:
var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true
为什么a == b的结果不是true?它们的字符串值相同(同为"[object Object]"),按道理应该相等才对?实际上不是这样,你可以回忆一下前面讲过的对象的相等比较(只有两个对象指向同一块内存区域才相等,二者比较期间不存在ToPrimitive)。
但是如果a < b和a == b结果为false,为什么a <= b和a >= b的结果会是true呢?
因为根据规范a <= b被处理为b < a,然后将结果反转。因为b < a的结果是false,所以a <= b的结果是true。
这可能与我们设想的大相径庭,即<=应该是“小于或者等于”。实际上JavaScript中<=是“不大于”的意思(即!(a > b),处理为!(b < a))。同理,a >= b处理为b <= a。
相等比较有严格相等,关系比较却没有“严格关系比较”(strict relational comparison)。也就是说如果要避免a < b中发生隐式强制类型转换,我们只能确保a和b为相同的类型,除此之外别无他法。
想要了解更详细的内容,见《你不知道的JS》中卷第一部分第四章