这篇文章主要记录从对象转换为 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 的类型是 object
a. 声明一个变量 hint,如果 preferredType 不存在,将 hint 赋值为 default
b. 如果 preferredType 为 string 或 number,将 hint 赋值为 string 或 number
c. 判断 input 是否存在 Symbol.toPrimitive 属性,如果存在则调用该属性指向的方法
d. 不存在该方法,当 hint 为 default 是,重新赋值为 number
e. 如果 hint 为 string,则按顺序调用 input 的 toString 以及 valueOf 方法,直到返回 Primitive Value
f. 如果 hint 为 number,则按顺序调用 input 的 valueOf 以及 toString 方法,直到返回 Primitive Value
g. 如果最终没有返回 Primitive Value,则抛错
- 如果返回值是 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
判断两个返回值的类型,如果其中有一个为 string
a. 对两个返回值执行 ToString
b. 进行字符串连接
对两个返回值执行 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) // 1String(o) // 'str'o + 1 // 'default1'复制代码
如果理解了上文的 ToPrimitive 函数,就可以知道:如果一个对象存在 Symbol.toPrimitive 属性,那么 valueOf 以及 toString 方法都不会被调用了。
最后
总结,emmmm...全文都是总结