先记住这个答案
对象参与运算时要先转成原始值。引擎会先查对象上有没有 [Symbol.toPrimitive] 方法,有就调用它,并传入一个 hint 参数表示这次希望得到什么类型:"number"、"string" 或 "default"。方法返回的原始值就是转换结果,valueOf 和 toString 完全不参与。没有这个方法时,才退回老规则:hint 是 string 先调 toString,否则先调 valueOf。
- 有
Symbol.toPrimitive时优先调用它 - hint 有三种:数字、字符串、默认
+与==使用 default hint- 必须返回原始值,返回对象会抛
TypeError
一个对象,三种场合三种结果
拿一个商品价格对象来试,在方法里把收到的 hint 打印出来,就能看到每种运算到底传了什么。
const price = {
[Symbol.toPrimitive](hint) {
console.log('hint:', hint)
return hint === 'string' ? '¥99' : 99
}
}
console.log(+price) // 一元加号要数字
console.log(`${price}`) // 模板字符串要字符串
console.log(price + 1) // 二元加号不确定
console.log(price > 50) // 比较运算要数字查看输出与解释
hint: number
99
hint: string
¥99
hint: default
100
hint: number
true前两行好理解。第三行 price + 1 收到的是 default,因为 + 既能做加法又能拼字符串,引擎在转换之前不知道你要哪个,就把决定权交给对象。这里对 default 返回了数字 99,所以结果是 100。如果改成对 default 也返回 '¥99',结果就会变成字符串 '¥991'。
没写 Symbol.toPrimitive 时怎么转
绝大多数对象没有这个方法,这时走的是规范里的 OrdinaryToPrimitive 流程,也就是 valueOf 和 toString 二选一的老办法。hint 是 string 时先调 toString,拿到原始值就用,拿不到再试 valueOf;hint 是 number 或 default 时顺序反过来。
普通对象的 valueOf 返回对象自己,不是原始值,所以最后总会落到 toString,得到 "[object Object]"。这就是 ({}) + 1 结果是 "[object Object]1" 的原因。数组的 toString 会用逗号拼接元素,所以 [1, 2] + '' 是 "1,2"。
- 为什么推荐用
Symbol.toPrimitive代替重写valueOf - 重写
valueOf和toString也能控制转换,但你没法知道当前是什么场合,只能被动地被按顺序调用。Symbol.toPrimitive直接拿到 hint,一个方法里就能把三种场合写清楚,逻辑集中也好测试。
Date 为什么是个例外
Date.prototype 自带了一个 [Symbol.toPrimitive],它把 default 当成 string 处理。这就解释了日期对象一个很迷惑的表现:加法变成拼接,减法却能算时间差。
const d = new Date(0)
console.log(typeof (d + 1)) // default 按 string 处理,变成拼接
console.log(d - 1) // 减法传 number,拿到时间戳 0 再减 1
console.log(typeof d[Symbol.toPrimitive])查看输出与解释
string
-1
function所以算两个日期的间隔可以直接写 end - start,但想给日期加一毫秒不能写 d + 1,要写 d.getTime() + 1 或 +d + 1。
容易答错的地方
- 以为二元
+传的是number - 二元
+和==传的都是default。只有一元+、减乘除运算,以及<这类大小比较才传number。这个细节决定了Date为什么加减表现不一样。 - 以为有了
Symbol.toPrimitive还会再调valueOf - 不会。只要这个方法存在,引擎就只调用它。它返回对象的话会直接抛
TypeError: Cannot convert object to primitive value,不会退回去试valueOf。 - 以为 hint 就是最终类型
- hint 只是一个建议,方法完全可以在
numberhint 下返回字符串。比如+obj拿到'abc',引擎会再把它转成数字,得到NaN。
面试官还会怎么问?
[] + {} 结果是什么,为什么?
结果是 "[object Object]"。两边都走 default hint,数组和普通对象都没有 Symbol.toPrimitive,valueOf 返回对象自身不算数,于是都调 toString。数组得到空字符串,对象得到 "[object Object]",拼接起来就是后者。
用对象当另一个对象的属性名会怎样?
属性名必须是字符串或 Symbol,所以会用 string hint 转换。普通对象都会变成 "[object Object]",两个不同对象当键会互相覆盖。需要用对象当键时应该换成 Map。
实际项目里什么时候会写 Symbol.toPrimitive?
金额、时长、版本号这类值对象最常用。比如一个 Money 类,数字场合返回分为单位的整数方便计算,字符串场合返回带货币符号的格式化文本,调用方不用每次手动调 format()。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。