美文网首页让前端飞
【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对象和其他对象也可以如此,这里就不再重复实现了。

相关文章

  • Javascript类型转换准则

    原始值转换: 对象转换为原始值 对象都会从Object继承valueOf 和toString 方法。默认的valu...

  • js对象的toString()方法和valueOf()方法

    在研究js的==和===的区别时,曾经说过,在js中非原始值对象,要参加运算需要ToPrimitive(x)转换成...

  • js 中的原始值和对象

    js中数据类型:原始值(null、 undefined、 布尔值、 数字 、字符串)与对象 原始值不可改变。对象可...

  • JavaScript权威指南(2)

    1、类型转换 原始值到对象的转换通过调用String()、Number()、Boolean()构造函数,转换为对应...

  • 基础复习笔记

    JS类型 原始类型存储的是值,对象类型存储的是地址(指针) JS原始(Primitive)类型 boolean n...

  • JS类型的转换

    类型 1.原始(值)类型 2.对象(引用)类型 3.原始类型和对象类型的区别 隐式的类型转换 显式的类型转换 类型...

  • 关于js的类型转换

    前言:js的类型转换真是容易让人一头雾水,接下来我将会好好整理一下。 原始值到原始值(数字,字符串,布尔值)的转换...

  • 原始类型与对象类型区别

    在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的...

  • 你理解js中map,set,array.from()吗

    1.Js中Map对象的使用(es6新增) Map对象保存键/值对,是键/值对的集合。任何值(对象或者原始值) 都可...

  • js的map对象实现对数组中某一项数据的分组

    1.Js中Map对象的使用(es6新增) Map对象保存键/值对,是键/值对的集合。任何值(对象或者原始值) 都可...

网友评论

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

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