2024 年 3 月 31 日

对象转基本类型

当对象被添加 obj1 + obj2、减去 obj1 - obj2 或使用 alert(obj) 打印时会发生什么?

JavaScript 不允许您自定义运算符在对象上工作的方式。与 Ruby 或 C++ 等其他一些编程语言不同,我们无法实现一个特殊的对象方法来处理加法(或其他运算符)。

对于此类操作,对象会自动转换为基本类型,然后在这些基本类型上执行操作并得到一个基本类型值。

这是一个重要的限制:obj1 + obj2(或另一个数学运算)的结果不能是另一个对象!

例如,我们不能创建表示向量或矩阵(或成就或其他任何内容)的对象,将它们相加并期望得到一个“求和”对象作为结果。这样的架构壮举会自动“脱离正轨”。

因此,由于我们在技术上无法在此处做太多事情,因此在实际项目中没有对象数学。当它发生时,除了极少数例外,这是因为编码错误。

在本章中,我们将介绍对象如何转换为基本类型以及如何对其进行自定义。

我们有两个目的

  1. 如果编码错误导致意外发生此类操作,这将使我们能够了解正在发生的事情。
  2. 有例外,此类操作是可能的,并且看起来不错。例如,减去或比较日期(Date 对象)。我们稍后会遇到它们。

转换规则

类型转换 一章中,我们已经看到了基本类型的数字、字符串和布尔转换规则。但我们为对象留了一个空白。现在,当我们了解了方法和符号后,就可以填补它了。

  1. 没有转换为布尔。在布尔上下文中,所有对象都是 true,就这么简单。只存在数字和字符串转换。
  2. 当我们减去对象或应用数学函数时,就会发生数字转换。例如,Date 对象(将在 日期和时间 一章中介绍)可以相减,而 date1 - date2 的结果是两个日期之间的时间差。
  3. 至于字符串转换——它通常发生在我们使用 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 尝试查找并调用三个对象方法

  1. 调用 obj[Symbol.toPrimitive](hint) – 具有符号键 Symbol.toPrimitive(系统符号)的方法,如果存在此类方法,
  2. 否则,如果提示为 "string"
    • 尝试调用 obj.toString()obj.valueOf(),无论存在哪一个。
  3. 否则,如果提示为 "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 会尝试查找方法 toStringvalueOf

  • 对于 "string" 提示:调用 toString 方法,如果它不存在或如果它返回对象而不是基元值,则调用 valueOf(因此 toString 优先用于字符串转换)。
  • 对于其他提示:调用 valueOf,如果它不存在或如果它返回对象而不是基元值,则调用 toString(因此 valueOf 优先用于数学运算)。

方法 toStringvalueOf 源于古代。它们不是符号(符号在很久以前不存在),而是“常规”字符串命名的方法。它们提供了一种替代的“旧式”方式来实现转换。

这些方法必须返回基元值。如果 toStringvalueOf 返回对象,则忽略它(与没有方法相同)。

默认情况下,普通对象具有以下 toStringvalueOf 方法

  • toString 方法返回字符串 "[object Object]"
  • valueOf 方法返回对象本身。

以下是演示

let user = {name: "John"};

alert(user); // [object Object]
alert(user.valueOf() === user); // true

因此,如果我们尝试将对象用作字符串,例如在 alert 中,那么默认情况下我们会看到 [object Object]

此处仅提及默认 valueOf 是为了完整性,以避免任何混淆。如您所见,它返回对象本身,因此被忽略。不要问我为什么,这是出于历史原因。因此我们可以假设它不存在。

让我们实现这些方法以自定义转换。

例如,此处 user 使用 toStringvalueOf 的组合而不是 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.toPrimitivevalueOf 的情况下,toString 将处理所有原始转换。

转换可以返回任何原始类型

了解所有原始转换方法的重要一点是,它们不一定返回“提示”的原始类型。

无法控制 toString 是否准确返回一个字符串,或者 Symbol.toPrimitive 方法是否为提示 "number" 返回一个数字。

唯一强制要求:这些方法必须返回一个原始类型,而不是一个对象。

历史记录

出于历史原因,如果 toStringvalueOf 返回一个对象,则没有错误,但此类值将被忽略(就像该方法不存在一样)。这是因为在远古时代,JavaScript 中没有良好的“错误”概念。

相比之下,Symbol.toPrimitive 更严格,它必须返回一个原始类型,否则将出现错误。

进一步转换

正如我们已经知道的那样,许多运算符和函数执行类型转换,例如乘法 * 将操作数转换为数字。

如果我们传递一个对象作为参数,那么将有两个计算阶段

  1. 对象转换为原始类型(使用上面描述的规则)。
  2. 如果进一步计算需要,则结果原始类型也将被转换。

例如

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
  1. 乘法 obj * 2 首先将对象转换为原始类型(即字符串 "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" 相同的方式实现它)

规范明确描述了哪个运算符使用哪个提示。

转换算法是

  1. 如果该方法存在,则调用 obj[Symbol.toPrimitive](hint)
  2. 否则,如果提示为 "string"
    • 尝试调用 obj.toString()obj.valueOf(),无论存在哪一个。
  3. 否则,如果提示为 "number""default"
    • 尝试调用 obj.valueOf()obj.toString(),无论存在哪一个。

所有这些方法都必须返回一个原始类型才能工作(如果已定义)。

在实践中,通常只需实现 obj.toString() 作为字符串转换的“万能”方法,该方法应返回对象的“人类可读”表示,用于记录或调试目的。

教程地图

评论

在评论之前请阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码 – 将它们包装在 <pre> 标签中,对于 10 行以上的代码 – 使用沙盒 (plnkrjsbincodepen…)