阅读这章之前,先思考下面代码:
+true // 1
+false // 0
+null // 0
+undefined // NaN
+'-0' // -0
+'Infinity' // Infinity
+'-Infinity' // -Infinity
''+(-0) // "0"
''+(-Infinity) // "-Infinity"
Boolean(NaN) // false
Boolean(Infinity) // true
Boolean(-Infinity) // true
Boolean(Symbol()) // true
+Symbol() // 报错
''+Symbol() // 报错
Symbol().toString() // "Symbol()"
// 对象转换
+{} // NaN
+[] // 0
+[1] // 1
+[Infinity] // Infinity
+[-0] // 0
+ [1, 2] // NaN
+[{}] // NaN
''+[] // ""
''+[1] // "1"
''+[1,2,3] // "1,2,3"
''+[Infinity, Infinity] // "Infinity,Infinity"
''+{} // "[object Object]"
''+function test (){} // "function test (){}"
[1, 2] + [2, 1] // '1,22,1'
老实说,我不能完全正确说出结果,也不能保证说出 为什么?
正因为我的js基础如此之垃圾,所以阅读规范中关于类型转换一章并翻译,同时配合MDN进行学习,希望有底气的回答上面代码的表现行为及原因。
正文
抽象运算不是ECMAScript语言的一部分;规范中定义它们仅是为了帮助指定ECMAScript语言的语义。
当需要时,ECMAScript会自动隐式执行类型转换。类型转换抽象运算只能接收 ECMAScript语言类型,不能接收 规范类型。
BigInt
类型没有隐式类型转换;程序必须显式的调用BigInt
进行类型转换。
将输入的input
转换为非Object
类型值。如果对象能够转换为多个原始类型,则可以使用可选提示PreferredType
来确定需要转换成哪个类型。
- 如果
input
是Object
类型
- 如果
PreferredType
不存在,定义过程变量 hint
赋值为 "default"
- 如果
PreferredType
是 String
,定义过程变量 hint
赋值为 "string"
- 否则,
- 断定
PreferredType
是 Number
- 定义过程变量
hint
赋值为 "number"
- 定义过程变量
exoticToPrim
,执行GetMethod(input, @@toPrimitive)并赋值给exoticToPrim
- 如果
exoticToPrim
不是undefined
,
- 定义过程变量
result
,调用 Call(exoticToPrim, input, « hint »)
并赋值给 result
,
- 如果
result
不是 Object
类型则返回 result
- 否则报类型错误
- 如果
hint
是 "default"
,将 hint
赋值为 "number"
- 返回 调用
OrdinaryToPrimitive(input, hint)
的返回值
- 如果
input
一开始就不是Object
类型,直接原路返回
注意:如果没传 PreferredType
,那么默认就是 number
。 但是可以通过定义一个 @@toPrimitive
来覆盖默认行为。Date
和 Symbol
对象会覆盖默认的ToPrimitive
行为。当没有PreferredType
时,Date
默认转为string
。
主要是为了将 Object
转为 原始数据类型。
O
必须是对象
hint
必须是 String
类型且值为 "string"
或 "number"
- 如果
hint
是 "string"
- 定义过程变量
methodNames
,值为List « "toString", "valueOf" »
- 如果
hint
是 "number"
- 定义过程变量
methodNames
,值为List « "valueOf", "toString" »
- 顺序遍历
methodNames
这个List,定义过程变量 name
来缓存List中的每个值
- 定义过程变量
method
,执行 Get(O, name)
赋值给 method
- 执行 IsCallable(method),如果为
true
- 定义过程变量
result
,执行 Call(method, O)赋值给 result
(这里本质上就是在执行 valueOf() 或 toString(),先后顺序看methodNames
这个List)
- 如果
result
不是 Object
,则返回 result
- 如果以上都不满足,报类型错误
直接看表就好:
参数类型 |
结果 |
Undefined |
false |
Null |
false |
Boolean |
argument |
Number |
+0, -0, NaN 都返回 false ,其余都是 true |
String |
空字符串即长度为0的返回false ,否则都是true |
Symbol |
true |
BigInt |
0n 返回 false ,其余都是true |
Object |
true |
顾名思义,转为数值的,只不过有Number
和BigInt
两种可能。
这个是转换为Number
类型的了。见下表:
参数类型 |
结果 |
Undefined |
NaN |
Null |
0 |
Boolean |
true => 1, false => +0 |
Number |
argument |
String |
string转number |
Symbol |
类型报错 |
BigInt |
规范目前写的有问题,见tc39/ecma262#1766 |
Object |
先转为原始值;然后将原始值转为Number |
注意:
2019.11.14号,ECMAScript262规范目前规定传入BigInt参数会报错,从测试来看,使用 + 操作应该默认的是ToNumber,符合要求;但是使用Number()却能正常转换,而该算法内部依然使用ToNumber,却不报错?
+0n // Uncaught TypeError: Cannot convert a BigInt value to a number
Number(+0n) // Uncaught TypeError: Cannot convert a BigInt value to a number
Number(0n) // 0
我去知乎提过这个问题,有大佬指出这是规范编写时的bug,还没有修复,见知乎。
根据链接我去查看了issue里面的其它链接,ToNumber ( argument )这里依然是抛类型错误,但是下面的 注意 告诉我:
该规范的主要设计决策是禁止隐式转换,并迫使程序员自己使用显式转换。
此外,最新的BigInt 文档补丁里的Number(value)方法内部把ToNumber(value) 改成 ToNumeric(value)了
针对String转Number,如果语法无法将String解释为StringNumericLiteral的扩展,则ToNumber的结果为 NaN。
该语法的结尾符号全部由Unicode基本多语言平面(BMP)中的字符组成。因此,如果字符串包含任何成对或不成对的前导代理或尾随代理代码单元,则ToNumber的结果将为NaN。
注意:
StringNumericLiteral
可以包括 前导 和 尾随空白 以及 行终止符
- 十进制的
StringNumericLiteral
可以具有任意数量的前导 0 数字
- 十进制的
StringNumericLiteral
可以包含 + 或 - 表示其符号
- 空 或 仅包含空格 的
StringNumericLiteral
会转换为 +0。
Infinity
和 -Infinity
被 StringNumericLiteral
认可,但它们不是 NumericLiteral
StringNumericLiteral
不能包含 BigIntLiteralSuffix
String
类型的数字转换为Number
类型的数字见:https://tc39.es/ecma262/#sec-runtime-semantics-mv-s
经过测试,小数点如果超过15位,那么会看第16位的值,然后不断进行四舍五入。本质还是和Number类型一样的。
测试:
+'9.999999999999999999999' // 10
+'9.1234567891234567891234' // 9.123456789123457
+'9.123456789999999' // 9.123456789999999
+'9.1234567899999991234567' // 9.123456789999999
+'9.1234567899999999234567' // 9.12345679
+'9.123456789123499' // 9.123456789123498
+'99999999999999999999' // 100000000000000000000
- 先执行
ToNumber(argument)
并将值赋给过程变量number
- 如果
number
是NaN
,则返回 +0
- 如果
number
是+0, -0, +Infinity 或 -Infinity
,则直接返回number
(这里用Infinity
代替规范中的∞
符号)
- 以上都不符合,则返回与
number
相同符号的 Number类型值,其大小是 floor(abs(number))
。先取正,再四舍五入。
将 argument
转成32位(4字节)有符号的整数,取值范围在 Math.pow(2, -31) ~ Math.pow(2, 31) - 1 之间。
- 先执行
ToNumber(argument)
并将值赋给过程变量 number
- 如果
number
是 NaN
, +0
, -0
, +Infinity
或 -Infinity
,则返回 +0
- 定义过程变量
int
,其值是 与 number
相同符号的 Number类型值且其大小是 floor(abs(number))
- 定义过程变量
int32bit
,其值为对 int
执行 2的32次幂 按模运算
- 如果
int32bit
>= 2的31次幂,返回 int32bit - 2的32次幂
;否则返回 int32bit
将 argument
转成32位无符号的整数,取值范围在 0 ~ Math.pow(2, 32) - 1 之间。
- 前 4 步与
ToInt32
一致
- 最后直接返回
int32bit
转为16位(2字节)有符号整数,取值范围在 -32768 ~ 32767
- 前三步与
ToInt32
一致
- 定义过程变量
int16bit
,其值为对 int
执行 2的16次幂 按模运算
- 如果
int32bit
>= 2的15次幂,返回 int16bit - 2的16次幂
;否则返回 int16bit
转为16位(2字节)无符号整数,取值范围在 0 ~ Math.pow(2, 16) - 1
- 前四步与
ToInt16
一致
- 最后直接返回
int16bit
转为 8 位有符号整数,取值范围在 -128 ~ 127,即 Math.pow(2, -7) ~ Math.pow(2, 7) - 1
- 前三步与
ToInt16
一致
- 定义过程变量
int8bit
,其值为对 int
执行 2的8次幂 按模运算
- 如果
int8bit
>= 2的7次幂,返回 int8bit - 2的8次幂
;否则返回 int8bit
转为 8 位无符号整数,取值范围在 0 ~ 255,即 0 ~ Math.pow(2, 8) - 1
- 前四步与
ToInt8
一致
- 最后直接返回
int8bit
目的和 ToUint8
一样,但是过程不一样
- 定义过程变量
number
,将 ToNumber(argument)
结果赋值给 number
- 如果
number
是 NaN
,返回 +0
- 如果
number
<= 0, 返回 +0
- 如果
number
>= 255, 返回 255
- 如果不符合以上条件,定义过程变量
f
,对 number
四舍五入并赋值给 f
- 如果
f
+ 0.5 < number
,返回 f
+ 1
- 如果
f
+ 0.5 > number
, 返回 f
- 如果
f
是奇书,返回 f
+ 1
- 都不满足,返回
f
ToUint8Clamp 和 Math.round 不同,ToUint8Clamp 舍入到一半,但是 Math.round 是四舍五入。
- 定义过程变量
prim
,执行 ToPrimitive(argument, hint Number)
将结果赋值给 prim
- 将
prim
值对照下表返回
参数类型 |
结果 |
Undefined |
返回类型错误 |
Null |
返回类型错误 |
Boolean |
true => 1n,false => 0n |
BigInt |
返回prim |
Number |
返回类型错误 |
String |
执行StringToBigInt(prim),如果结果是NaN ,返回类型错误,否则返回结果 |
Symbol |
返回类型错误 |
其实这里对 Number
类型的参数执行结果和上面的 ToNumber
一样令人困惑,但这里的本意是指 禁止我们开发中将Number隐式转换为BigInt,要想转换,必须显式调用
根据 重学js —— js数据类型:BigInt 对象 一章,BigInt(Number 类型值)
会做特殊处理,不会走 ToBigInt
方法
调用 https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type 处的算法,略微有点不同:
转换为64位有符号整数值,取值范围 Math.pow(2, -63) ~ Math.pow(2, 63) - 1
- 定义过程变量
n
,调用 ToBigInt(argument)
将结果赋值给 n
- 定义过程变量
int64bit
,对 n
进行2的64次幂 取模运算,将结果赋值给 int64bit
- 如果
int64bit
>= Math.pow(2, 63),返回 int64bit
- Math.pow(2, 64);否则返回 int64bit
转换为64位无符号整数值,取值范围 0 ~ Math.pow(2, 64) - 1
- 前两步与
ToBigInt64
一致
- 返回
int64bit
见表:
这里 Symbol
转为 String
,调用 toString()
或者 String()
来转换是OK的,但是直接使用 "" + Symbol()
会报错,本意应该还是希望我们进行显式转换。
见下表:
参数类型 |
结果 |
Undefined |
类型错误 |
Null |
类型错误 |
Boolean |
返回一个Boolean对象,其[[BooleanData]] 内置插槽值为argument,详见Boolean Objects |
Number |
返回一个Number对象,其[[NumberData]] 内置插槽值为argument,详见Number Objects |
String |
返回一个String对象,其[[StringData]] 内置插槽值为argument,详见String Objects |
Symbol |
返回一个Symbol对象,其[[SymbolData]] 内置插槽值为argument,详见Symbol Objects |
BigInt |
返回一个BigInt对象,其[[BigIntData]] 内置插槽值为argument,详见BigInt Objects |
Object |
返回argument |
听名字就知道,转换成对象能用的属性类型:String 和 Symbol
给类数组对象用的,确定其长度
- 先执行
ToInteger(argument)
并赋值给 len
- 如果
len
<= +0,返回 +0
- 否则返回 len 与 Math.pow(2, 53) 两者中较小的数
如果是ToString会生成的Number的字符串表示形式,则返回转换为数值的参数,或者是字符串 "-0"
。否则返回 undefined
。
有点绕人,就是看argument
能不能转换成 Number
,不能返回 undefined
,能得话区分 -0
。
如果是有效的整数索引值,则将其值参数转换为非负整数。
回归代码
回到开篇给出的那些代码,结合规范,其中一些代码能直接给出原因,例如:
+true // 1
+ false // 0
+null // 0
+undefined // NaN
Boolean(NaN) // false
Boolean(Infinity) // true
Boolean(-Infinity) // true
Boolean(Symbol()) // true
+Symbol() // 报错
''+Symbol() // 报错
+'-0' // -0 这里看上文的String转Number
+'Infinity' // Infinity 这里看上文的String转Number
+'-Infinity' // -Infinity 这里看上文的String转Number
但是还有很多代码,仍需要深究为什么?
例如以下代码,需要看Number::toString(argument)
''+(-0) // "0"
''+(-Infinity) // "-Infinity"
Number::toString(argument)中规定无论 +0 还是 -0,都返回 "0";小于0的argument会返回带-
的字符串"-argument"。
还有与对象相关的转换为 Number
或 String
:
+{} // NaN
+[] // 0
+[1] // 1
+[Infinity] // Infinity
+[-0] // 0
+ [1, 2] // NaN
+[{}] // NaN
''+[] // ""
''+[1] // "1"
''+[1,2,3] // "1,2,3"
''+[Infinity, Infinity] // "Infinity,Infinity"
''+{} // "[object Object]"
''+function test (){} // "function test (){}"
上面不论是转为 Number
还是 String
,其本质都需要调用 ToPrimitive 算法,问题的关键点就在该算法内部调用的OrdinaryToPrimitive算法的规则。其实本质上就是调用对象的 valurOf()
或 toString()
实现个ToPrimitive
const ToPrimitive = (input, PreferredType) => {
if (typeof input !== 'object' && typeof input !== "symbol") {
return input;
}
let hint;
if (!PreferredType) {
hint = "defaullt";
} else {
hint = typeof PreferredType === 'String' ? "string" : "number";
}
if (typeof input === "symbol") {
let exoticToPrim = Symbol.prototype[Symbol.toPrimitive];
if (exoticToPrim !== undefined) {
let result = exoticToPrim.call(input);
if (typeof result !== 'object') {
return result;
} else {
throw "TypeError";
}
}
}
if (hint === "defaullt") {
hint = "number";
}
if (hint === "number") {
if (typeof input.valueOf() !== 'object') {
return input.valueOf();
}
if (typeof input.toString() !== 'object') {
return input.toString();
}
} else {
if (typeof input.toString() !== 'object') {
return input.toString();
}
if (typeof input.valueOf() !== 'object') {
return input.valueOf();
}
}
throw "TypeError";
}
参考
- Js中的对象转换为数值类型时是不是返回结果都是NaN?
- JS那些巧妙的数据类型转换--实用版
2020-07-29 补充
数组类型转换时犯浑了!
[1, 2] + [2, 1] // 1,22,1
我误以为数组和对象的 toString
返回一致,其实我记错了,原题啊都能记错!!!
({}).toString() // "[object Object]"
我TM看到 [1, 2]
这种心想怎么转为数值啊?是不是转为 "[object Array]"
啊???
其实数组转为原始值,存在多个元素的话会通过类似 join(',')
的操作转为字符串的!!!