对象 — 原始值转换

当对象相加 obj1 + obj2,相减 obj1 - obj2,或者应用 alert(obj) 打印时会产生什么?

在这种状况下,对象会被主动转换为原始值,而后执行操作。

在 类型转换 一章中,咱们曾经看到了数值,字符串和布尔转换的规定。然而咱们没有讲对象的转换规则。当初咱们曾经把握了办法(method)和 symbol 的相干常识,能够开始学习对象原始值转换了。

  1. 所有的对象在布尔上下文(context)中均为 true。所以对于对象,不存在 to-boolean 转换,只有字符串和数值转换。
  2. 数值转换产生在对象相减或利用数学函数时。例如,Date 对象(将在 日期和工夫 一章中介绍)能够相减,date1 - date2 的后果是两个日期之间的差值。
  3. 至于字符串转换 —— 通常产生在咱们像 alert(obj) 这样输入一个对象和相似的上下文中。

ToPrimitive

咱们能够应用非凡的对象办法,对字符串和数值转换进行微调。

上面是三个类型转换的变体,被称为 "hint",在 标准 中有具体介绍(译注:当一个对象被用在须要原始值的上下文中时,例如,在 alert 或数学运算中,对象会被转换为原始值):

"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。

// 二元加法应用默认 hintlet total = obj1 + obj2;// obj == number 应用默认 hintif (user == 1) { ... };

<> 这样的小于/大于比拟运算符,也能够同时用于字符串和数字。不过,它们应用 "number" hint,而不是 "default"。这是历史起因。

实际上,咱们没有必要记住这些奇异的细节,除了一种状况(Date 对象,咱们稍后会学到它)之外,所有内建对象都以和 "number" 雷同的形式实现 "default" 转换。咱们也能够这样做。

没有 "boolean" hint
请留神 —— 只有三种 hint。就这么简略。

没有 "boolean" hint(在布尔上下文中所有对象都是 true)或其余任何货色。如果咱们将 "default""number" 视为雷同,就像大多数内建函数一样,那么就只有两种转换了。

为了进行转换,JavaScript 尝试查找并调用三个对象办法:

  1. 调用 obj[Symbol.toPrimitive](hint) —— 带有 symbol 键 Symbol.toPrimitive(零碎 symbol)的办法,如果这个办法存在的话,
  2. 否则,如果 hint 是 "string" —— 尝试 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 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;  }};// 转换演示:alert(user); // hint: string -> {name: "John"}alert(+user); // hint: number -> 1000alert(user + 500); // hint: default -> 1500

从代码中咱们能够看到,依据转换的不同,user 变成一个自描述字符串或者一个金额。单个办法 user[Symbol.toPrimitive] 解决了所有的转换状况。

toString/valueOf

办法 toStringvalueOf 来自上古时代。它们不是 symbol(那时候还没有 symbol 这个概念),而是“惯例的”字符串命名的办法。它们提供了一种可选的“老派”的实现转换的办法。

如果没有 Symbol.toPrimitive,那么 JavaScript 将尝试找到它们,并且依照上面的程序进行尝试:

  • 对于 "string" hint,toString -> valueOf
  • 其余状况,valueOf -> toString

这些办法必须返回一个原始值。如果 toStringvalueOf 返回了一个对象,那么返回值会被疏忽(和这里没有办法的时候雷同)。

默认状况下,一般对象具备 toStringvalueOf 办法:

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

上面是一个示例:

let user = {name: "John"};alert(user); // [object Object]alert(user.valueOf() === user); // true

所以,如果咱们尝试将一个对象当做字符串来应用,例如在 alert 中,那么在默认状况下咱们会看到 [object Object]

这里提到默认值 valueOf 只是为了残缺起见,以防止混同。正如你看到的,它返回对象自身,因而被疏忽。别问我为什么,那是历史起因。所以咱们能够假如它基本就不存在。

让咱们实现一下这些办法。

例如,这里的 user 执行和后面提到的那个 user 一样的操作,应用 toStringvalueOf 的组合(而不是 Symbol.toPrimitive):

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 -> 1000alert(user + 500); // valueOf -> 1500

咱们能够看到,执行的动作和后面应用 Symbol.toPrimitive 的那个例子雷同。

通常咱们心愿有一个“全能”的中央来解决所有原始转换。在这种状况下,咱们能够只实现 toString,就像这样:

let user = {  name: "John",  toString() {    return this.name;  }};alert(user); // toString -> Johnalert(user + 500); // toString -> John500

如果没有 Symbol.toPrimitivevalueOftoString 将解决所有原始转换。

返回类型

对于所有原始转换方法,有一个重要的点须要晓得,就是它们不肯定会返回 "hint" 的原始值。

没有限度 toString() 是否返回字符串,或 Symbol.toPrimitive 办法是否为 hint "number" 返回数字。

惟一强制性的事件是:这些办法必须返回一个原始值,而不是对象。

历史起因

因为历史起因,如果 toStringvalueOf 返回一个对象,则不会呈现 error,然而这种值会被疏忽(就像这种办法基本不存在)。这是因为在 JavaScript 语言倒退初期,没有很好的 "error" 的概念。

相同,Symbol.toPrimitive 必须 返回一个原始值,否则就会呈现 error。

进一步的转换

咱们曾经晓得,许多运算符和函数执行类型转换,例如乘法 * 将操作数转换为数字。

如果咱们将对象作为参数传递,则会呈现两个阶段:

  1. 对象被转换为原始值(通过后面咱们形容的规定)。
  2. 如果生成的原始值的类型不正确,则持续进行转换。

例如:

let obj = {  // toString 在没有其余办法的状况下解决所有转换  toString() {    return "2";  }};alert(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。
  1. 乘法 obj * 2 首先将对象转换为原始值(字符串 "2")。
  2. 之后 "2" * 2 变为 2 * 2(字符串被转换为数字)。

二元加法在同样的状况下会将其连接成字符串,因为它更违心承受字符串:

let obj = {  toString() {    return "2";  }};alert(obj + 2); // 22("2" + 2)被转换为原始值字符串 => 级联

总结

对象到原始值的转换,是由许多冀望以原始值作为值的内建函数和运算符主动调用的。

这里有三种类型(hint):

  • "string"(对于 alert 和其余须要字符串的操作)
  • "number"(对于数学运算)
  • "default"(多数运算符)

标准明确形容了哪个运算符应用哪个 hint。很少有运算符“不晓得冀望什么”并应用 "default" hint。通常对于内建对象,"default" hint 的解决形式与 "number" 雷同,因而在实践中,最初两个 hint 经常合并在一起。

转换算法是:

  1. 调用 obj[Symbol.toPrimitive](hint) 如果这个办法存在,
  2. 否则,如果 hint 是 "string"

    • 尝试 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是 "number" 或者 "default"

    • 尝试 obj.valueOf()obj.toString(),无论哪个存在。

在实践中,为了便于进行日志记录或调试,对于所有可能返回一种“可读性好”的对象的表达形式的转换,只实现以 obj.toString() 作为全能转换的办法就够了。


古代 JavaScript 教程:开源的古代 JavaScript 从入门到进阶的优质教程。React 官网文档举荐,与 MDN 并列的 JavaScript 学习教程。

在线收费浏览:https://zh.javascript.info


微信扫描下方二维码,关注公众号「技术漫谈」,订阅更多精彩内容。