30 March 2016
说说JS的隐式类型转换
我们都知道JS是一门弱类型语言,既然是弱类型,那么必然会有许许多多的隐式类型转换。经常在我们都没有意识到的时候,隐式类型转换已经发生了。如果我们不知道这其中的规则,那么踩坑只是迟早的事。
之前我研究过JS在判断true/false时的隐式类型转换的规则(《Truth, Equality and JavaScript》、译文),对一些场景下的隐式类型转换有了一定的了解。
但是在最近当我尝试判断 10 > undefined
时,错误的记成undefined会被转换为0,然后statement将会为true。然而这显然是错的,我才突然发现我对比较操作符的比较规则与其类型转换规则并不了解,翻阅了下ES5规范后,有了此文(有兴趣的同学可以直接去查看规范《ECMA-262 5.1 Edition - 11.8 Relational Operators》)。
首先大概说下JS判断true、false时的规则,主要有三种会判断true、false的场景:
-
条件语句与操作符, 包括 if , ? : , && , 等; - ==, 比较操作符;
- ===,严格比较操作符。
1. 条件语句与操作符(Conditionals)
这个比较简单,以If(Expression)
为例,JS将对Expression调用一个抽象方法ToBoolean,将Expression转换为一个boolean类型。
ToBoolean的算法规则:
参数类型 | 结果 |
---|---|
Undefined | false |
Null | false |
Boolean | 返回自身 |
Number | 如果参数为 +0, -0, NaN,返回false, 否则返回true |
String | 如果参数为空字符串(string.length == 0),返回false,否则返回true |
Object | true |
所以,为true的有这些: true,"potato",36,[1,2,4] and {a:16}
,
而为false的有: false,0,"",null and undefined
。
2. 判等操作符(==,The Equals Operator)
比较操作符比较宽容,以lRef == rRef
为例,如果lRef与rRef为不同类型,那么JS将首先按照一定规则将他们转换为同类型。规则如下:
Type(x) | Type(y) | 结果 |
---|---|---|
x与y同类型 | - | 结果参见严格判断操作符(===) |
null | Undefined | true |
Undefined | null | true |
Number | String | x == toNumber(y) |
String | Number | toNumber(x) == y |
Boolean | (any) | toNumber(x) == y |
(any) | Boolean | x == toNumber(y) |
String or Number | Object | x == toPrimitive(y) |
Object | String or Number | toPrimitive(x) == y |
otherwise… | - | false |
如果算法的结果是一个表达式,那么它将被重新计算,直到最后得到一个Primitive的Boolean值。
其中ToNumber与ToPrimitive跟上面的ToBoolean一样,为抽象方法,规则如下:
ToNumber
| 参数类型 | 结果 | | ——– | ———— | | Undefined| NaN | | Null | +0 | | Boolean | 如果为true,返回1,否则返回0 | | Number | 返回自身 | | String | 与调用Number(string)结果一致: “abc” -> NaN, “123” -> 123 | | Object | 会执行以下步骤:1. 让primValue = ToPrimitive(input argument, hint Number); 2. 返回ToNumber(primValue) |
ToPrimitive
| 参数类型 | 结果 | | ————- | ———— | | Object |(在判等操作符的场景下)如果input.valueOf()返回一个原始类型(primitive),直接返回input.valueOf();否则,如果input.toString()返回一个原始类型,那么返回input.toString(); 否则报错。 | | otherwise… | 返回自身 |
3.严格判等操作符(===,The Strict Equals Operator)
这个也比较简单,还是以lRef == rRef
为例,只要Type(lRef)与Type(rRef)不同,那么总是会返回false。否则,对象必须指向相同的对象引用,字符串必须包含相同的字符,其他原始类型必须拥有相同的值。
另外,null和undefined永远不会===除自己以外其他类型,而NaN不会与任何类型===,甚至包括自己。
具体规则如下:
Type(x) | 值 | 结果 |
---|---|---|
Type(x)与Type(不同) | - | false |
Undefined/Null | - | true |
Number | x与y的值相等(但不能为NaN) | true |
String | x与y的字符串一样 | true |
Boolean | x与y同时为ture或false | true |
Object | x与y指向同一个引用 | true |
otherwise… | - | true |
比较操作符(<,<=,>,>=,Relational Operators)
好了终于到了今天的主角,我们以lRef < rRef
为例。 为了简单起见,只讨论lRef与rRef为非引用类型且不为String的场景(因为比较引用类型与比较字符串规则较为复杂,且使用场景较少,有兴趣的同学可以自行研究《ECMA-262 5.1 Edition - 11.8 Relational Operators》)
先看规则:
- 因为讨论左右表达式都为非引用类型,所以直接使用判断规则(引用类型的还需要多做几部操作)。
- 对lRef调用ToPrimitive方法, lRef = ToPrimitive(lRef);
- 对rRef调用ToPrimitive方法, rRef = ToPrimitive(rRef);
- 先后对lRef于rRef调用ToNumber方法, lVal = ToNumber(lRef), rVal = ToNumber(rRef);
- 只要lVal或rVal其中之一为NaN,返回false;
- 后面的规则就没什么特殊的了,因为已经转为了数字,比较数字大小,返回结果。
可以看到,限于我们讨论的场景,影响结果最关键的地方就在ToNumber方法,回到我最开始遇到的问题:
10 > undefined
,查看上面ToNumber方法的规则,可以看到ToNumber(undefined)返回NaN, 所以此时比较操作符永远返回false。
总结
隐式类型转换是JS的一把双刃剑,对此业内也是褒贬不一。我觉得嘛,只要我们把底层的转换规则了然于胸,那么我们可以充分的利用JS弱类型的特性把JS写的很风骚很风骚,如果只凭感觉或者根据经验记结论,终究还是找不到那种游刃有余、胸有成竹的感觉。
Posted in 2016-03-30 16:51