前言
类型转换在各个语言中都存在,而在 JavaScript 中由于缺乏对其的了解而不慎在使用中经常造成bug被人诟病。为了避免某些场景下的意外,甚至推崇直接使用 Strict Equality( === )来代替 ==(最新的eslint规则默认就是使用===)。这确实能避免很多bug,但更是一种对语言不理解的逃避(个人观点)。
来看几道经典的题目:
[] == [] // false
[] == ![] // true
{} == !{} // false
{} == {} // false
是不是很奇怪?本文将从书中看到的知识与规范相结合,来详细说明一下JavaScript在类型转换时候发生的故事。
JS的数据类型
首先,回想一下JS的类型都有什么。
原始值(primitives): undefined, null, booleans, numbers,strings, symbol(es6)
对象值(objects): Object
ToPrimitive
在发生转换的时候,js其实都是会将操作对象转化为原始的对象,这也是最为诟病的地方,因为js很难直接抛出错误,她会用一套自己的方法去理解我们的错误,并做相应的调整,哪怕这些错误我们是无意识的。所以我们要知道她的转换方式,才能做到知己知彼,对代码的控制更为精准。
签名:ToPrimitive(input, PreferredType?) //PreferredType: Number 或者 String
流程如下:
input为原始值,直接返回;
不是原始值,调用该对象的valueOf()方法,如果结果是原始值,返回原始值;
调用valueOf()不是原始值,调用此对象的toString()方法,如果结果为原始值,返回原始值;
如果返回的不是原始值,抛出异常TypeError。
其中PreferredType控制线调取valueOf()还是toString()。
ps: Date类型按照String去调用。
总结:==操作符
比较运算 x==y,其中x和y是值,产生true或者false。这样的比较如下方式进行:
一、若Type(x)与Type(y)相同,则:
a.若Type(x)为Undefined,返回true。
b.若Type(x)为Null,返回true。
c.若Type(x)为Number,则:
1.若x为NaN,返回false。
2.若y为NaN,返回false。
3.若x与y为相等数值,返回true。
4.若x为+0且y为-0,返回true。
5.若x为-0且y为+0,返回true。
6.返回false。
d.若Type(x)为String,则当x和y完全相等的字符序列(长度相等且相同字符在相同位置)时返回true。否则,返回false。
e.当Type(x)为Boolean,当x和y同为true或者同为false时返回true。否则,返回false。
f.当x和y为引用同一对象时返回true。否则,返回false。
二、若x为null且y为undefined,返回true。
三、若x为undefined且y为null,返回true。
四、若Type(x)为number且Type(y)为String,返回comparison x==ToNumber(y)的结果。
五、若Type(x)为String且Type(y)为Number,返回comparison ToNumber(x) == y的结果。
六、若Type(x)为Boolean,返回比较ToNumber(x) == y的结果。
七、若Type(x)为Boolean,返回比较x == ToNumber(y)的结果。
八、若Type(x)为String或Number,且Type(y)为Object,返回比较x == ToPrimitive(y)的结果。
九、若Type(x)为Object且Type(y)为String或Number,返回比较ToPrimitive(x) == y的结果。
十、返回false。
上图中的 toPrimitive 就是对象转基本类型。
这里来解析一道题目 [] == ![] // -> true ,下面是这个表达式为何为 true 的步骤
// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
备注
ToString
按照以下规则转化被传递的参数
ArgumentType | Result |
---|---|
Undefined | “undefined” |
Null | “null” |
Boolean | true -> “true”false – > “false” |
Number | 1:’NaN -> “NaN”。 2:+0 -0 -> “0”。 3:-1 -> “-1。 4:”infinity -> “Infinity” |
String | 不转换 直接返回 |
Object | 1:调用ToPrimitive抽象操作, hint 为 String 将返回值作为 value。2:返回ToString(value)。 |
String(undefined) // "undefined"
String(null) // "null"
String(true) // "true"
ToNumber
按照以下规则转换被传递参数
Argument Type | Result |
---|---|
Undefined | NaN |
Null | +0 |
Boolean | 1:true -> 1。 2:false -> +0。 |
Number | 直接返回 |
String | 如果不是一个字符串型数字,则返回NaN(具体规则见规范9.3.1) |
Object | 1:调用ToPrimitive抽象操作, hint 为 Number 将返回值作为 value。 2:返回ToNumber(value)。 |
ToBoolean
按照以下规则转换被传递参数
Argument Type | Result |
---|---|
Undefined | false |
Null | false |
Boolean | 直接返回 |
Number | 1:+0 -0 NaN -> false。 2:其他为true。 |
String | 1:空字符串(length为0) -> false。 2:其他为true。 |
Object | true |
ToPrimitive
顾名思义,该抽象操作定义了该如何将值转为基础类型(非对象),接受2个参数,第一个必填的要转换的值,第二个为可选的hint,暗示被转换的类型。
按照以下规则转换被传递参数
Argument Type | Result |
---|---|
Undefined | 直接返回 |
Null | 直接返回 |
Boolean | 直接返回 |
Number | 直接返回 |
String | 直接返回 |
Object | 返回一个对象的默认值。一个对象的默认值是通过调用该对象的内部方法[[DefaultValue]]来获取的,同时传递可选参数hint。 |
[[DefaultValue]] (hint)
当传递的hint为 String 时候,
- 如果该对象的toString方法可用则调用toString
如果toString返回了一个原始值(除了object的基础类型)val,则返回val
如果该对象的valueOf方法可用则调用valueOf方法
如果valueOf返回了一个原始值(除了object的基础类型)val,则返回val
抛出TypeError的异常
当传递的hint为 Number 时候, - 如果该对象的valueOf方法可用则调用valueOf方法
如果valueOf返回了一个原始值(除了object的基础类型)val,则返回val
如果该对象的toString方法可用则调用toString
如果toString返回了一个原始值(除了object的基础类型)val,则返回val
抛出TypeError的异常 - hint的默认值为Number,除了Date object
举个栗子
var a = {}
a.toString = function () {return 1}
a.valueOf = function () {return 2}
String(a) // "1"
Number(a) // 2
a + '' // "2" ???????
+a // 2
a.toString = null
String(a) // "2"
a.valueOf = null
String(a) // Uncaught TypeError: balabala
似乎我们发现了一个很不合规范的返回值,为什么 a + ''不应该返回”1″吗
问题的答案其实很简单 + 操作符会对两遍的值进行 toPrimitive 操作。由于没有传递 hint 参数,那么就会先调用a.valueOf 得到2后因为+右边是字符串,所以再对2进行ToString抽象操作后与””的字符串拼接。