目录

⏳ JavaScript 对象与原始值的转换

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

在此类运算的情况下,对象会被自动转换为原始值,然后对这些原始值进行运算,并得到运算结果(也是一个原始值)。(⭐️ 重要限制:对象运算的结果不能是另一个对象)

  • 例如,无法使用对象来表示向量或矩阵(或成就或其他),把它们相加并期望得到一个 “总和” 向量作为结果。
  • 从技术上无法实现此类运算,所以在实际项目中不存在对对象的数学运算。

# 转换规则

  • 对于布尔值类型。对象没有转换为布尔值,所有的对象在布尔上下文(context)中均为 true
  • 数字转换发生在对象相减或应用数学函数时。例如, Date 对象可以相减, date1 - date2 的结果是两个日期之间的差值。
  • 对于字符串类型。通常为 alert(obj) 这样输出一个对象和类似的上下文中。

# hint

JavaScript 对象的类型转换在各种情况下有三种变体(规范 (opens new window) ):

  • "string" :对象到字符串的转换,当对期望一个字符串的对象执行操作时,例如 alert

    // 输出
    alert(obj);
    
    // 将对象作为属性键
    anotherObj[obj] = 123;
    
    1
    2
    3
    4
    5
  • "number" :对象到数字的转换,例如进行数学运算时:

    // 显式转换
    let num = Number(obj);
    
    // 数学运算(除了二元加法)
    let n = +obj; // 一元加法
    let delta = date1 - date2;
    
    // 小于/大于的比较
    let greater = user1 > user2;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  • “default” :在少数情况下发生,当运算符 **「不确定」期望值的类型 ** 时。(有默认 hint)

    // 二元加法使用默认 hint
    let total = obj1 + obj2;
    
    // obj == number 使用默认 hint
    if (user == 1) { ... };
    
    1
    2
    3
    4
    5
    • 二元加法 + 可用于字符串(连接),也可以用于数字(相加)。因此,当二元加法得到对象类型的参数时,它将依据 "default" hint 来对其进行转换。
    • 如果对象被用于与字符串、数字或 symbol 进行 == 比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default" hint。
    • <> 这样的小于 / 大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 “number” hint,而不是 “default” 。这是历史原因。

以上的 hint 规则出了一种情况以外,所有内建对象都以和 "number" 相同的方式实现 "default" 转换。

为了进行转换,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,它被用来给转换方法命名:

obj[Symbol.toPrimitive] = function(hint) {
  // 这里是将此对象转换为原始值的代码
  // 它必须返回一个原始值
  // hint = "string"、"number" 或 "default" 中的一个
}
1
2
3
4
5

如果 Symbol.toPrimitive 方法存在,则它会被用于所有 hint,无需更多其他方法。

🌰 user 对象 实现 Symbol.toPrimitive

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 -> 1000
alert(user + 500); // hint: default -> 1500
1
2
3
4
5
6
7
8
9
10
11
12
13

从中,根据转换的不同, user 变成一个自描述字符串或者一个金额。 user[Symbol.toPrimitive] 方法处理了所有的转换情况。

# toString/valueOf

如果没有 Symbol.toPrimitive ,那么 JavaScript 将尝试寻找 toStringvalueOf 方法:

  • 对于 "string" hint:调用 toString 方法,如果它不存在,则调用 valueOf 方法(因此,对于字符串转换,优先调用 toString )。
  • 对于其他 hint:调用 valueOf 方法,如果它不存在,则调用 toString 方法(因此,对于数学运算,优先调用 valueOf 方法)。

toStringvalueOf 方法很早己有了。它们不是 symbol(那时候还没有 symbol 这个概念),而是「常规的」字符串命名的方法。它们提供了一种可选的 “老派” 的实现转换的方法。

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

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

  • toString 方法返回一个字符串 "[object Object]"
  • valueOf 方法返回对象自身。(忽略)

🌰 例子:

let user = {name: 'Simon'}

alert(user) // [object Oject]
alert(user.valueOf() === user); // true
1
2
3
4

实现这些方法自定义转换:

🌰 例子:

let user = {
  name: 'Simon',
  money: 1000,
  
  toString() {
    return `{name: "${this.name}"`;
  },
  valueOf() {
    return this.money
  }
}

alert(user) // {name: "Simon"}
alert(+user.valueOf()) // 1000
alert(user + 500) // 1500
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以看到执行的动作和前面使用 Symbol.toPrimitive 的那个例子相同。

通常希望有一个「全能」的地方来处理所有原始转换。在这种情况下,可以只实现 toString ,例如:

let user = {
  name: 'Simon',
  toString() {
    return this.name
  }
}

alert(user) // toString -> Simon
alert(user + 500) // toString -> Simon500
1
2
3
4
5
6
7
8
9

如果没有 Symbol.toPrimitivevalueOftoString 将处理所有原始转换。

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

⭐️ 所有的原始转换方法,不一定会返回 “hint” 的原始值。

  • 没有限制 toString() 是否返回字符串,或 Symbol.toPrimitive 方法是否为 "number" hint 返回数字。
  • 唯一强制性的事情是:这些方法必须返回一个原始值,而不是对象。

由于历史原因,如果 toStringvalueOf 返回一个对象,则不会出现 error,但是这种值会被忽略(就像这种方法根本不存在)。这是因为在 JavaScript 语言发展初期,没有很好的 「error」 的概念。

相反, Symbol.toPrimitive 更严格,它 必须 返回一个原始值,否则就会出现 error。

# 进一步的转换

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

如果将对象作为参数传递,则会出现两个运算阶段:

  1. 对象被转换为原始值(通过前面我们描述的规则)。
  2. 如果还需要进一步计算,则生成的原始值会被进一步转换。

🌰 例子:

let obj = {
  toString() {
    return '2'
  }
}

alert(obj * 2) // 4
1
2
3
4
5
6
7
  • 乘法 obj * 2 首先将对象转换为原始值(字符串 “2”)。
  • 之后 "2" * 2 变为 2 * 2 (字符串被转换为数字)。

🌰 例子:⚠️ 对于二元加法会将其连接为字符串,因为它更愿意接受字符串。

let obj = {
  toString() {
    return "2";
  }
};

alert(obj + 2); // 22
1
2
3
4
5
6
7

# 对象转换总结

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

  • Symbol.primitive / toStringvalueOf 是内建的对象方法,可以重新实现。

  • 三种 “hint”:

    • "string" (对于 alert 和其他需要字符串的操作)
    • "number" (对于数学运算)
    • "default" (少数运算符,通常对象以和 "number" 相同的方式实现 "default" 转换)
  • 转换规则:

    • 调用 obj[Symbol.toPrimitive](hint) 如果这个方法存在;
    • 否则,如果 hint 是 "string"
      • 尝试调用 obj.toString()obj.valueOf() ,无论哪个存在。
    • 否则,如果 hint 是 "number" 或者 "default"
      • 尝试调用 obj.valueOf()obj.toString() ,无论哪个存在。
    • 所有这些方法都必须返回一个原始值才能工作(如果已定义)。

实际使用中,通常只实现 toString() 就足够了。一般返回对象的「可读表示」,用于调试或者日记。

🤯 后知后觉,这不就是 Java 的类中,可以重写 toString() 方法一个道理吗。如果不写 toString() 方法打印整个类(的实例对象)就只输出类名和类的地址(?) ,而重写了 toString() 方法就可以指定打印的内容。

📢 上次更新: 2022/09/02, 10:18:16