隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。换句话说,自己觉得不够明显的强制类型转换都可以算作隐式强制类型转换。
显式强制类型转换旨在让代码更加清晰易读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得晦涩难懂。隐式强制类型转换的作用是减少冗余,让代码更简洁。

隐式地简化

隐式转换可以省去中间步骤,简化代码,抽象和隐藏那些细枝末节,有助于提高代码的可读性。

字符串和数字之间的隐式强制类型转换

通过重载,+ 运算符即能用于数字加法,也能用于字符串拼接。js 怎样来判断我们要执行的是哪个操作?例如:

  1. var a = "42";
  2. var b = "0";
  3. var c = 42;
  4. var d = 0;
  5. a + b; // "420"
  6. c + d; // 42

为什么会有两个结果?通常的理解是,因为某一个或者两个操作数都是字符串,所以 + 执行的是字符串拼接操作。这样解释只对了一半,实际情况要复杂得多。

  1. var a = [1,2];
  2. var b = [3,4];
  3. 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
可以将数字和空字符串 ""+ 来将其转换为字符串:

  1. var a = 42;
  2. var b = a + "";
  3. b; // "42"

a + ""(隐式)和前面的String(a)(显式)之间有一个细微的差别需要注意。根据 ToPrimitive抽象操作规则,a + ""会对a调用valueOf()方法,然后通过ToString抽象操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()
它们最后返回的都是字符串,但如果 a 是对象而非数字结果可能会不一样!

  1. var a = {
  2. valueOf: function() { return 42; },
  3. toString: function() { return 4; }
  4. };
  5. a + ""; // "42"
  6. String( a ); // "4"

一般不太可能会遇到上述问题。在定制 valueOf()toString() 方法时需要特别小心,因为这会影响强制类型转换的结果。
字符串强制类型转换为数字的情况:

  1. var a = "3.14";
  2. var b = a - 0;
  3. b; // 3.14

-是数字减法运算符,因此a - 0会将a强制类型转换为数字。也可以使用a * 1a / ``1,因为这两个运算符也只适用于数字,只不过这样的用法不太常见。
对象的 - 操作与 + 类似:

  1. var a = [3];
  2. var b = [1];
  3. a - b; // 2

为了执行减法运算,ab 都需要被转换为数字,它们首先被转换为字符串(通过toString()),然后再转换为数字。

布尔值到数字的隐式强制类型转换

  1. function onlyOne(a,b,c) {
  2. return !!((a && !b && !c) ||
  3. (!a && b && !c) || (!a && !b && c));
  4. }
  5. var a = true;
  6. var b = false;
  7. onlyOne( a, b, b ); // true
  8. onlyOne( b, a, b ); // true
  9. onlyOne( a, b, a ); // false

如果其中有且仅有一个参数为 true,则 onlyOne(..) 返回 true。其在条件判断中使用了隐式强制类型转换,其他地方则是显式的,包括最后的返回值。
但如果有多个参数时(4 个、5 个,甚至 20 个),用上面的代码就很难处理了。这时就可以使用从布尔值到数字(0 或 1)的强制类型转换:

  1. function onlyOne() {
  2. var sum = 0;
  3. for (var i=0; i < arguments.length; i++) {
  4. // 跳过假值,和处理0一样,但是避免了NaN
  5. if (arguments[i]) {
  6. sum += arguments[i];
  7. }
  8. }
  9. return sum == 1;
  10. }
  11. var a = true;
  12. var b = false;
  13. onlyOne( b, a ); // true
  14. onlyOne( b, a, b, b, b ); // true
  15. onlyOne( b, b ); // false
  16. onlyOne( b, a, b, b, b, a ); // false

onlyOne(..) 中除了使用 for 循环,还可以使用 ES5 规范中的 reduce(..) 函数。

  1. function onlyOne() {
  2. var sum = 0
  3. Array.prototype.slice.call(arguments).reduce((accumulator, currentValue, index) => {
  4. if (arguments[index]) {
  5. return sum += accumulator + currentValue
  6. }
  7. })
  8. return sum == 1;
  9. }
  10. var a = true;
  11. var b = false;
  12. onlyOne( b, a ); // true
  13. onlyOne( b, a, b, b, b ); // true
  14. onlyOne( b, b ); // false
  15. onlyOne( b, a, b, b, b, a ); // false

通过sum += arguments[i]中的隐式强制类型转换,将真值(true/truthy)转换为1并进行累加。如果有且仅有一个参数为 true,则结果为 1;否则不等于 1sum == 1 条件不成立。
同样的功能也可以通过显式强制类型转换来实现:

  1. function onlyOne() {
  2. var sum = 0;
  3. for (var i=0; i < arguments.length; i++) {
  4. sum += Number( !!arguments[i] );
  5. }
  6. return sum === 1;
  7. }

!!arguments[i]首先将参数转换为 truefalse。因此非布尔值参数在这里也是可以的, 比如:onlyOne("42", 0)(否则的话,字符串会执行拼接操作,这样结果就不对了)。转换为布尔值以后,再通过 Number(..) 显式强制类型转换为 01

隐式强制类型转换为布尔值

相对布尔值,数字和字符串操作中的隐式强制类型转换还算比较明显。下面的情况会发生 布尔值隐式强制类型转换。

  • if (..)语句中的条件判断表达式。
  • for ( .. ; .. ; .. )语句中的条件判断表达式(第二个)。
  • while (..)do..while(..) 循环中的条件判断表达式。
  • 三元表达式? :中的条件判断表达式。
  • 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。
    1. var a = 42;
    2. var b = "abc";
    3. var c;
    4. var d = null;
    5. if (a) {
    6. console.log( "yep" ); // yep
    7. }
    8. while (c) {
    9. console.log( "nope, never runs" );
    10. }
    11. c = d ? a : b;
    12. c; // "abc"
    13. if ((a && d) || c) {
    14. console.log( "yep" ); // yep
    15. }

    || 和 &&

    作者不太赞同将它们称为“逻辑运算符”,因为这不太准确。称它们为“选择器运算符”(selector operators)或者“操作数选择器运算符”(operand selector operators)更恰当些。
    因为在js中它们返回的不是布尔值,而是两个操作数中的一个(且仅一个)。
    引述 ES5 规范 11.11 节:

    &&|| 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。

  1. var a = 42;
  2. var b = "abc";
  3. var c = null;
  4. a || b; // 42
  5. a && b; // "abc"
  6. c || b; // "abc"
  7. c && b; // null

||&& 首先会对第一个操作数(a 和 c)执行条件判断,如果其不是布尔值(如上例)就先进行 ToBoolean 强制类型转换,然后再执行条件判断。
对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(ac)的值,如果为 false 就返回第二个操作数(b)的值。
&& 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返回第一个操作数(ac)的值。
||&& 返回它们其中一个操作数的值,而非条件判断的结果(其中可能涉及强制类型转 换)。c && bcnull,是一个假值,因此&&表达式的结果是null(即c的值),而非条件判断的结果 false

  1. a || b;
  2. // 大致相当于(roughly equivalent to):
  3. a ? a : b;
  4. a && b;
  5. // 大致相当于(roughly equivalent to):
  6. a ? b : a;

之所以说大致相当,是因为它们返回结果虽然相同但是却有一个细微的差别。在a ? a : b中,如果a是一个复杂一些的表达式(比如有副作用的函数调用等),它有可能被执行两次(如果第一次结果为真)。而在 a|| ba 只执行一次,其结果用于条件判断和返回结果(如果适用的话)。a && b a ? b : a 和也是如此。
下面是一个十分常见的 || 的用法,就是设置默认值:

  1. function foo(a,b) {
  2. a = a || "hello";
  3. b = b || "world";
  4. console.log( a + " " + b );
  5. }
  6. foo(); // "hello world"
  7. foo( "yeah", "yeah!" ); // "yeah yeah!"
  1. foo( "That’s it!", "" ); // "That’s it! world"

这种用法很常见,但是其中不能有假值,除非加上更明确的条件判断,或者转而使用? : 三元表达式。
如果第一个操作数为真值,则 && 运算符“选择”第二个操作数作为返回值,这也叫作“守护运算符” (guard operator),即前面的表达式为后面的表达式“把关”:

  1. function foo() {
  2. console.log( 43 );
  3. }
  4. var a = 42;
  5. a && foo(); // 43

foo()只有在条件判断a通过时才会被调用。如果条件判断未通过,a && foo()就会终止(也叫作“短路”,short circuiting),foo() 不会被调用。

符号的强制类型转换

ES6 允许 从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。

  1. var s1 = Symbol( "cool" );
  2. String( s1 ); // "Symbol(cool)"
  3. var s2 = Symbol( "not cool" );
  4. s2 + ""; // TypeError: Cannot convert a Symbol value to a string

符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true)。