吐槽
一门语言总会有一部分存在争议,JS 中的类型转换就是如此,于此广大程序员也是褒贬不一,至于我的态度.......Oh,Shit!
前言
本文根据ECMAScript 2021讨论类型转换,因为类型转换的情况着实复杂,所以本文只例举几个常见的情况。
大部分内容还是讨论规范下的类型转换是怎样实现的。
语言类型
ECMAScript 2021 规定了这几种语言类型:Undefined、Null、Boolean、String、Symbol、Numeric、Object。
Numeric 可细分为 Number 和 BigInt。
类型转换只能作用于上面提及的语言类型。
类型转换
类型转换分为隐式和显式,本文主要讨论隐式类型转换,显式类型转换这里简单说一下:如函数 String()、Number()、BigInt()...就是显式转换。
注意:
-
下面介绍的方法均为规范中类型转换的实现,JS 引擎没有暴露相关函数。
-
BigInt 类型不存在隐式类型转换,所以
'2'*1n会报错,必须显式调用 BigInt 进行类型转换BigInt('2')*1n。
ToPrimitive(input[, PreferredType])
如果 input 的类型是 Object,那么逻辑如下:
if PreferredType 不存在,设置 hint 为 default。
else if PreferredType 为 hint String, 设置 hint 为 string。
else 设置 hint 为 number。
if input 所属类型重新定义了 ToPrimitive 方法, 那么执行重定义的方法并返回得到的值。
if hint 为 default,设置 hint 为 number。
返回 OrdinaryToPrimitive(input, hint);
否则直接返回 input。
注释
- OrdinaryToPrimitive(O, hint) 方法如下:
if hint 为 string,设置 methodNames 为 ["toString", "valueOf"]。
else 设置 methodNames 为 ["valueOf", "toString"]。
For each name in methodNames
if O 存在 name 方法,那么执行 name 方法,如果执行结果不是对象,那么返回该结果。
- 只有 Date 对象和 Symbol 对象重新定义了 ToPrimitive 方法,Date 对象 hint 默认为string(其它对象为number),Symbol 对象直接返回 input。
ToBoolean(argument)
ToNumeric(value)
设置 primValue 为 ToPrimitive(value, hint Number)。
if primValue 类型为 BigInt,返回 primValue。
返回 ToNumber(primValue)。
注释
对于 String 类型来说,空格字符,换行符在文本前后是允许的,如果文本为空(例如""、" ")则结果为 0,二进制 0b、八进制 0o、十六进制 0x、以及科学计数法(例如 1.2E3)也是符合语法规定的,详细请参考规范相关内容。
ToNumber(argument)
ToString(argument)
注释
-
当 x 为+0 或 -0, Number::toString(x)结果为"0"。
-
BigInt::toString (x)方法如下:
if x 小于 0,返回 "-" 加上 BigInt::toString (-x)。
返回 x 对应的十进制数值的字符串形式。
例子
var a = {};
var b = { key: "b" };
var c = { key: "c" };
a[b] = "b";
a[c] = "c";
console.log(a[b]);
ToPropertyKey(argument) 将 argument 转换为对象的属性名。
- 设置 key 为 ToPrimitive(argument, hint String),得到结果"[object Object]"。
- if key 的类型为 Symbol,返回 key。
- 返回 ToString(key),即"[object Object]"。
所以a[b] = "b"即a["[object Object]"] = "b",同理a[c],所以最终打印"c"。
可以加上一句a["[object Object]"] = "object"验证。
console.log(+[]);
console.log(+["1"]);
console.log(+["1", "2"]);
对于一元操作符 + 会调用 ToNumber 进行类型转换。
参照 ToNumber(argument),argument 为 Object。
- 设置 primValue 为 ToPrimitive(argument, hint Number),得到结果
[]对应"",["1"]对应"1",["1", "2"]对应"1,2"。 - 返回 ToNumber(primValue),即 ToNumber("")、ToNumber("1")、ToNumber("1,2"),得到结果为 0、1、NaN。
补充:Array.prototype.toString()调用的是Array.prototype.join()构成字符串,除了 undefined 和 null 为"",其它元素均通过 ToString 进行类型转换
console.log([] + []);
console.log({} + []);
console.log(null + 1);
console.log([null] + "1");
console.log(1n + 1);
对于二元操作符 lval + rval 会进行以下判断:
设置 lprim 为 ToPrimitive(lval)。
设置 rprim 为 ToPrimitive(rval)。
如果 lprim 或者 rprim 有一个为 String 类型,则调用 ToString 进行类型转换,返回拼接后的字符串。
调用 ToNumeric 进行类型转换,如果返回值的类型不同,抛错,否则返回操作结果。
console.log([] + []),lprim 为"",rprim 为"",满足 lprim 或者 rprim 有一个为 String 类型,调用 ToString(lprim)得到"",ToString(rprim)得到"",""+""得到""。console.log({} + []),lprim 为"[object Object]",rprim 为"",满足 lprim 或者 rprim 有一个为 String 类型,调用 ToString(lprim)得到"[object Object]",ToString(rprim)得到"","[object Object]"+""得到"[object Object]"。console.log(null + 1),lprim 为 null,rprim 为 1,调用 ToNumeric(lprim)得到 +0,ToNumeric(rprim)得到 1,类型均为 Number,所以 +0+1 得到 1。console.log([null] + "1"),lprim 为"",rprim 为"1",满足 lprim 或者 rprim 有一个为 String 类型,调用 ToString(lprim)得到"",ToString(rprim)得到"1",""+"1"得到"1"。console.log(1n + 1),lprim 为 1n,rprim 为 1,调用 ToNumeric(lprim)得到 1n,ToNumeric(rprim)得到 1,类型分别为 Number、BigInt,所以报错。
最后提一下特殊情况:直接在 Chrome 或者 Firebug 开发工具中的命令行输入{} + [],会得到不一样的结果。
> {} + {}
// 火狐: NaN
// 谷歌: "[object Object][object Object]"
上面差异的原因主要是火狐与谷歌 JS 引擎对代码块的判断不同。
首先,对于一个表达式来说,如果开头是{的形式,那么相对应开头的}构成的一段代码(假设叫头代码)就有两种可能:代码块和对象。(一个表达式后面出现的{内容}一定是对象)。
JaegerMonkey 引擎的处理
直接把头代码当成代码块。
下面代码会报错,因为这是代码块。
注意:如果只有a: 1是不会报错的,这种语句无意义。
> { a: 1, b: 2 }+[]
// error
> { a: 1, b: 2 }+{}
// error
写成代码块形式:
> { console.log(); }+[]
// 0
> { console.log(); }+{}
// NaN
V8 引擎的处理
其实就多了一个判断:头代码紧跟着的是对象 ? 根据头代码的内容(无内容时,即{},作为对象处理)确定是代码块还是对象 : 当做代码块处理。
分析下面几种情况:
> { a: 1, b: 2 }+[]
// error
> { console.log(); }+[]
// 0
> { a: 1, b: 2 }+{}
// "[object Object][object Object]"
> { console.log(); }+{}
// NaN
关于 "=="
"=="这个判断我个人是不会用的,至少我觉得这是 JS 中的糟粕。
如果想了解"=="中的隐式转换,可以参考规范的相关内容。