JavaScript 中将对象转换到基本类型值

翻译、演绎自:javascript.info/object-toprimitive

将两个对象在一起相加(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 个:

  1. 如果 obj[Symbol.toPrimitive](hint) 方法存在,就调用。
  2. 否则,如果 hint 是 "string"
    • 不论是否存在,尝试调用 obj.toString()obj.valueOf()
  3. 否则,如果 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 之前,对象转换依赖的是 toStringvalueOf 方法,它们会按照这样的顺序调用。

  • 对于 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 对象转换结果是 truetrue + 2 触发的是 ToNumber 转换,而不是字符串连接。true 转换成数字是 11 + 2 就是 3 了。

注意,toStringvalueOf 方法 *应该 * 返回一个基本类型值,但由于历史原因,如果返回了一个对象,也不会报错,而是会忽略这个方法(就像这个方法不存在一样)。

与此相反,Symbol.toPrimitive 必须 返回一个基本类型值,否则会报错。

总结

对象到基本类型值的转换是自动调用内置的几个方法实现的。一共有 3 种场景(hint):

  • "string"
  • "number"
  • "default"

在规范中明确定义了在每一个 hint 场景下,转换过程会调用的内置方法的顺序,其算法如下:

  1. 如果 obj[Symbol.toPrimitive](hint) 方法存在,就调用。
  2. 否则,如果 hint 是 "string"
    • 不论是否存在,尝试调用 obj.toString()obj.valueOf()
  3. 否则,如果 hint 是 number 或者 default
    • 不论是否存在,尝试调用 obj.valueOf()obj.toString()

在实践中,通常仅实现 obj.toString() 方法作为所有转换场景的唯一处理通道就足够了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!