让前端飞

【JS】对象的原始值转换

2023-10-16  本文已影响0人  来一斤BUG

在文章开始前,我们首先需要了解Symbol.toPrimitive是什么。
Symbol.toPrimitive是一种特殊的Symbol值,它可以作为对象的属性键,用于定义对象在被转换为原始值时的行为。当一个对象被转换为原始值时,JavaScript引擎会尝试调用对象上的Symbol.toPrimitive方法来确定转换的结果。比如对象{[Symbol.toPrimitive]: () => 1}转换成原始值就是1
需要注意的是,Symbol.toPrimitive必须为函数,不然会报错。

Symbol.toPrimitive方法中有一个参数,即转换的目标类型,可以是以下三个字符串之一:

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.toPrimitivevalueOf或者toString,都为true

// true
Boolean({[Symbol.toPrimitive]: () => false})

将对象转换成大整数(BigInt)

对象转换成BigInt的规则和转换成number的规则类似都是按照Symbol.toPrimitive -> valueOf -> toString的顺序,可以直接参考“将对象转换成数字”部分内容

// 111n
BigInt({
    [Symbol.toPrimitive]: () => "111",
    valueOf: () => "222",
    toString: () => "333",
})

用处

由于js没有其他语言中的操作符重载功能,我们只能利用原始值转换实现类似的功能,下面举几个例子:

  1. 比较时间

由于Date.prototype.valueOf()返回的是时间戳数字,于是我们可以直接通过关系运算符判断两个时间的先后。

const date1 = new Date(2023, 0, 1);
const date2 = new Date(2023, 0, 2);
console.log(date1 < date2); // true
  1. 金额计算

我们可以实现一个金额类,利用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元
  1. 将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对象和其他对象也可以如此,这里就不再重复实现了。

上一篇下一篇

猜你喜欢

热点阅读