JavaScript 中将对象转换到基本类型值
将两个对象在一起相加(obj1 + obj2
)、相减(obj1 - obj2
)或者用 alert
弹出(alert(obj)
),会是什么结果呢?
在对象里,为了实现对象到基本类型值的转换,提供了几个特别的方法。
在 《JavaScript 类型转换》 一节,我们学习了将基本类型值转换成数字、字符串和布尔值的规则。现在我们就来讲将对象转换为基本类型值的规则。
对于对象运算,没有到布尔值的转换(所有对象转换为布尔值,都是 true
)—-最终对象不是转换成字符串,就是转换成数字。
对象转换成数字的情况发生在对象相减或者应用了数学函数的时候。拿 Date
举例,date1 - date2
就得到两个时间的(时间戳)差。
对象转换成字符串的情况发生在输出(alert(obj)
)或者其他类似的场景下。
ToPrimitive
当一个对象处于一个期望值是基本类型值的场景下,就会发生对象转换为基本类型值。这里会用到 ToPrimitive
算法(规格)。
这个算法允许我们自定义对象的转换规则,但这要依赖环境,用所谓的 hint
表示,它可能的取值有 3 个。
"string"
当操作的期望值是字符串的时候。这样的操作包括输出对象或者将对象作为属性使用。
let obj = {},
anotherObj = {};
// 输出操作
alert(obj); // [object Object]
// 将对象作为属性名使用
anotherObj[obj] = 123;
// 等同于
anotherObj['[object Object]'] = 123;
"number"
当操作的期望值是数字的时候。比如数学运算:
// 显式转换
let num = Number(obj);
// 数学运算(除了二元加号)
let n = +obj; // 一元加号
let delta = date1 - date2;
// 大于/小于比较
let greater = user1 > user2;
"default"
适应于运算结果“不确定”的操作中。
比如:二元加号既可以用来连接两个字符串,也可以用来把两个数字相加,二元加号也可以用在对象上。还有当一个对象使用 ==
与字符串、数字、布尔值或者 Symbol 值比较的时候。
// 二元加号
let total = car1 + car2;
// obj == string/number/boolean/symbol
if (user == 1) {}
大于/小于运算符 <>
也能作用在字符串和数字。但是由于历史原因,它是 "number"
hint 环境,而不是 "default"
。
在实践中,所有内置对象(除了 Date
)对于 "default"
hint 和 "number"
hint 的处理,都遵循同一套转换规则。
请注意,hint
可能的取值共 3 个,没有 "boolean"
hint(所有对象转换为布尔值都为 true
)。如果单就几乎所有内置对象对于 "default"
hint 和 "number"
hint 的处理遵循同一套规则来看的话,hint 相当于只有两种可能取值。
对象转换过程中,会尝试查找和调用的方法有 3 个:
- 如果
obj[Symbol.toPrimitive](hint)
方法存在,就调用。 - 否则,如果 hint 是
"string"
- 不论是否存在,尝试调用
obj.toString()
和obj.valueOf()
。
- 不论是否存在,尝试调用
- 否则,如果 hint 是
number
或者default
- 不论是否存在,尝试调用
obj.valueOf()
和obj.toString()
。
- 不论是否存在,尝试调用
Symbol.toPrimitive
JavaScript 有一个预先定义的 Symbol 值 Symbol.toPrimitive
,定义对象发生转换时调用的方法名。
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 -> 1000
alert(user + 500); // hint: default -> 1500
可以看到,当 user
对象发生转换时,user[Symbol.toPrimitive]
处理了所有可能的 3 种情况。
toString/valueOf
在 ES6 引入 Symbol 之前,对象转换依赖的是 toString
和 valueOf
方法,它们会按照这样的顺序调用。
- 对于 hint 等于
"string"
的情况:toString -> valueOf
。 - 否则(即 hint 等于
"number"
和"default"
的情况):valueOf -> toString
。
需要注意的是:如果先调用的方法返回了一个基本类型值,就不再调用后面的方法。例如,对于 hint 等于 "string"
的情况,如果先调用的 toString
方法返回了一个基本类型值,那么 valueOf
就不再被调用。
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 )1000
alert(user + 500); //(调用 valueOf)1500
如果我们不提供 valueOf
方法,那么 toString
方法就成为对象转换的唯一通道了。
let user = {
name: "John",
toString() {
return this.name;
}
};
alert(user); // (调用 toString)"John"
alert(user + 500); // (还是调用 toString)"John500"
ToPrimitive 和 ToString/ToNumber
值得注意的是,并不要求 toString()
必须返回一个字符串,针对 hint 是 "number"
的情况,Symbol.toPrimitive
方法必须返回一个数字。
唯一的强制要求是:toString()
和 Symbol.toPrimitive
必须返回一个基本类型值。
例如:
- 数学操作(除了两元
+
操作符)会触发ToNumber
转换:
let obj = {
toString() { // 在其他方法缺席的情况下,toString 处理所有情况下的转换
return "2";
}
};
console.log(obj * 2); // 4
上面的输出结果是 4,转换步骤如下:"2" * 2 -> 2 * 2 -> 4
。
- 当两元
+
操作符中的一个操作数是字符串,执行的是字符串连接运算;否则触发ToNumber
转换:
字符串连接运算:
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // "22"
obj
对象转换结果是 "2"
,"2" + 2
结果就是 "22"
了。
ToNumber
转换:
let obj = {
toString() {
return true;
}
};
alert(obj + 2); // 3
obj
对象转换结果是 true
,true + 2
触发的是 ToNumber
转换,而不是字符串连接。true
转换成数字是 1
,1 + 2
就是 3
了。
注意,toString
和 valueOf
方法 *应该 * 返回一个基本类型值,但由于历史原因,如果返回了一个对象,也不会报错,而是会忽略这个方法(就像这个方法不存在一样)。
与此相反,Symbol.toPrimitive
必须 返回一个基本类型值,否则会报错。
总结
对象到基本类型值的转换是自动调用内置的几个方法实现的。一共有 3 种场景(hint):
"string"
"number"
"default"
在规范中明确定义了在每一个 hint 场景下,转换过程会调用的内置方法的顺序,其算法如下:
- 如果
obj[Symbol.toPrimitive](hint)
方法存在,就调用。 - 否则,如果 hint 是
"string"
- 不论是否存在,尝试调用
obj.toString()
和obj.valueOf()
。
- 不论是否存在,尝试调用
- 否则,如果 hint 是
number
或者default
- 不论是否存在,尝试调用
obj.valueOf()
和obj.toString()
。
- 不论是否存在,尝试调用
在实践中,通常仅实现 obj.toString()
方法作为所有转换场景的唯一处理通道就足够了。
本作品采用《CC 协议》,转载必须注明作者和本文链接