原文链接: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():
// 输出
alert(obj);
// 将对象作为属性名
anotherObj[obj] = 123;
"number"
当一个操作的期望值是数字的时候,就会发生对象到数字的转换,像算术运算:
// 显式转换
let num = Number(obj);
// 算术运算(除了两元加操作符)
let n = +obj; // 一元加操作符
let delta = date1 - date2;
// 大于/小于比较
let greater = user1 > user2;
"default"
在极少数情况下,操作符“不确定”会发生什么类型的转换。
例如,两元加 + 既对字符串操作生效(连接字符串),也对数字生效(相加),所以操作数既可以是字符串,也可以是数字。或者用一个对象跟字符串、数字或者 Symbol 进行 == 比较的时候。
// 两元加
let total = car1 + car2;
// obj === string/number/symbol
if (user === 1) { ... }
大于/小于运算符 <> 也可以作用在字符串和数字上,但是它的 hint 是“number”,不是“default”。这是历史原因。
在实践中,所以内置对象(Date 对象除外,之后学到)实现的“default”转换与“number”是一样的。也许我们也应该这样做。
请注意,仅有这 3 个 hint 可能取值。比较简单,没有“boolean”hint(所以对象转换成布尔值都为 true)。如果考虑“default”和“default”处理逻辑一样的话,也就是多数内置对象仅有两种转换方式。
转换过程中,JavaScript 会查找和调用下列 3 个对象方法:
调用 obj[Symbol.toPrimitive],如果存在的话。
否则,如果 hint 值为“string”
- 不管存在与否,尝试调用 obj.toString() 和 obj.valueOf()。
- 否则,如果 hint 值为“number”或者“default”
- 不管存在与否,尝试调用 obj.valueOf() 和 obj.toString()。
Symbol.toPrimitive
我们从第一个方法开始说起,就是内置的 Symbol.toPrimitive 方法,这个特殊的 Symbol 类型值作为对象转换时使用的方法名存在。像这样:
obj[Symbol.toPrimitive] = function(hint) {
// 返回一个原始类型值
// hint 等于 "string", "number", "default" 中的值之一
}
例如,这里的 user 对象实现了它:
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// 转换 demo:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
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 转换方法,达到了和上述代码一样的效果。
let user = {
name: "John",
money: 1000,
// 针对 hint="string"
toString() {
return `{name: "${this.name}"}`;
},
// 针对 hint="number" 或者 "default"
valueOf() {
return this.money;
}
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500
我们通常想要一个能够“捕获所有”处理转换到原始类型值的方法。这种场景下,我们可以通过仅实现 toString 来达到效果:
let user = {
name: "John",
toString() {
return this.name;
}
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500
在 Symbol.toPrimitive 和 valueOf 方法缺席的情况下,toString 会处理掉所有转换到原始类型值的场景。
ToPrimitive 和 ToString/ToNumber
有一件的重要的事情需要知道的是,所有的原始类型转换方法,都不必返回“hint”所表示的值类型。
没有控制说,toString 就应该返回自字符串,或者 Symbol.toPrimitive 方法在 hint 值是“number”时就该返回一个数字。
唯一一个强制条件是:这些方法必须返回原始类型值。
转换发生后,会得到一个原始类型值,接着继续使用这个值来操作,如果需要再进一步转换。
例如:
- 算术操作(除了两元操作符加)会发生 ToNumber 转换:
let obj = {
toString() { // 在其他方法缺失的情况下,toString 受理一切转换场景
return "2";
}
};
alert(obj * 2); // 4, ToPrimitive 后得到 "2", 然后变成 2
- 两元加检查原始类型值——如果是字符串,就进行连接操作;否则当成数字,发生 ToNumber 转换。
字符串例子:
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // 22 (ToPrimitive 返回字符串 => 连接)
数字例子:
let obj = {
toString() {
return true;
}
};
alert(obj + 2); // 3 (ToPrimitive 返回布尔值, 不是字符串,于是发生 ToNumber 转换)
历史原因
由于历史原因,toString 和 valueOf 方法都应该返回一个原始类型值。如果他们中的任一个返回了对象,也不会产生错误,而是那个返回的对象会被忽略(就像这个方法不存在一样)。
比较来说,Symbol.toPrimitive 必须返回一个原始类型值,否则,就会产生一个错误。
总结
对象到原始类型值的转换经常在某些场景下发生,比如许多的内置函数调用和操作符操作时,期望的是一个原始类型的值。
一共有 3 中类型(hint):
“string”(比如 alert 或者其他字符串转换)。
“number”(针对算术运算)。
“default”(少许操作)。
规范里明确描述了那个操作符使用哪个 hint。有一些极少的操作“不知道期望值类型是什么”,此时的 hint 值是“default”。对于内置对象,对“defauly”hint 的处理通常和“number”是一样的,因此在实践中,最后两个通常看做是一个。
转换的算法如下:
如果 obj[Symbol.toPrimitive] 方法存在,就调用。
否则,如果 hint 是“string”
- 不管存在与否,尝试调用 toString() 和 obj.valueOf() 方法。
- 否则,如果 hint 值是“number”或者“default”
- 不管存在与否,尝试调用 obj.valueOf() 和 obj.toString()。
在实践中,仅实现一个 toString() 方法就足够了,它会“捕捉所有”的对象转换的场景,然后返回一个“人类可读的”对象表示,为了达到记录和 debug 的目的。
(完)