当对象被添加 obj1 + obj2
、减去 obj1 - obj2
或使用 alert(obj)
打印时会发生什么?
JavaScript 不允许您自定义运算符在对象上工作的方式。与 Ruby 或 C++ 等其他一些编程语言不同,我们无法实现一个特殊的对象方法来处理加法(或其他运算符)。
对于此类操作,对象会自动转换为基本类型,然后在这些基本类型上执行操作并得到一个基本类型值。
这是一个重要的限制:obj1 + obj2
(或另一个数学运算)的结果不能是另一个对象!
例如,我们不能创建表示向量或矩阵(或成就或其他任何内容)的对象,将它们相加并期望得到一个“求和”对象作为结果。这样的架构壮举会自动“脱离正轨”。
因此,由于我们在技术上无法在此处做太多事情,因此在实际项目中没有对象数学。当它发生时,除了极少数例外,这是因为编码错误。
在本章中,我们将介绍对象如何转换为基本类型以及如何对其进行自定义。
我们有两个目的
- 如果编码错误导致意外发生此类操作,这将使我们能够了解正在发生的事情。
- 有例外,此类操作是可能的,并且看起来不错。例如,减去或比较日期(
Date
对象)。我们稍后会遇到它们。
转换规则
在 类型转换 一章中,我们已经看到了基本类型的数字、字符串和布尔转换规则。但我们为对象留了一个空白。现在,当我们了解了方法和符号后,就可以填补它了。
- 没有转换为布尔。在布尔上下文中,所有对象都是
true
,就这么简单。只存在数字和字符串转换。 - 当我们减去对象或应用数学函数时,就会发生数字转换。例如,
Date
对象(将在 日期和时间 一章中介绍)可以相减,而date1 - date2
的结果是两个日期之间的时间差。 - 至于字符串转换——它通常发生在我们使用
alert(obj)
输出对象以及类似上下文中时。
我们可以使用特殊对象方法自己实现字符串和数字转换。
现在让我们深入了解技术细节,因为这是深入了解该主题的唯一途径。
提示
JavaScript 如何决定应用哪种转换?
有三种类型的转换,它们发生在不同的情况下。正如 规范 中所述,它们被称为“提示”
"string"
-
对于对象到字符串转换,当我们在对象上执行操作时,该对象需要一个字符串,例如
alert
// output alert(obj); // using object as a property key anotherObj[obj] = 123;
"number"
-
对于对象到数字转换,例如当我们在进行数学运算时
// explicit conversion let num = Number(obj); // maths (except binary plus) let n = +obj; // unary plus let delta = date1 - date2; // less/greater comparison let greater = user1 > user2;
大多数内置数学函数也包括此类转换。
"default"
-
在操作符“不确定”期望哪种类型时,在极少数情况下发生。
例如,二元加号
+
可以同时用于字符串(连接它们)和数字(相加)。因此,如果二元加号将对象作为参数,它将使用"default"
提示将其转换。此外,如果使用
==
将对象与字符串、数字或符号进行比较,那么也不清楚应该进行哪种转换,因此使用了"default"
提示。// binary plus uses the "default" hint let total = obj1 + obj2; // obj == number uses the "default" hint if (user == 1) { ... };
大于和小于比较运算符(例如
<
>
)也可以同时用于字符串和数字。不过,它们使用"number"
提示,而不是"default"
。这是出于历史原因。
不过,在实践中,事情会简单一些。
除了一个情况(Date
对象,我们稍后会了解它)之外,所有内置对象都以与 "number"
相同的方式实现 "default"
转换。我们也应该这样做。
不过,了解所有 3 个提示很重要,我们很快就会明白为什么。
为了进行转换,JavaScript 尝试查找并调用三个对象方法
- 调用
obj[Symbol.toPrimitive](hint)
– 具有符号键Symbol.toPrimitive
(系统符号)的方法,如果存在此类方法, - 否则,如果提示为
"string"
- 尝试调用
obj.toString()
或obj.valueOf()
,无论存在哪一个。
- 尝试调用
- 否则,如果提示为
"number"
或"default"
- 尝试调用
obj.valueOf()
或obj.toString()
,无论存在哪一个。
- 尝试调用
Symbol.toPrimitive
我们从第一个方法开始。有一个名为 Symbol.toPrimitive
的内置符号,应将其用于命名转换方法,如下所示
obj[Symbol.toPrimitive] = function(hint) {
// here goes the code to convert this object to a primitive
// it must return a primitive value
// hint = one of "string", "number", "default"
};
如果方法 Symbol.toPrimitive
存在,则它将用于所有提示,并且不需要更多方法。
例如,此处 user
对象实现了它
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// conversions demo:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
从代码中可以看到,user
根据转换成为自描述字符串或金额。单个方法 user[Symbol.toPrimitive]
处理所有转换情况。
toString/valueOf
如果没有 Symbol.toPrimitive
,则 JavaScript 会尝试查找方法 toString
和 valueOf
- 对于
"string"
提示:调用toString
方法,如果它不存在或如果它返回对象而不是基元值,则调用valueOf
(因此toString
优先用于字符串转换)。 - 对于其他提示:调用
valueOf
,如果它不存在或如果它返回对象而不是基元值,则调用toString
(因此valueOf
优先用于数学运算)。
方法 toString
和 valueOf
源于古代。它们不是符号(符号在很久以前不存在),而是“常规”字符串命名的方法。它们提供了一种替代的“旧式”方式来实现转换。
这些方法必须返回基元值。如果 toString
或 valueOf
返回对象,则忽略它(与没有方法相同)。
默认情况下,普通对象具有以下 toString
和 valueOf
方法
toString
方法返回字符串"[object Object]"
。valueOf
方法返回对象本身。
以下是演示
let user = {name: "John"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true
因此,如果我们尝试将对象用作字符串,例如在 alert
中,那么默认情况下我们会看到 [object Object]
。
此处仅提及默认 valueOf
是为了完整性,以避免任何混淆。如您所见,它返回对象本身,因此被忽略。不要问我为什么,这是出于历史原因。因此我们可以假设它不存在。
让我们实现这些方法以自定义转换。
例如,此处 user
使用 toString
和 valueOf
的组合而不是 Symbol.toPrimitive
执行与上述相同操作
let user = {
name: "John",
money: 1000,
// for hint="string"
toString() {
return `{name: "${this.name}"}`;
},
// for hint="number" or "default"
valueOf() {
return this.money;
}
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500
如我们所见,行为与使用 Symbol.toPrimitive
的上一个示例相同。
通常,我们希望有一个“万能”的地方来处理所有基元转换。在这种情况下,我们可以仅实现 toString
,如下所示
let user = {
name: "John",
toString() {
return this.name;
}
};
alert(user); // toString -> John
alert(user + 500); // toString -> John500
在没有 Symbol.toPrimitive
和 valueOf
的情况下,toString
将处理所有原始转换。
转换可以返回任何原始类型
了解所有原始转换方法的重要一点是,它们不一定返回“提示”的原始类型。
无法控制 toString
是否准确返回一个字符串,或者 Symbol.toPrimitive
方法是否为提示 "number"
返回一个数字。
唯一强制要求:这些方法必须返回一个原始类型,而不是一个对象。
出于历史原因,如果 toString
或 valueOf
返回一个对象,则没有错误,但此类值将被忽略(就像该方法不存在一样)。这是因为在远古时代,JavaScript 中没有良好的“错误”概念。
相比之下,Symbol.toPrimitive
更严格,它必须返回一个原始类型,否则将出现错误。
进一步转换
正如我们已经知道的那样,许多运算符和函数执行类型转换,例如乘法 *
将操作数转换为数字。
如果我们传递一个对象作为参数,那么将有两个计算阶段
- 对象转换为原始类型(使用上面描述的规则)。
- 如果进一步计算需要,则结果原始类型也将被转换。
例如
let obj = {
// toString handles all conversions in the absence of other methods
toString() {
return "2";
}
};
alert(obj * 2); // 4, object converted to primitive "2", then multiplication made it a number
- 乘法
obj * 2
首先将对象转换为原始类型(即字符串"2"
)。 - 然后
"2" * 2
变为2 * 2
(字符串转换为数字)。
二进制加号将在相同情况下连接字符串,因为它乐于接受字符串
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // "22" ("2" + 2), conversion to primitive returned a string => concatenation
总结
对象到原始类型的转换由许多内置函数和运算符自动调用,这些函数和运算符期望一个原始类型作为值。
它有 3 种类型(提示)
"string"
(用于alert
和其他需要字符串的操作)"number"
(用于数学)"default"
(少数运算符,通常对象以与"number"
相同的方式实现它)
规范明确描述了哪个运算符使用哪个提示。
转换算法是
- 如果该方法存在,则调用
obj[Symbol.toPrimitive](hint)
, - 否则,如果提示为
"string"
- 尝试调用
obj.toString()
或obj.valueOf()
,无论存在哪一个。
- 尝试调用
- 否则,如果提示为
"number"
或"default"
- 尝试调用
obj.valueOf()
或obj.toString()
,无论存在哪一个。
- 尝试调用
所有这些方法都必须返回一个原始类型才能工作(如果已定义)。
在实践中,通常只需实现 obj.toString()
作为字符串转换的“万能”方法,该方法应返回对象的“人类可读”表示,用于记录或调试目的。
评论
<code>
标签,对于多行代码 – 将它们包装在<pre>
标签中,对于 10 行以上的代码 – 使用沙盒 (plnkr、jsbin、codepen…)