现在大家对 类型 和 值 比较了解之后,就需要进入最具争议的话题 “强制转型”(Coercion)了。

关于强制转型的争议由来已久,但我们的目标是——没有知不道的JS,所以这一章我们要深入了解强制转型的机制。

值的转换(converting values)

一般把一种类型的值转换成另一种类型的值的过程叫做“类型映射”(type casting)。

这种转换可以是显式的,比如

Number('42'); // 42

也可以是隐式的,比如

"" + 42 // "42"

这两种转换都叫“强制转型”,注意 JS 中强制转型只会转成内置类型中的 “简单标量”(scalar primitive),也就是 string / number / boolean 等,而不会转成 object 或 function ,当然可以通过第三章讲的“装箱”(boxing),将 简单标量转型成对应的对象形式,但这不是本章的重点。

toPrimative

toPrimative 的意思就是把复杂类型(对象、函数、数组等)映射成简单类型值的过程,它不是一种单一的方法,因为复杂类型不只一个属性,那么这种映射如何执行呢?通常会用到以下三类方法:

  1. toString
  2. toNumber
  3. toBoolean

toString 的方法

  1. x.toString()
  2. JSON.stringify()

toNumber 的方法

  1. Number()

toBoolean 的方法

在简单的内置类型中,这些值 undefined 、 null 、+0、 -0 、NaN、 "" 转换成布尔值的结果都是 false,其他的则都是 true ,只有一个例外就是 document.all 这个对象也是 false。

  1. falsy value // undefined 、 null 、+0、 -0 、NaN、 ""
  2. falsy object // 非标准的 document.all ,其他的object都是true

显式转型

String <--> Number

  1. String(x)
  2. Number(x) / parseInt(x) / parseFloat(x)
  3. x.toString()
  4. +x / -x
  5. x | 0 / ~x / ~~x

第4个很容易让人误解 var a = '2' b=1+ +a; b=+a; 这两个看起来很容易错,因此最好不要在操作符后面接 +a

* --> Boolean

  1. Boolean(x)
  2. !!x

隐式转型

与显式转型相对的就是隐式转型,显式转型让人更清晰的了解代码,而隐式正相反,这也是为什么JS的隐式转型被许多人吐嘈。

String <--> Number

首先就是JS里最为奇葩的“+”操作符,既可以用来进行加法运算,又可以拼接字符串,并且前面还说了它可以进行显式把字符串转型成数字,那么问题就是这个“+”什么时候是数学意义,什么时候是拼接呢?

在 ES5 的官方文档中是这样定义的:

  1. “+”操作符两侧都有值,只要一侧是字符串,那就进行拼接操作
  2. 如果“+”两侧有 Object 类型,则先将 Object 用 ToPrimitive 的各种方法(valueOf() / toString()等)转为简单标量,然后再判断是运算还是拼接。

  3. 如果“+”只有后面有非number的值,就执行显式转型。

.

typeof (new Number('1') + new Number('2')) //=> "number"

下面这个例子我也是理解了好几个月,才有点懂,需要先理解第五章的 '{}' 上下文,试解释一下

{} + [] ; //=> 0
[] + {} ; //=> "[object Object]"

上面这个因为{}被JS认为是一个没有意义的代码块,所以被跳过了,而剩下的就是 +[] 这会进行一个显式转型,就是把 [] 转成字符串后又转成了数字,最后就结果变成 0 了;

而第二行中[] 被转换成了 空字符串 "",{}被JS认为是一个空对象,因此对象toString就变成 "[object Object]",和空字符串一拼接就是第二个结果了。

那么你可以猜猜 +[] + {} 的返回值是什么吗?

如果你觉得上面的比较简单,下面的你就可以看懂了

for(var _i=+'';_i<+(+!''+''+(+''));_i++) (i=>setTimeout(()=>console.log(i),i*[+!![]][+[]]+[]+(+[])+(+'')+(+[])))(_i);

我们知道了“+”如何处理字符串,那 “-”“*”“/”呢? 它们都会将 两边的值(字符串或对象)强制转型成 数字(number 或 NaN)

'1e309' - 1; // infinity
'' * 1; // 0 
[1]-[-2] // 3
{} - [2] // -2
'n' - 1; // NaN

({valueOf : () => 2}) - 3 // 感受一下 
3 - {valueOf : () => 2} // 再感受一下上下文

这两天用 => 偷懒让我理解了 => 函数果然是绑定到当前作用域

 o = {
  a:1,
  b:2,
  valueOf : () => this.o.a + this.o.b
}

'' + o; // 3

Boolean --> Number

"+"号的行为实在是....

false + true + true + undefined // NaN
false + true + true + null // 2

* --> Boolean

这个应该是大家最常见也最常用的了

  1. if () ..
  2. for () ..
  3. while () / do .. while()
  4. .. ? .. : ..
  5. && 或 || (这两个操作符虽然判断逻辑,但返回原值)

这里说下第5个,比较特殊,可以这样理解

a && b 和 a ? b : a; 是等价的
a || b 和 a ? a : b; 是等价的

a || b 这种形式你肯定很熟悉,只不过现在你的理解更深入了

function foo(a) {
    a = a || 'no argument';
}

相等 ( == ) 与 全等(===)

有人可能关心性能,是有点区别,相等在两侧类型不同时会先强制转型,然后再比较,而全等则不会。不过这不是最要紧的,你要取决于自己的需求。

相等的强制转型有以下几种情况:

  1. string == number // 会把string 强制转换成number
  2. * == boolean // 会把boolean转成数字,而如果变成了数字和字符串,就回到第一步的比较
  3. nulls == undefinded // 认为是一样的
  4. object == nonObject // Object 会被 toPrimitive

比较表,如图:

enter image description here 来源:JavaScript-Equality-Table

比较(< > <= >=)

比较也会导致隐式转型,ES5规定的算法是这样的,先通过toPrimitive转型,如果返回的是字符串,再用toNumber操作进行比较。 <= 这里翻译的也有问题,待续

比如

'一' < '二' // true 
'三' < '二' // true
'三' < '一' // false

字符串到底转成了什么?

我猜想 charCodeAt 在这里被当作 toNumber 方法对 字符串 进行了转型,应该返回了 Unicode 码,然后再进行比较,所以我觉得这些问题可能有望成为前端面试题 :)