隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。
显式强制类型转换旨在让代码更加清晰易读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得晦涩难懂。隐式强制类型转换的作用是减少冗余,让代码更简洁。
隐式地简化
隐式转换可以省去中间步骤,简化代码,抽象和隐藏那些细枝末节,有助于提高代码的可读性。
字符串和数字之间的隐式强制类型转换
通过重载,+ 运算符即能用于数字加法,也能用于字符串拼接。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 ); // trueonlyOne( b, a, b ); // trueonlyOne( 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一样,但是避免了NaNif (arguments[i]) {sum += arguments[i];}}return sum == 1;}var a = true;var b = false;onlyOne( b, a ); // trueonlyOne( b, a, b, b, b ); // trueonlyOne( b, b ); // falseonlyOne( b, a, b, b, b, a ); // false
在 onlyOne(..) 中除了使用 for 循环,还可以使用 ES5 规范中的 reduce(..) 函数。
function onlyOne() {var sum = 0Array.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 ); // trueonlyOne( b, a, b, b, b ); // trueonlyOne( b, b ); // falseonlyOne( 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; // 42a && 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)。
