隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。
显式强制类型转换旨在让代码更加清晰易读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得晦涩难懂。隐式强制类型转换的作用是减少冗余,让代码更简洁。
隐式地简化
隐式转换可以省去中间步骤,简化代码,抽象和隐藏那些细枝末节,有助于提高代码的可读性。
字符串和数字之间的隐式强制类型转换
通过重载,+
运算符即能用于数字加法,也能用于字符串拼接。js 怎样来判断我们要执行的是哪个操作?例如:
var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; // "420"
c + d; // 42
为什么会有两个结果?通常的理解是,因为某一个或者两个操作数都是字符串,所以 +
执行的是字符串拼接操作。这样解释只对了一半,实际情况要复杂得多。
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
a 和 b 都不是字符串,但是它们都被强制转换为字符串然后进行拼接。
根据 ES5 规范 11.6.1 节,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+
将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作(规范 9.1 节),该抽象操作再调用 [DefaultValue],以数字作为上下文。
或许注意到这与 ToNumber 抽象操作处理对象的方式一样。因为数组的 valueOf()
操作无法得到简单基本类型值,于是它转而调用 toString()
。因此上例中的两个数组变成了 "1,2"
和 "3,4"
。+
将它们拼接后返回 "1,23,4"
。
如果 +
的其中一个操作数是字符串(或者通过以上步骤可以得到字符串), 则执行字符串拼接;否则执行数字加法。
有一个坑常常被提到,即 [] + {}
和 {} + []
,它们返回不同的结果,分别是 "[object Object]"
和 0
。
可以将数字和空字符串 ""
相 +
来将其转换为字符串:
var a = 42;
var b = a + "";
b; // "42"
a + ""
(隐式)和前面的String(a)
(显式)之间有一个细微的差别需要注意。根据 ToPrimitive抽象操作规则,a + ""
会对a
调用valueOf()
方法,然后通过ToString
抽象操作将返回值转换为字符串。而 String(a)
则是直接调用 ToString()
。
它们最后返回的都是字符串,但如果 a 是对象而非数字结果可能会不一样!
var a = {
valueOf: function() { return 42; },
toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"
一般不太可能会遇到上述问题。在定制 valueOf()
和 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()
),然后再转换为数字。
布尔值到数字的隐式强制类型转换
function onlyOne(a,b,c) {
return !!((a && !b && !c) ||
(!a && b && !c) || (!a && !b && c));
}
var a = true;
var b = false;
onlyOne( a, b, b ); // true
onlyOne( b, a, b ); // true
onlyOne( a, b, a ); // false
如果其中有且仅有一个参数为 true
,则 onlyOne(..)
返回 true
。其在条件判断中使用了隐式强制类型转换,其他地方则是显式的,包括最后的返回值。
但如果有多个参数时(4 个、5 个,甚至 20 个),用上面的代码就很难处理了。这时就可以使用从布尔值到数字(0 或 1)的强制类型转换:
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
在 onlyOne(..)
中除了使用 for 循环,还可以使用 ES5 规范中的 reduce(..)
函数。
function onlyOne() {
var sum = 0
Array.prototype.slice.call(arguments).reduce((accumulator, currentValue, index) => {
if (arguments[index]) {
return sum += accumulator + currentValue
}
})
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
通过sum += arguments[i]
中的隐式强制类型转换,将真值(true/truthy)转换为1
并进行累加。如果有且仅有一个参数为 true
,则结果为 1
;否则不等于 1
,sum == 1
条件不成立。
同样的功能也可以通过显式强制类型转换来实现:
function onlyOne() {
var sum = 0;
for (var i=0; i < arguments.length; i++) {
sum += Number( !!arguments[i] );
}
return sum === 1;
}
!!arguments[i]
首先将参数转换为 true
或 false
。因此非布尔值参数在这里也是可以的, 比如:onlyOne("42", 0)
(否则的话,字符串会执行拼接操作,这样结果就不对了)。转换为布尔值以后,再通过 Number(..)
显式强制类型转换为 0
或 1
。
隐式强制类型转换为布尔值
相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生 布尔值隐式强制类型转换。
if (..)
语句中的条件判断表达式。for ( .. ; .. ; .. )
语句中的条件判断表达式(第二个)。while (..)
和do..while(..)
循环中的条件判断表达式。- 三元表达式
? :
中的条件判断表达式。 - 逻辑运算符
||
(逻辑或)和&&
(逻辑与)左边的操作数(作为条件判断表达式)。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
}
|| 和 &&
作者不太赞同将它们称为“逻辑运算符”,因为这不太准确。称它们为“选择器运算符”(selector operators)或者“操作数选择器运算符”(operand selector operators)更恰当些。
因为在js中它们返回的不是布尔值,而是两个操作数中的一个(且仅一个)。
引述 ES5 规范 11.11 节:&&
和||
运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
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
)的值。||
和 &&
返回它们其中一个操作数的值,而非条件判断的结果(其中可能涉及强制类型转 换)。c && b
中c
为null
,是一个假值,因此&&
表达式的结果是null
(即c
的值),而非条件判断的结果 false
。
a || b;
// 大致相当于(roughly equivalent to):
a ? a : b;
a && b;
// 大致相当于(roughly equivalent to):
a ? b : a;
之所以说大致相当,是因为它们返回结果虽然相同但是却有一个细微的差别。在a ? a : b
中,如果a
是一个复杂一些的表达式(比如有副作用的函数调用等),它有可能被执行两次(如果第一次结果为真)。而在 a|| b
中 a
只执行一次,其结果用于条件判断和返回结果(如果适用的话)。a && b
a ? b : a
和也是如此。
下面是一个十分常见的 ||
的用法,就是设置默认值:
function foo(a,b) {
a = a || "hello";
b = b || "world";
console.log( a + " " + b );
}
foo(); // "hello world"
foo( "yeah", "yeah!" ); // "yeah yeah!"
foo( "That’s it!", "" ); // "That’s it! world"
这种用法很常见,但是其中不能有假值,除非加上更明确的条件判断,或者转而使用? :
三元表达式。
如果第一个操作数为真值,则 &&
运算符“选择”第二个操作数作为返回值,这也叫作“守护运算符” (guard operator),即前面的表达式为后面的表达式“把关”:
function foo() {
console.log( 43 );
}
var a = 42;
a && foo(); // 43
foo()
只有在条件判断a
通过时才会被调用。如果条件判断未通过,a && foo()
就会终止(也叫作“短路”,short circuiting),foo()
不会被调用。
符号的强制类型转换
ES6 允许 从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。
var s1 = Symbol( "cool" );
String( s1 ); // "Symbol(cool)"
var s2 = Symbol( "not cool" );
s2 + ""; // TypeError: Cannot convert a Symbol value to a string
符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true
)。