【JS】对象的原始值转换
在文章开始前,我们首先需要了解Symbol.toPrimitive是什么。
Symbol.toPrimitive是一种特殊的Symbol值,它可以作为对象的属性键,用于定义对象在被转换为原始值时的行为。当一个对象被转换为原始值时,JavaScript引擎会尝试调用对象上的Symbol.toPrimitive方法来确定转换的结果。比如对象{[Symbol.toPrimitive]: () => 1}
转换成原始值就是1
。
需要注意的是,Symbol.toPrimitive
必须为函数,不然会报错。
Symbol.toPrimitive
方法中有一个参数,即转换的目标类型,可以是以下三个字符串之一:
- "number":表示将对象转换为数值类型。
- "string":表示将对象转换为字符串类型。
- "default":表示根据上下文中的要求进行转换,在隐式类型转换和默认转换类型的场景中使用。
举例:
const obj = {
value: 0,
[Symbol.toPrimitive](hint) {
switch (hint) {
case "number": {
return this.value;
}
case "string": {
return `value is ${this.value}`;
}
default: {
return this.value.toString();
}
}
},
};
console.log(Number(obj)); // 0
console.log(String(obj)); // value is 0
console.log(obj + 1); // 01
将对象转换成数字
将对象转换成数字时,首先会调用Symbol.toPrimitive
方法,如果Symbol.toPrimitive
不存在或者返回的不是js原始值(以下省略原始值这一条规则),则会调用valueOf
方法,如果valueOf
不存在,则会调用toString
方法,如果toString
也不存在,转换就会报错:TypeError: Cannot convert object to primitive value
,意思是无法将对象转换成原始值。
总的来说,调用顺序是:Symbol.toPrimitive -> valueOf -> toString
。
// js中可以使用+号将其他类型转换成number,和Number()的作用一样,为了表达式的简洁,以下将使用+代替Number()
// 0
+{[Symbol.toPrimitive]: () => 0}
// 1
+{valueOf: () => 1}
// 2
+{toString: () => 2} // 没错,toString方法可以返回number、boolean乃至其他类型
// 0
// 优先调用Symbol.toPrimitive,所以返回0
+{
[Symbol.toPrimitive]: () => 0,
valueOf: () => 1,
toString: () => 2,
}
// 0
// 如果返回的原始值不是`number`类型,则会再次进行转换;
// {[Symbol.toPrimitive]: () => "0"}转换成原始值为字符串"0";
// 接着再将这个字符串"0"转换成数字0。
+{[Symbol.toPrimitive]: () => "0"}
// NaN
// 空对象中不存在Symbol.toPrimitive方法,会调用valueOf方法;
// 对象的valueOf默认会返回自身,也就是说没有返回原始值,继续调用toString方法;
// 对象的toString方法默认会返回"[object " + 对象.constructor.name + "]",在这里将被转换成"[object Object]";
// 由于"[object Object]"属于原始类型,则js将其转换成number类型,当然它一眼看上去就不是个数字,只能转换成了NaN。
+{}
// 报错 TypeError: Cannot convert object to primitive value
// Object.create(null)创建的对象没有原型链,也就是没有valueOf和toString更没有Symbol.toPrimitive,所以只能转换失败了
+Object.create(null)
// 666
// parseInt和parseFloat如果传入对象,会先将对象转换成字符串,可以参考“将对象转换成字符串”部分内容
parseInt({
[Symbol.toPrimitive]: () => "666",
})
// 666
// Math对象中的方法如果传入了对象,先会将对象转换成number,然后才进行计算
Math.floor({
[Symbol.toPrimitive]: () => 666.6,
})
// 0
// null对象比较特殊,转换成number是0
+null
// NaN
// undefined对象比较特殊,转换成number是NaN
+void 0
将对象转换成字符串
一般情况下,我们会使用String()
或者xx.toString()
将对象转换为字符串,但是他们是有些区别的。String()
方法会优先尝试调用对象中的Symbol.toPrimitive
方法;如果Symbol.toPrimitive
不存在,则会尝试调用对象中的toString
方法。
用String()
转换对象成字符串的顺序为:Symbol.toPrimitive -> toString
,是的,不会调用valueOf
。
// "Hello, Symbol.toPrimitive!"
String({[Symbol.toPrimitive]: () => "Hello, Symbol.toPrimitive!"})
// "Hello, toString!"
String({toString: () => "Hello, toString!"})
// "Hello, Symbol.toPrimitive!"
String({
[Symbol.toPrimitive]: () => "Hello, Symbol.toPrimitive!",
toString: () => "Hello, toString!",
})
// "[object Object]"
// 空对象中不存在Symbol.toPrimitive方法,会调用toString方法;
// 对象的toString方法默认会返回"[object " + 对象.constructor.name + "]",在这里将被转换成"[object Object]";
String({})
// "[object Object]"
// 由于对象转换成字符串时不会调用valueOf,所以会调用默认的toString方法,可以参考String({})
String({
valueOf: () => "Hello, valueOf!",
})
// "Hello, Symbol.toPrimitive!"
// 使用模板字符串转换对象时,规则与String()相同,优先使用Symbol.toPrimitive
`${{[Symbol.toPrimitive]: () => "Hello, Symbol.toPrimitive!", toString: () => "Hello, toString!"}}`
// 报错 TypeError: Cannot convert object to primitive value
// Object.create(null)创建的对象没有原型链,也就是没有toString更没有Symbol.toPrimitive,所以只能转换失败了
// 顺便一提,`${Object.create(null)}`也会报这个错
String(Object.create(null))
// "{}"
// JSON.stringify会忽略对象中的方法,不受原始值转换规则的约束,所以这里的值为"{}"
JSON.stringify({
[Symbol.toPrimitive]: () => {
return "{a:1}";
},
})
// "null"
String(null)
// "undefined"
// undefined不属于对象,放在这里只是为了方便对比
// void 0实际上就是undefined
String(void 0)
将对象转换成布尔值
对象转换成布尔值的规则比较特殊,不论对象里面是否有Symbol.toPrimitive
、valueOf
或者toString
,都为true
// true
Boolean({[Symbol.toPrimitive]: () => false})
将对象转换成大整数(BigInt)
对象转换成BigInt的规则和转换成number的规则类似都是按照Symbol.toPrimitive -> valueOf -> toString
的顺序,可以直接参考“将对象转换成数字”部分内容
// 111n
BigInt({
[Symbol.toPrimitive]: () => "111",
valueOf: () => "222",
toString: () => "333",
})
用处
由于js没有其他语言中的操作符重载功能,我们只能利用原始值转换实现类似的功能,下面举几个例子:
- 比较时间
由于Date.prototype.valueOf()
返回的是时间戳数字,于是我们可以直接通过关系运算符判断两个时间的先后。
const date1 = new Date(2023, 0, 1);
const date2 = new Date(2023, 0, 2);
console.log(date1 < date2); // true
- 金额计算
我们可以实现一个金额类,利用js对象的原始值转换使代码更加简洁。
注意:下面的代码只是简化的写法,实际开发中需要更完善的代码
/**
* 金额类
*/
class Money {
/**
* 数量,单位为分,解决浮点数精度问题
* @type {number}
* @private
*/
_amount = 0;
/**
* 构造函数
* @param amount {number | Money} 金额,当传入 Money 类型时,会复制其金额
*/
constructor(amount = 0) {
if (amount instanceof Money) {
this._amount = +amount;
return;
}
this._amount = amount;
}
/**
* 金额相加
* @param money {Money} 金额
*/
add(money) {
return new Money(this + money);
}
/**
* 用于程序内部计算,返回金额,单位为分
* @return {number}
*/
valueOf() {
return this._amount;
}
/**
* 转换为字符串,保留两位小数,用于展示给用户
* @return {string}
*/
toString() {
return (this._amount / 100).toFixed(2);
}
}
const money1 = new Money(111);
const money2 = new Money(222);
/**
* 相加后的金额
* @type {Money}
*/
const addMoney = money1.add(money2);
console.log(`金额一为${money1}元`);
console.log(`金额二为${money2}元`);
console.log(`相加后的金额为${addMoney}元`);
下面是控制台中打印的数据:
金额一为1.11元
金额二为2.22元
相加后的金额为3.33元
- 将Set对象转换成字符串:
Set
是js内置的数据结构,某些情况下我们需要查看其内容,但是运行环境又不支持直接查看对象的内容时,我们需要转换成字符串。Set对象直接转换成字符串时会返回"[object Set]"
,这时候我们可以通过替换Set
原型上的函数实现将Set转换成字符串的功能。
Set.prototype.toString = function () {
return `Set(${this.size}) { ${[...this].join(", ")} }`;
};
Set.prototype[Symbol.toPrimitive] = function (hint) {
switch (hint) {
case "string": {
return this.toString();
}
default: {
return this.size;
}
}
};
const set = new Set([1, 2, 3]);
console.log(`${set}`);
console.log("两倍的set.size是", set * 2);
下面是控制台中打印的数据:
Set(3) { 1, 2, 3 }
两倍的set.size是 6
同理,Map对象和其他对象也可以如此,这里就不再重复实现了。