先记住这个答案
Number.prototype.valueOf() 返回的是原始类型 number。它的本职工作是拆箱:new Number(42) 是个对象,调 valueOf() 拿回原始值 42。对自定义对象来说,valueOf 是引擎做类型转换时会主动调用的钩子。对象参与 +、-、*、>、== 这类运算时,要先转成原始值,hint 是 number 或 default 时先调 valueOf,拿到原始值就用它来算。如果还定义了 Symbol.toPrimitive,它优先级更高,valueOf 就不会被调用。
valueOf返回原始number,不是对象- 对象参与运算前会先转成原始值
- 数字 hint 通常先试
valueOf Symbol.toPrimitive存在时两者都不调
包装对象拆箱和自定义对象参与运算
先看 Number 本身。typeof new Number(42) 是 object,这就是包装对象。平时写 (42).toFixed(2) 时,引擎会临时包一层,调完方法就扔掉,所以我们很少直接碰到它。valueOf 就是把这层壳剥掉的方法。
const n = new Number(42)
console.log(typeof n) // 包装对象
console.log(typeof n.valueOf()) // 拆出来的原始值
console.log(n.valueOf() === 42)
const price = {
valueOf() { return 99 },
toString() { return '¥99' }
}
console.log(price + 1) // 加法用 default hint,先调 valueOf
console.log(`${price}`) // 模板字符串用 string hint,先调 toString
console.log(price > 50) // 比较用 number hint查看输出与解释
object
number
true
100
¥99
trueprice 只是普通对象,但写了 valueOf 之后,price + 1 得到 100,price > 50 得到 true,引擎把它当成数字 99 来算。模板字符串走的是 string hint,所以先调 toString,拿到 '¥99'。这就是 valueOf 影响类型转换的方式:它不是被你调用的,而是被运算符隐式调用的。
引擎到底按什么顺序找方法
规范里这一步叫 ToPrimitive。引擎先看对象有没有 Symbol.toPrimitive 方法,有就把 hint('number'、'string'、'default' 三选一)传进去,直接用它的返回值。没有的话,hint 是 string 就按 toString → valueOf 的顺序试,否则按 valueOf → toString 的顺序试。哪个先返回原始值就用哪个。
valueOf 返回对象会被跳过JavaScriptconst weird = {
valueOf() { return {} }, // 不是原始值,引擎直接忽略
toString() { return '7' }
}
console.log(weird * 2) // 走到 toString,'7' 再转数字
const both = {
[Symbol.toPrimitive](hint) { return hint === 'number' ? 1 : 'text' },
valueOf() { return 100 } // 有 Symbol.toPrimitive 时根本不会被调用
}
console.log(+both)
console.log(both + '')查看输出与解释
14
1
text普通对象默认的 valueOf 返回的是对象自己,不是原始值,所以 {} + '' 最后会落到 toString,得到 '[object Object]'。这也解释了为什么多数对象拼字符串时表现得「只认 toString」:不是 valueOf 没被调用,而是调了但结果被丢掉了。
- 什么时候用
Symbol.toPrimitive更好 valueOf和toString只能各返回一种东西,控制不了 hint 细节。比如一个金额对象,希望做运算时是数字、显示时是带单位的字符串、用==比较时又是数字,这时写一个Symbol.toPrimitive按 hint 分支更直观,也不用担心两个方法谁先谁后。
容易答错的地方
- 以为
+会优先调toString - 很多人记成「遇到字符串拼接就调
toString」。实际上obj + ''的 hint 是 default,普通对象先调的还是valueOf。上面price + 1如果换成price + '',结果是'99'而不是'¥99'。只有模板字符串、String(obj)这类明确要字符串的场景才先调toString。 - 以为
new Number(0)是假值 - 包装对象是对象,对象转布尔永远是
true,这一步不走valueOf。所以if (new Number(0))会进分支。这也是不建议用new Number()的原因,直接用字面量或者不带new的Number(x)做转换。 - 以为能在任何值上调
Number.prototype.valueOf - 它会检查
this是不是数字或Number对象。写Number.prototype.valueOf.call('1')会抛TypeError,它不会顺手帮你把字符串转成数字。
面试官还会怎么问?
Date 对象相加为什么得到字符串?
Date 自己实现了 Symbol.toPrimitive,把 default hint 当成 string 处理。所以 new Date() + 1 拼出一串日期文字加个 1,而 new Date() - 0 是 number hint,得到毫秒时间戳。这是内置对象里少见的「default 当 string」的例外。
[] == 0 为什么是 true?
== 两边一个对象一个数字时,对象先做 ToPrimitive。数组默认的 valueOf 返回数组本身,被跳过,接着调 toString 得到空字符串 '','' 再转数字是 0,所以相等。同样的逻辑,[1] == 1 也是 true。
怎么让一个对象不能被隐式转成数字?
写一个 Symbol.toPrimitive,在 hint 为 number 时直接抛错,就能在误用时尽早发现问题。或者让 valueOf 和 toString 都返回对象,这样转换会抛 TypeError,只是报错信息没那么清楚。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。