原文链接:http://javascript.info/object-toprimitive,translate with ❤️ by zhangbao.

两个对象相加(obj1 + obj2)或者相减(obj1 - obj2)会得到什么呢?alert(obj) 的结果又是什么?

在对象中,存在一种特殊方法,就是在对象进行类型转换时使用的。

在《类型转换》一章里,我们探讨了原始数据类型布尔、数字和字符串的转换规则,但是我们留下对象没谈。现在,我们已经学习了方法和 Symbol 类型,所以可以学习它了。

对象没有到布尔值的转换,因为所有的对象转换成布尔值都为 true。所以只有到字符串和数字的转换。

在将两个对象相减,或者调用了算术函数时,发生的都是到数字的转换。例如,Date 对象(在《日期和时间》一章里讲解)可以用来相减,date1 - date2 的结果就是两个时间之间的差值(单位是毫秒)。

至于字符串转换,通常发生在比如我们在输出对象的操作里(像 alert(obj))或者类似场景下。

ToPrimitive

当使用对象的上下文环境期望的是一个原始类型值的时候,例如,alert 或者算术操作的时候,就会发生到基本类型值的转换,使用了 ToPrimitive 算法(规范)。

这个算法,以对象方法的形式呈现,允许我们自定义对象转换到原始类型值的规则。

转换依赖于称为“hint”的上下文环境。

可能的取值有 3 个:

"string"

当一个操作的期望值是字符串的时候,就会发生对象到字符串的转换,像 alert():

  1. // 输出
  2. alert(obj);
  3. // 将对象作为属性名
  4. anotherObj[obj] = 123;

"number"

当一个操作的期望值是数字的时候,就会发生对象到数字的转换,像算术运算:

  1. // 显式转换
  2. let num = Number(obj);
  3. // 算术运算(除了两元加操作符)
  4. let n = +obj; // 一元加操作符
  5. let delta = date1 - date2;
  6. // 大于/小于比较
  7. let greater = user1 > user2;

"default"

在极少数情况下,操作符“不确定”会发生什么类型的转换。

例如,两元加 + 既对字符串操作生效(连接字符串),也对数字生效(相加),所以操作数既可以是字符串,也可以是数字。或者用一个对象跟字符串、数字或者 Symbol 进行 == 比较的时候。

  1. // 两元加
  2. let total = car1 + car2;
  3. // obj === string/number/symbol
  4. if (user === 1) { ... }

大于/小于运算符 <> 也可以作用在字符串和数字上,但是它的 hint 是“number”,不是“default”。这是历史原因。

在实践中,所以内置对象(Date 对象除外,之后学到)实现的“default”转换与“number”是一样的。也许我们也应该这样做。

请注意,仅有这 3 个 hint 可能取值。比较简单,没有“boolean”hint(所以对象转换成布尔值都为 true)。如果考虑“default”和“default”处理逻辑一样的话,也就是多数内置对象仅有两种转换方式。

转换过程中,JavaScript 会查找和调用下列 3 个对象方法:

  1. 调用 obj[Symbol.toPrimitive],如果存在的话。

  2. 否则,如果 hint 值为“string”

  • 不管存在与否,尝试调用 obj.toString() 和 obj.valueOf()。
  1. 否则,如果 hint 值为“number”或者“default”
  • 不管存在与否,尝试调用 obj.valueOf() 和 obj.toString()。

Symbol.toPrimitive

我们从第一个方法开始说起,就是内置的 Symbol.toPrimitive 方法,这个特殊的 Symbol 类型值作为对象转换时使用的方法名存在。像这样:

  1. obj[Symbol.toPrimitive] = function(hint) {
  2. // 返回一个原始类型值
  3. // hint 等于 "string", "number", "default" 中的值之一
  4. }

例如,这里的 user 对象实现了它:

  1. let user = {
  2. name: "John",
  3. money: 1000,
  4. [Symbol.toPrimitive](hint) {
  5. alert(`hint: ${hint}`);
  6. return hint == "string" ? `{name: "${this.name}"}` : this.money;
  7. }
  8. };
  9. // 转换 demo:
  10. alert(user); // hint: string -> {name: "John"}
  11. alert(+user); // hint: number -> 1000
  12. alert(user + 500); // hint: default -> 1500

我们从代码里可以看到,user 依据转换场景不同,输出自描述的字符串或者多少钱。user[Symbol.toPrimitive] 这个方法处理了所有的转换情况。

toString/valueOf

toString 和 valueOf 方法来源自远古时代。它们不是 Symbol 值类型(那之前还没有 Symbol 类型),而是“普通的”字符串方法名。它们提供了“旧式的”实现转换的方式。

如果没有 Symbol.toPrimitive,那么 JavaScript 会按照下面的顺序查找和调用方法:

  • 针对 “string” hint:toString -> valueOf。

  • 其他情况,valueOf -> toString。

例如,下面的 user 使用了 toString 和 valueOf 转换方法,达到了和上述代码一样的效果。

  1. let user = {
  2. name: "John",
  3. money: 1000,
  4. // 针对 hint="string"
  5. toString() {
  6. return `{name: "${this.name}"}`;
  7. },
  8. // 针对 hint="number" 或者 "default"
  9. valueOf() {
  10. return this.money;
  11. }
  12. };
  13. alert(user); // toString -> {name: "John"}
  14. alert(+user); // valueOf -> 1000
  15. alert(user + 500); // valueOf -> 1500

我们通常想要一个能够“捕获所有”处理转换到原始类型值的方法。这种场景下,我们可以通过仅实现 toString 来达到效果:

  1. let user = {
  2. name: "John",
  3. toString() {
  4. return this.name;
  5. }
  6. };
  7. alert(user); // toString -> John
  8. alert(user + 500); // toString -> John500


在 Symbol.toPrimitive 和 valueOf 方法缺席的情况下,toString 会处理掉所有转换到原始类型值的场景。

ToPrimitive 和 ToString/ToNumber

有一件的重要的事情需要知道的是,所有的原始类型转换方法,都不必返回“hint”所表示的值类型。

没有控制说,toString 就应该返回自字符串,或者 Symbol.toPrimitive 方法在 hint 值是“number”时就该返回一个数字。

唯一一个强制条件是:这些方法必须返回原始类型值。

转换发生后,会得到一个原始类型值,接着继续使用这个值来操作,如果需要再进一步转换。

例如:

  • 算术操作(除了两元操作符加)会发生 ToNumber 转换:
  1. let obj = {
  2. toString() { // 在其他方法缺失的情况下,toString 受理一切转换场景
  3. return "2";
  4. }
  5. };
  6. alert(obj * 2); // 4, ToPrimitive 后得到 "2", 然后变成 2
  • 两元加检查原始类型值——如果是字符串,就进行连接操作;否则当成数字,发生 ToNumber 转换。

字符串例子:

  1. let obj = {
  2. toString() {
  3. return "2";
  4. }
  5. };
  6. alert(obj + 2); // 22 (ToPrimitive 返回字符串 => 连接)

数字例子:

  1. let obj = {
  2. toString() {
  3. return true;
  4. }
  5. };
  6. alert(obj + 2); // 3 (ToPrimitive 返回布尔值, 不是字符串,于是发生 ToNumber 转换)

历史原因

由于历史原因,toString 和 valueOf 方法都应该返回一个原始类型值。如果他们中的任一个返回了对象,也不会产生错误,而是那个返回的对象会被忽略(就像这个方法不存在一样)。

比较来说,Symbol.toPrimitive 必须返回一个原始类型值,否则,就会产生一个错误。

总结

对象到原始类型值的转换经常在某些场景下发生,比如许多的内置函数调用和操作符操作时,期望的是一个原始类型的值。

一共有 3 中类型(hint):

  • “string”(比如 alert 或者其他字符串转换)。

  • “number”(针对算术运算)。

  • “default”(少许操作)。

规范里明确描述了那个操作符使用哪个 hint。有一些极少的操作“不知道期望值类型是什么”,此时的 hint 值是“default”。对于内置对象,对“defauly”hint 的处理通常和“number”是一样的,因此在实践中,最后两个通常看做是一个。

转换的算法如下:

  1. 如果 obj[Symbol.toPrimitive] 方法存在,就调用。

  2. 否则,如果 hint 是“string”

  • 不管存在与否,尝试调用 toString() 和 obj.valueOf() 方法。
  1. 否则,如果 hint 值是“number”或者“default”
  • 不管存在与否,尝试调用 obj.valueOf() 和 obj.toString()。

在实践中,仅实现一个 toString() 方法就足够了,它会“捕捉所有”的对象转换的场景,然后返回一个“人类可读的”对象表示,为了达到记录和 debug 的目的。

(完)