JS中的类型转换

200 阅读5分钟

吐槽

一门语言总会有一部分存在争议,JS 中的类型转换就是如此,于此广大程序员也是褒贬不一,至于我的态度.......Oh,Shit!

前言

本文根据ECMAScript 2021讨论类型转换,因为类型转换的情况着实复杂,所以本文只例举几个常见的情况。

大部分内容还是讨论规范下的类型转换是怎样实现的。

语言类型

ECMAScript 2021 规定了这几种语言类型:Undefined、Null、Boolean、String、Symbol、Numeric、Object。

Numeric 可细分为 Number 和 BigInt。

类型转换只能作用于上面提及的语言类型。

类型转换

类型转换分为隐式和显式,本文主要讨论隐式类型转换,显式类型转换这里简单说一下:如函数 String()、Number()、BigInt()...就是显式转换。

注意:

  1. 下面介绍的方法均为规范中类型转换的实现,JS 引擎没有暴露相关函数。

  2. 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。

注释

  1. OrdinaryToPrimitive(O, hint) 方法如下:

if hint 为 string,设置 methodNames 为 ["toString", "valueOf"]。

else 设置 methodNames 为 ["valueOf", "toString"]。

For each name in methodNames

if O 存在 name 方法,那么执行 name 方法,如果执行结果不是对象,那么返回该结果。

  1. 只有 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)

注释

  1. 当 x 为+0 或 -0, Number::toString(x)结果为"0"。

  2. 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 转换为对象的属性名。

  1. 设置 key 为 ToPrimitive(argument, hint String),得到结果"[object Object]"。
  2. if key 的类型为 Symbol,返回 key。
  3. 返回 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。

  1. 设置 primValue 为 ToPrimitive(argument, hint Number),得到结果[]对应"",["1"]对应"1",["1", "2"]对应"1,2"。
  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 进行类型转换,如果返回值的类型不同,抛错,否则返回操作结果。

  1. console.log([] + []),lprim 为"",rprim 为"",满足 lprim 或者 rprim 有一个为 String 类型,调用 ToString(lprim)得到"",ToString(rprim)得到"",""+""得到""。
  2. console.log({} + []),lprim 为"[object Object]",rprim 为"",满足 lprim 或者 rprim 有一个为 String 类型,调用 ToString(lprim)得到"[object Object]",ToString(rprim)得到"","[object Object]"+""得到"[object Object]"。
  3. console.log(null + 1),lprim 为 null,rprim 为 1,调用 ToNumeric(lprim)得到 +0,ToNumeric(rprim)得到 1,类型均为 Number,所以 +0+1 得到 1。
  4. console.log([null] + "1"),lprim 为"",rprim 为"1",满足 lprim 或者 rprim 有一个为 String 类型,调用 ToString(lprim)得到"",ToString(rprim)得到"1",""+"1"得到"1"。
  5. 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 中的糟粕。

如果想了解"=="中的隐式转换,可以参考规范的相关内容