当对象相加 obj1 + obj2,相减 obj1 - obj2,或者使用 alert(obj) 打印时会发生什么?
在这种情况下,对象会被自动转换为原始值,然后执行操作。
ToPrimitive 算法
JavaScript 对象转换到基本类型值时,会使用 ToPrimitive 算法,这是一个内部算法,是编程语言在内部执行时遵循的一套规则。
ToPrimitive 算法在执行时,会被传递一个参数 hint。(hint的中文意思提示)根据这个 hint 参数,ToPrimitive 算法来决定内部的执行逻辑。
hint
当然,hint 并不需要我们手动传入,而是JS根据上下文自动判断。hint 参数的只能 string,number,default三者之一。
"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 相同。
转换算法
当对象发生到基本类型值的转换时,会按照下面的逻辑调用对象上的方法:
- 调用
obj[Symbol.toPrimitive](hint)—— 带有 symbol 键Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是
"string"—— 尝试obj.toString()和obj.valueOf(),无论哪个存在。 - 否则,如果 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 常常合并在一起。
转换算法是:
- 调用
obj[Symbol.toPrimitive](hint)如果这个方法存在, - 否则,如果 hint 是
"string"- 尝试
obj.toString()和obj.valueOf(),无论哪个存在。
- 尝试
- 否则,如果 hint 是
"number"或者"default"- 尝试
obj.valueOf()和obj.toString(),无论哪个存在。
- 尝试
另外 valueOf返回的是对象本身,因此除非自定义了它的返回值,否则会被忽略。
在实践中,为了便于进行日志记录或调试,对于所有能够返回一种“可读性好”的对象的表达形式的转换,只实现以 obj.toString() 作为全能转换的方法就够了。