对象原始值转换

摘要:

对象隐式类型转换规则:Symbol.toPrimitive、valueOf、toString

先看看下方的代码会怎样执行

js 复制代码
const obj1 = {}
const obj2 = {}
const compare = obj1 > obj2 || obj1 < obj2 || obj1 == obj2 // false
const compare = obj1 >= obj2 && obj1 <= obj2 // true
const num = +obj1 // NaN
const add = obj1 + obj2  // [object Object][object Object]
const minus = obj1 - obj2 // NaN
obj2[obj1] = ''; // {"[object Object]": ''}

为什么会出现上面的情况,js又是如何处理的呢?首先需要了解一个概念“hint”。

hint

类型转换在各种情况下有三种变体。它们被称为 “hint”

string

对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”

js 复制代码
// 输出
alert({});

// 将对象作为属性键
anotherObj[{}] = 123;

// 显示调用
({}).toString()
String({})

number

当我们进行数学运算时(二元加法除外)

js 复制代码
// 显式转换
const obj = {}
let num = Number(obj);

// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;

// 小于/大于的比较
let greater = user1 > user2;

default

当到底应该进行哪种转换不是很明确时。(比如二元加法,或者===比较),这些可以同时用于字符串和数字的操作。

js 复制代码
const obj1 = {}
const obj2 = {}
const total = obj1 + obj2
if(obj1 === 1) {}

<> 这样的小于/大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 “number” hint,而不是 “default”。这是历史原因。

上面这些规则看起来比较复杂,但在实践中其实挺简单的。

除了一种情况(Date 对象)之外,所有内建对象都以和 "number" 相同的方式实现 "default" 转换。我们也可以这样做。

了解了上面这些,我们就可以自主操纵转换行为了

自定义转换方法及优先级

1、调用 obj[Symbol.toPrimitive](hint) —— 带有 symbolSymbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话,
2、否则,如果 hint"string" —— 尝试调用 obj.toString()obj.valueOf(),无论哪个存在。
3、否则,如果 hint"number""default" —— 尝试调用 obj.valueOf() 或 obj.toString(),无论哪个存在。

Symbol.toPrimitive

有了此方法,toString和valueOf方法在隐式转换时就不会再被调用了。

js 复制代码
const user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    return hint == "string" ? JSON.stringify(this) : this.money;
  }
};
const a = String(user) // '{"name":"John","money":1000}'
const b = user - 1 // 999

toString/valueOf

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

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

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

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

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

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

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

这里提到的默认的 valueOf 只是为了完整起见,以避免混淆。正如你看到的,它返回对象本身,因此被忽略。别问我为什么,这是历史原因。所以我们可以假设它根本就不存在。

js 复制代码
const user = {
  name: "John",
  money: 1000,

  // 对于 hint="string"
  toString() {
    return JSON.stringify(this);
  },

  // 对于 hint="number" 或 "default"
  valueOf() {
    return this.money;
  }
};
const a = String(user) // '{"name":"John","money":1000}'
const b = user - 1 // 999

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

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

解释开头的代码

js 复制代码
const obj1 = {}
const obj2 = {}
// 这里使用比较运算符,那么hint的类型为number,会调用valueOf方法,
// 但valueOf返回的对象类型会被忽略,然后调用toString,返回 "[object Object]"
// 对象的是否相等比较就不转换了,直接比较对象的引用地址是否相同,只有对象和原始类型比较相等才会转换。
const compare = obj1 > obj2 || obj1 < obj2 || obj1 == obj2 // false
const compare = obj1 >= obj2 && obj1 <= obj2 // true
// 相当于 Number("[object Object]")
const num = +obj1 // NaN
// "[object Object]" + "[object Object]"
const add = obj1 + obj2  // [object Object][object Object]
// "[object Object]" - "[object Object]"
const minus = obj1 - obj2 // NaN
// obj2["[object Object]"] = ''
obj2[obj1] = ''; // {"[object Object]": ''}

一个恶趣味的面试题

实现变量a,满足console.log(a == 1 && a == 2 && a == 3) 结果为true。

js 复制代码
const a = {
  value: 0,
  [Symbol.toPrimitive]() {
    return ++this.value
  }
}
console.log(a == 1 && a == 2 && a == 3) // true

评论

0条评论

logo

暂无内容,去看看其他的吧~