跳到主要内容

对象转原始类型是根据什么流程运行的?

面试速答(30 秒版 TL;DR)

  • 对象参与 +==< 等运算时,都会先走 ToPrimitive(obj, hint)
  • 顺序(必须背):
    1. 先看 obj[Symbol.toPrimitive],有就调用它(传入 hint:"number"|"string"|"default")。
    2. 否则按 hint 选择顺序:
      • hint 是 "string":先 toString()valueOf()
      • hint 是 "number""default":先 valueOf()toString()
  • Date 很特殊:它把 "default" 当成 "string" 处理,所以 new Date() + 1 往往走字符串拼接。

Mermaid:一眼看懂 ToPrimitive 流程


为什么会有 hint?

不同语境期望的“默认语义”不同:

  • String(obj) 倾向拿“可读字符串”,所以 hint 通常是 "string"
  • 数值运算(如 obj - 1)倾向拿“数值”,hint 通常是 "number"
  • +== 等有历史兼容行为,常会用 "default"

关键例子(面试最常出)

例 1:[] 为什么经常表现得像空字符串?

String([]); // ""
Number([]); // 0
[] + 1; // "1"

解释口径:

  • [] 是对象,先 ToPrimitive。
  • 数组的 toString() 结果是 ""(空数组)或逗号分隔的元素字符串。
  • 再根据语境 ToNumber/ToString。

例 2:{} + 1 为什么很坑?

这题受“语法位置”和解析方式影响(块语句 vs 对象字面量),面试更建议你答:

  • “对象参与 + 会先 ToPrimitive,但 {} + 1 这个表达式本身有解析歧义,业务代码不要写这种。”

如何自定义对象的“转换结果”?

优先级从高到低:

  1. Symbol.toPrimitive
  2. valueOf
  3. toString

示例:

const a = {
i: 1,
[Symbol.toPrimitive](hint) {
if (hint === "string") return `i=${this.i}`;
return this.i++;
},
};

String(a); // "i=1"
a + 1; // 2 (a 先变 1,再加 1)
a + 1; // 3

易错点

  • ToPrimitive 要求返回“原始值”(primitive)。如果 Symbol.toPrimitive / valueOf / toString 返回的还是对象,规范会继续尝试或最终抛 TypeError
  • Date 的默认 hint 行为和普通对象不一样,别把“普通对象的默认顺序”硬套到 Date 上。