# 对象的原始值转换
# ToPrimitive
对象可以转换的类型(变体),被称为 hint
, hint
有 三种模式:
# "string"
对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”:
// 输出
alert(obj);
// 将对象作为属性键
anotherObj[obj] = 123;
# "number"
对象到数字的转换,例如当我们进行数学运算时:
// 显式转换
let num = Number(obj);
// 数学运算(除了二进制加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小于/大于的比较
let greater = user1 > user2;
# "default"
在少数情况下发生,当运算符“不确定”期望值的类型时。
例如,二进制加法 +
可用于字符串(连接),也可以用于数字(相加),所以字符串和数字这两种类型都可以。因此,当二元加法得到对象类型的参数时,它将依据 "default"
hint 来对其进行转换。
此外,如果对象被用于与字符串、数字或 symbol 进行 ==
比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default"
hint。
// 二元加法使用默认 hint
let total = obj1 + obj2;
// obj == number 使用默认 hint
if (user == 1) { ... };
像 <
和 >
这样的小于/大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 “number” hint,而不是 “default”。这是历史原因。
# js尝试转换对象的过程
- 调用
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键Symbol.toPrimitive
(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"
—— 尝试obj.toString()
和obj.valueOf()
,无论哪个存在。 - 否则,如果 hint 是
"number"
或"default"
—— 尝试obj.valueOf()
和obj.toString()
,无论哪个存在。
# Symbol.toPrimitive
我们从第一个方法开始(个人觉得指的是从Object之初)。有一个名为 Symbol.toPrimitive
的内建 symbol,它被用来给转换方法命名,像这样:
obj[Symbol.toPrimitive] = function(hint) {
// 返回一个原始值
// hint = "string"、"number" 和 "default" 中的一个
}
然后我们可以再对象里实现他:
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// 转换演示:
// 这里hint的值的决定参见上面的[ToPrimitive]
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
# toString/valueOf
方法 toString 和 valueOf 来自上古时代。它们不是 symbol(那时候还没有 symbol 这个概念),而是“常规的”字符串命名的方法。它们提供了一种可选的“老派”的实现转换的方法。
如果没有 Symbol.toPrimitive,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:
- 对于 “string” hint,toString -> valueOf。
- 其他情况,valueOf -> toString。
这些方法必须返回一个原始值。如果 toString 或 valueOf 返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。
默认情况下,普通对象具有 toString 和 valueOf 方法:
- toString 方法返回一个字符串 "[object Object]"。
- valueOf 方法返回对象自身
接下来将上面的 user
例子在用这俩实现一下:
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
而如果没有 valueOf
:
let user = {
name: "John",
// toString 在没有其他方法的情况下处理所有转换
toString() {
return this.name;
}
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500
# 总结
对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。 这里有三种类型(hint):
"string"
(对于alert
和其他需要字符串的操作)"number"
(对于数学运算)"default"
(少数运算符)
规范明确描述了哪个运算符使用哪个 hint。很少有运算符“不知道期望什么”并使用 "default"
hint。通常对于内建对象,"default"
hint 的处理方式与 "number"
相同,因此在实践中,最后两个 hint 常常合并在一起。
转换算法是:
- 调用
obj[Symbol.toPrimitive](hint)
如果这个方法存在, - 否则,如果 hint 是
"string"
- 尝试
obj.toString()
和obj.valueOf()
,无论哪个存在。
- 尝试
- 否则,如果 hint 是
"number"
或者"default"
- 尝试
obj.valueOf()
和obj.toString()
,无论哪个存在
- 尝试
# 小例子
# 任意数量的括号求和
写一个函数 sum
,它有这样的功能:
sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15
解:
function sum(a) {
let currentSum = a;
function f(b) {
currentSum += b;
return f;
}
f.toString = function() {
return currentSum;
};
return f;
}
alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15