沐光

记录在前端之路的点点滴滴

记 JS 的类型转换

前言

JS 内最为诟病的便是其隐式类型转换问题,对于初学者来说,其复杂的规则以及诡异的结果很容易一头雾水。这难道表明 JS 不是很好吗?其实不然,隐式的转换带给我们的是代码的简介度,在合适的时机使用,同样会带来优质的 coding 体验。本文就来梳理一下 JS 类型转化规则,方便大家理解。

本篇参考文章 《你所不知道的 JavaScript 中篇》 第四章

总体转换规则

文档内介绍的比较清除,此处为了更好的展示效果,仅引用一下截取自 Alex Dorey 的 GithubIO 的一张图:

JavaScript 中的相等比较

仔细理解一下,对于 JS 中很多相等比较的奇妙结果便都能解释了。比如:

1
"0" == false;  // true

当 string 与 boolean 类型比较时,会优先将 boolean 转化为 number,因此我们得到中间步骤为:

1
2
// Step 1
"0" == 0

然后 string 比较 number 时,此时会优先将 string 转化为 number,因此我们得到

1
2
// Step 2
0 == 0

最终的结果显而易见,那就是 true 了。

极端情况

当然我们不排除有些难以理解的极端情况,以下列举一个 Case:

1
[] == ![]  // true

分析一下,由于 ! 的存在,我们优先进行显示转换,于是有:

1
2
3
4
5
6
// Step 1
[] == !Boolean([])
// Step 2
[] == !true
// Step 3
[] == false

然后对象与 boolean 类型比较时,我们需要调用对象的 [[DefaultValue]] 方法(toString/valueOf),因此我们得到

1
2
3
4
// Step 4
[].toString() == false
// Step 5
'' == false

这一步又回到了 string 与 boolean 的比较了,很明显最终的结果为 true。

此外,还有部分比较难以理解的,这里就稍微列举一下:

1
2
3
4
// 数组单个值的解析
'' == [null]
'' == [null].toString()
'' == '' // true
1
2
3
4
// 对象的解析
0 == {}
0 == ToString({}) // 调用自身 __proto__ 上的 toString 方法
0 == '[Object object]' // false

逻辑运算符

隐式类型转换相关的还有逻辑运算符,但是 JS 中的逻辑运算符又和其它强类型语言(如 C)的逻辑运算符不同,其返回值并非 boolean 类型。我们看个例子:

1
2
3
4
5
6
7
8
9
const a = 4;
const b = "a";
const c = null;

a || b // 4
a && b // "a"

c || b // "a"
c && b // null

可见,|| 和 && 首先会对第一个操作数(a 和 c)执行条件判断,如果其不是布尔值就进行 toBoolean 强制类型转换,然后再执行条件判断。

其规律为:

对于 || 来说,如果条件判断的结果为 true 就返回第一个操作数(a 和 c)的值,否则返回第二个操作数(b)的值。

对于 && 来说,如果条件判断的结果为 true 就返回第二个操作数(b)的值,否则返回第一个操作数(a 和 c)的值。

换个角度来看,它们很像三元运算符,但是它不会有副作用即:其不会执行两次。如 a ? a : c,如果 a 为比较复杂的运算,那么 a 会执行两次,但是 a || c,其 a 只会执行一次。

此“逻辑运算符”在 JavaScript 中可能更为贴切的叫法为“操作数选择器”吧,在大多数的代码压缩后的内容中我们常常能见到其影子,工作中不妨也可以一试 ☆*:.。. o(≧▽≦)o .。.:*☆

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

代码块

我们知道,在 JS 中 { // } 表明这是一个对象,但是我们常常忘了它还能表示为代码块,比如 while / if 后面的大括号。然而这和 JS 类型转换有什么关系呢,我们看看下例:

1
2
[] + {}  // [object Object]
{} + [] // 0

那么问题来了,仅仅调换了一个位置,为什么结果会相差这么多?

分析一下,在第一个例子中,{} 是在 + 运算符表达式中,词法分析将 {} 解析为第二个参数,因此经过隐式转换,{} 会被转换成 ‘[object Object]’,空数组就不用多解释了,所对应的结果也就如上了。

然而第二个却有些不同。我们知道 {} 作为空代码块是不执行任何操作的,且不用带 ; 结尾,所以在此处是不存在语法问题的。因此自左向右来看,词法分析将此分解为 {} 与 +[] 两部分,前者作为空代码块执行,无任何副作用,后者强制转换结果就是 0 了。

参考文章