一文搞懂JS 对象转换 toPrimitive valueOf toString 究竟是什么以及调用顺序?

1,609 阅读5分钟

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

在这种情况下,对象会被自动转换为原始值,然后执行操作。

ToPrimitive 算法

JavaScript 对象转换到基本类型值时,会使用 ToPrimitive 算法,这是一个内部算法,是编程语言在内部执行时遵循的一套规则。

ToPrimitive 算法在执行时,会被传递一个参数 hint。(hint的中文意思提示)根据这个 hint 参数,ToPrimitive 算法来决定内部的执行逻辑。

hint

当然,hint 并不需要我们手动传入,而是JS根据上下文自动判断。hint 参数的只能 stringnumberdefault三者之一。

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

// 二元加法使用默认 hint 
let total = obj1 + obj2;
// obj == number 使用默认 
hint if (user == 1) { ... };

其实除了Date对象外,所有内建对象的 default转换的实现方式都和 number 相同。

转换算法

当对象发生到基本类型值的转换时,会按照下面的逻辑调用对象上的方法:

  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.Primitive

当我们创建一个普通对象时({}new Object() 的方式等),对象上是不具备 [Symbol.toPrimitive] (方法)属性的。

不过,我们可以手动为其添加 [Symbol.toPrimitive]。根据ECMAScript规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。”

const user = {
	name: 'john',
	money: 1000,
	[Symbol.toPrimitive](hint){
		console.log(hint);
		//如果hint是string,则返回名字,如果是数字或者是default则返回钱量
		return hint === 'string' ? this.name: this.money;
	}
}

接着让我们来测试一下:

对象 + xx 是二元加法,所以 hint 是default,因此会返回this.money

上面的例子如果比较困惑的,那就让我们换一个更简单一点的。

接着,让我们将 user 作为 新对象 test 的属性名,我们知道对象的键名只能为字符串。因此这里的 hint 为string, ToPrimitive 返回 this.name

toString/valueOf

前面提到,通过 {}new Object() 创建的对象它们都没有 [Symbol.toPrimitive]

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

  • 对于 “string” hint,toString -> valueOf
  • 其他情况(即 number 或者 default hint),valueOf -> toString
let user = {name: "John"};
alert(user); //[object object] hint 为 string

user.toString(); //[object object]

我们可以看到 valueOf 的返回值是对象本身,因此它直接被忽略了。所以我们可以假设它根本就不存在。不信我们可以试试。

我们知道 == 时,hint是 default,如果是default的换,会先调用 valueOf方法。但是它被忽略了,不然的话 obj 的 valueof()方法的值还是obj,是一个对象,是不可能等于 [object Object]这个字符串的。

唯一的可能就是 valueOf()直接被忽略了,然后调用了 obj 的 toString()方法。

我觉得 我们本来就是想要将对象转换为原始值的,结果 valueOf() 直接给返回了对象本身,而我们要的是原始值啊,所以它注定是要被忽略的。


当然它也可以不被忽略,那就是我们可以自己定义 valueOf() 方法,也可以自己定义 toString() 方法。

let user = {
 name: 'John',
 money: 1000,
 
 // hint 为string
 toString(){
 	return `name:${this.name}`
 },
 // hint 为 number 或者 default
 valueOf(){
 	return this.money;
 }
}

接着让我们测试一波:

自定义 toString() 后,终于不再弹出[object Object]了!

另外,+user的hint是 number,所以此时调用的是我们自定义的 valueOf()方法,返回了 1000 。

总结

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

这里有三种类型(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(),无论哪个存在。

另外 valueOf返回的是对象本身,因此除非自定义了它的返回值,否则会被忽略。

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

本文以zh.javascript.info/object-topr… 为依据