美文网首页让前端飞
【JS】对象的原始值转换

【JS】对象的原始值转换

作者: 来一斤BUG | 来源:发表于2023-10-16 14:16 被阅读0次

    在文章开始前,我们首先需要了解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.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对象和其他对象也可以如此,这里就不再重复实现了。

    相关文章

      网友评论

        本文标题:【JS】对象的原始值转换

        本文链接:https://www.haomeiwen.com/subject/uduxidtx.html