这篇文章主要记录从对象转换为 Primitive Value 的过程,至于基本类型之间的转换,可以看看其他的文章,或者直接去看规范。
首先,我们需要知道,下文中会出现的一些函数,比如 ToObject、ToString、ToNumber 以及 ToPrimitive 等等,都是在规范中实现的函数,我们没有办法直接通过 JavaScript 访问到。
valueOf
valueOf()方法返回指定对象的原始值。来自MDN
一般情况下,所有的对象都可访问到 valueOf 方法,比如 Array,虽然自身没有 valueOf 方法,但是可以根据原型链从 Object.prototype 中找到。
Object 原型上的 valueOf 方法会调用规范中的 ToObject(argument) 函数。顾名思义,这个函数会返回一个对象。如果 argument 本身就是对象,就返回自身;如果是 null 与 undefined 则抛错;像 boolean、string、number 以及 symbol 这样的基本类型,就会返回对应的包装对象。相反,如果这些包装类型调用 valueOf 方法,则会返回对应的基本类型值,并不是返回自身,因为这些包装类型都实现了自己的 valueOf 方法。
我们可以将 ToObject() 大致类比为 Object(),只不过后者在处理 null 和 undefined 的时候会返回 {}。
toString
toString()方法返回一个表示该对象的字符串。来自MDN
提一下 Number.prototype.toString 以及 Array.prototype.toString 吧。
-
Number.prototype.toString接受一个参数radix,可以是二进制,八进制等等,当然默认是十进制。 -
Array.prototype.toString内部调用的是join方法,join的实现在这里
ToPrimitive(input[, preferredType])
The abstract operation ToPrimitive converts its input argument to a non-Object type.
来自规范
简单讲 ToPrimitive 会将一个值转换为 Primitive Value。在梳理 ToPrimitive 的执行过程前,我们先了解一个内置的 Symbol 值。
-
Symbol.toPrimitive
这个值可以作为对象的属性名,指向一个方法,用于控制对象如何转换为 Primitive Value。这个方法会在
ToPrimitive的执行过程中使用到。并不是所有内置对象都有这个属性的,只有Date.prototype以及Symbol.prototype存在这个属性。虽然普通对象上没有这个属性,但我们是可以手动添加上这个属性,比如:var o = { [Symbol.toPrimitive]: function(hint) {} }
下面就梳理下 ToPrimitive 的执行过程:
-
如果 input 的类型是
objecta. 声明一个变量
hint,如果 preferredType 不存在,将hint赋值为defaultb. 如果 preferredType 为
string或number,将hint赋值为string或numberc. 判断 input 是否存在
Symbol.toPrimitive属性,如果存在则调用该属性指向的方法- 如果返回值是 Primitive Value,则返回该值;否则抛错
d. 不存在该方法,当
hint为default是,重新赋值为numbere. 如果
hint为string,则按顺序调用 input 的toString以及valueOf方法,直到返回 Primitive Valuef. 如果
hint为number,则按顺序调用 input 的valueOf以及toString方法,直到返回 Primitive Valueg. 如果最终没有返回 Primitive Value,则抛错
-
input 本身就是一个 Primitive Value,直接返回 input
其中步骤 e-f 对应的是规范中的 OrdinaryToPrimitive 函数
ToNumber(argument)
当尝试把一个对象转换为数字时,会有以下两个步骤:
-
调用
ToPrimitive(input, Number),返回值为primValue -
调用
ToNumber(primValue)
比如我们会用到的 Number(value),就会使用到 ToNumber 这个内部函数(前提是你传了一个参数,不然的话就直接返回 0 了)。另外,一元 + 运算也相当于 Number(value)
举个例子:Number({}) // NaN
-
将
{}作为 argument,调用ToNumber(argument) -
将
{}作为 input,调用ToPrimitive(input, Number),hint被赋值为number -
因为
{}不存在Symbol.toPrimitive属性,所以按顺序调用valueOf以及toString -
调用
valueOf返回自身,不是 Primitive Value -
调用
toString返回[object Object] -
将
[object Object]返回,作为primValue -
调用
ToNumber(primValue)返回NaN
简单讲:一般情况下,尝试将对象转换为数字时,会调用 valueOf 以及 toString,直到返回 Primitive Value。
ToString(argument)
与 ToNumber 类似,当尝试把对象转换为字符串时,也会有两个步骤:
-
调用
ToPrimitive(input, String),返回值为primValue -
调用
ToString(primValue)
举个例子:String({}) // [object Object]
-
将
{}作为 argument,调用ToString(argument) -
将
{}作为 input,调用ToPrimitive(input, String),hint被赋值为string -
因为
{}不存在Symbol.toPrimitive属性,所以按顺序调用toString以及valueOf -
调用
toString返回[object Object],是一个 Primitive Value -
将
[object Object]返回,作为primValue -
调用
ToString(primValue)返回[object Object]
简单讲:一般情况下,尝试将对象转换为字符串时,会调用 toString 以及 valueOf,直至返回 Primitive Value。
二元 + 运算
我们知道 + 不仅能进行数学加法,又可以连接字符串。不仅 1 + '1' 可以执行,甚至像 null + 1、[] + {} 等等运算都可以执行。我们可以根据规范梳理下二元 + 的运算过程,在这个过程中也用到了 ToPrimitive 函数:
-
对两个操作数调用
ToPrimitive(input),此时没有指定 preferredType,hint会被赋值为default -
判断两个返回值的类型,如果其中有一个为
stringa. 对两个返回值执行
ToStringb. 进行字符串连接
-
对两个返回值执行
ToNumber,执行数学加法
以 [] + {} 为例,分析一下:
-
执行
ToPrimitive([]),根据规则,会调用[].toString(),返回值为'' -
执行
ToPrimitive({}),同样会调用({}).toString(),返回值为[object Object] -
进行字符串连接得到
[object Object]
那么,如果手动改变了 valueOf 或者 toString 的行为呢,比如:
var o1 = {
valueOf: function () {
return 1
}
}
var o2 = {
toString: function () {
return 2
}
}
当执行 o1 + o2 时,最终就是执行数学加法,结果为 3。
== 运算
根据规范,x == y 运算的执行过程如下:
-
如果 x 与 y 的类型相同,执行
x === y(===执行步骤) -
如果 x 与 y 中,其中一个为
undefined另一个为null,则返回true -
如果 x 与 y 中,其中一个为
string另一个为number,则返回ToNumber(one) == another的结果 -
如果 x 与 y 中,存在一个
boolean,则返回ToNumber(one) == another的结果 -
如果 x 与 y 中,其中一个为
object,另一个为任一string、number或者symbol,则返回ToPrimitive(one) == another的结果 -
以上情况之外,返回
false
我们以 [] == ![] 为例来分析这个过程:
-
![]会调用ToBoolean([])并取反,得到结果false -
比较
[] == false,根据上文步骤 4,得到[] == 0 -
根据上文步骤 5,得到
'' == 0 -
根据上文步骤 3,得到
0 == 0 -
返回
true
再看 Symbol.toPrimitive
上文提到,在一般情况下,当对象转字符串或者数字时,会调用 valueOf 以及 toString。那么,除一般情况以外会是怎么样呢?
回到 Symbol.toPrimitive 属性,我们现在手动为普通对象添加这个属性(上文说过,除了 Date.prototype 和 Symbol.prototype,其他对象都没有这个属性)。
var o = {
[Symbol.toPrimitive]: function (hint) {
switch (hint) {
case 'number':
return 1
case 'string':
return 'str'
case 'default':
return 'default'
default:
throw new Error()
}
}
}
Number(o) // 1
String(o) // 'str'
o + 1 // 'default1'
如果理解了上文的 ToPrimitive 函数,就可以知道:如果一个对象存在 Symbol.toPrimitive 属性,那么 valueOf 以及 toString 方法都不会被调用了。
最后
总结,emmmm...全文都是总结