JavaScript中加法运算为何优先调用对象的valueOf而非toString的技术疑问
嘿,这个问题戳中了JavaScript加法运算里一个超容易踩的坑!我当初刚啃ES规范的时候也被这个逻辑绕晕过,先直接看你的代码:
console.log("12"+ {"x":"1",toString(){return 10;},valueOf(){return 90}});
你预期结果是"1210"(调用toString()),但实际输出是"1290"(调用valueOf()),核心问题出在你对加法运算的类型转换hint参数理解偏差上——加法运算给ToPrimitive的hint是default,而不是你以为的string。
先拆解ES规范里的加法运算核心逻辑(对应你提到的12.8.3章节):
- 先把左右两个操作数都转换为原始值(调用
ToPrimitive),这里的关键是:加法运算给ToPrimitive传递的hint参数是default,不是string; - 拿到两个原始值后,如果其中一个是字符串,就把另一个也转成字符串做拼接;否则做数字加法。
那ToPrimitive在hint为default时的规则是什么呢?
- 对于普通对象(非Date),
defaulthint等价于numberhint:先尝试调用对象的valueOf(),如果返回的是原始值(比如你的例子里返回90,是数字原始值),就直接用这个值;如果valueOf()返回的还是对象,才会去调用toString(); - 只有
Date对象是例外,defaulthint对Date来说等价于stringhint,会优先调用toString()。
回到你的代码:
- 右边的普通对象,
ToPrimitive用defaulthint,先执行valueOf()返回90(原始值); - 左边是字符串
"12",所以把90转成字符串"90",拼接后就是"1290"。
为了验证这个逻辑,我们可以改一下代码,让valueOf()返回一个对象:
console.log("12"+ {"x":"1",toString(){return 10;},valueOf(){return {}}});
这时候valueOf()返回的不是原始值,就会 fallback 到toString(),输出结果就是你预期的"1210"。
再对比一个明确用string hint的场景,比如直接用String()构造函数:
console.log(String({"x":"1",toString(){return 10;},valueOf(){return 90}}));
这里ToPrimitive的hint是string,会直接优先调用toString(),输出"10"。
最后把规范里的关键内容贴出来帮你巩固:
12.8.3 The Addition Operator ( + )
NOTE
The addition operator either performs string concatenation or numeric addition.
12.8.3.1 Runtime Semantics: Evaluation
AdditiveExpression: AdditiveExpression + MultiplicativeExpression
- 计算左操作数的引用lref,获取其值lval;
- 计算右操作数的引用rref,获取其值rval;
- 将lval转换为原始值lprim,调用
ToPrimitive(lval, "default");- 将rval转换为原始值rprim,调用
ToPrimitive(rval, "default");- 如果lprim或rprim是字符串,则将另一个原始值转换为字符串并执行拼接;否则将两个原始值转换为数字并执行加法。
7.1.1 ToPrimitive(input [, PreferredType])
- 若PreferredType为
default,对于普通对象,等价于PreferredType为number;对于Date对象,等价于string;- 对于PreferredType为
number的情况:先调用input.valueOf(),若返回原始值则使用该值;否则调用input.toString(),若返回原始值则使用该值;否则抛出TypeError。
这样是不是就把逻辑理得明明白白了?下次遇到加法运算的类型转换,先想清楚ToPrimitive的hint参数是什么,就不会踩坑啦!
备注:内容来源于stack exchange,提问作者Achiyant

