JavaScript 对象转值的原理

1,053 阅读3分钟

JavaScript 作为一门弱类型的语言,在类型转换上非常的灵活。它在提供便利的同时,也带来了不少困惑。

[] + [] , [] + {}, `${obj}` ,这些表达式计算结果是什么,又是如何得出这个结果的?

参与数学计算的运算数必须是值类型,并且类型相同。在强类型的语言中,类型不同的值参与计算会导致编译错误。

对于 js 来说,它会在运行时「智能」的进行类型转换,得到一个使用者可能期望的结果。

但如果使用者对这个结果不满意,希望自定义,就需要了解运行时类型转换的原理。

类型转换会发生在值类型与值类型,值类型与对象类型之间。这里我们主要来探讨一下对象到值类型的转换过程。

求值的基础

JavaScript 中存在的六种值类型:Null, Undefined, Boolean, Number, String, Symbol。

既然是对象,就说明它存在,不能转为 Null 和 Undefined 类型。

同理,对象一定非空,转 Boolean 类型为 true

对象不会自动发生 Symbol 转换,除非使用强制类型转换函数 Symbol()

比较常见的情况是,对象转 Number 和 String。

根据 EMACScript 规范,对象转值(toPrimitive)类型会产生三种的 hint,分别是 numberstringdefault

我们可以根据 hint 来自定义转换的目标值。

number

显式地调用 Number() 函数,或者对象参与数学计算时:

let a = Number(obj); // NaN
let b = +obj; // NaN
let c = obj1 - obj2; // NaN

虽然默认的计算结果为 NaN ,但它属于 number 转换。

string

显式地调用 String() 函数,或者某些期望接受 String 类型的参数,当传入对象时,会发生对象到 string 的转换。

String(obj); // '[object Object]'
console.log( `${obj}` ); // [object Object]
anotherObj[obj] = 123; // { '[object Object]': 123 }

字符串模板接受字符串参数,传入非字符串类型,会发生隐式转换。

对象的属性名支持字符串和 Symbol 类型, 除这两种类型外,会默认转为字符串。

default

当不能确定某个操作期望得到什么类型时,例如二元运算符 + 的结果,可能是字符串也可能是数字。因此无法确定是把运算数转为 number 还是 string

这会产生一个 default 类型的 hint。

对象与 String, Number, Symbol 类型进行判等,也无法确定转换的目标类型。

// 产生 default 类型的 hint
obj1 + obj2;
obj == 1;

我们不必记忆哪些情况是 default 类型的转换,后面会讲到,它的转换处理流程和 number 相同。

特殊的是,比较操作符,例如: >< ,虽然能用于比较 Number 和 String 类型,但它们的 hint 是 number ,这是历史遗留问题。

类型转换的过程

在实现上,对象到值类型的转换,会分三个步骤:

1、判断对象是否存在一个属性名为 Symbol.toPrimitive 的函数,如果存在调用它,并传入以上三种之一的 hint 作为参数。如果这个函数返回值类型,就是对象转值的结果;如果返回非值类型,会抛出异常。如果不存在这个属性,执行下一步。

2、hint 类型如果是 string ,会依次调用对象的 toString()valueOf() 函数,直到某一个函数在该对象实现,并返回值类型为止。如果返回非值类型,会忽略这个方法。

3、hint 类型如果是 number 或者 default ,会依次调用对象的 valueOf()toString() ,直到某个函数在该对象实现,并返回值类型为止。如果返回非值类型,会忽略这个方法。

Symbol.toPrimitive

一个实现 Symbol.toPrimitive 属性的 user 对象:

const user = {
  name: 'Smallfly',
  age: '27',
  [Symbol.toPrimitive](hint) {
    console.log( `[${hint}]` );
    if (hint === 'string') {
      console.log('name: ' + this.name);
      return this.name;
    } else {
      console.log('age: ' + this.age);
      return this.age;
    }
  }
}

console.log(`${user}`); // [string] name: Smallfly -- Smallfly
console.log(+user); // [number] age: 27 -- 27
console.log(user + 1); // [default] age: 27 -- 27

user 传入字符串模板触发了 string 转换; + 一元运算符触发了 number 转换; + 二元运算符触发 default 转换。

Symbol.toPrimitive 函数包含了 user 转简单值类型的所有情况。

toString/ValueOf

如果对象没有实现 Symbol.toPrimitive 方法,会继续尝试调用 toStringvalueOf 方法。

const user = {
  name: 'Smallfly',
  age: '27',
  toString() {
    console.log('[toString]');
    console.log('name: ' + this.name);
    return this.name;
  },
  valueOf() {
    console.log('[valueOf]');
    console.log('age: ' + this.age);
    return this.age;
  }
}

console.log(`${user}`); // [toString] name: Smallfly
console.log(+user); // [valueOf] age: 27 -- 27
console.log(user + 1); // [valueOf] age: 27 -- 27

从结果上看, toString/valueOf 的组合功能和 Symbol.toPrimitive 完全一样。

Symbol.toPrimitive 是 ES6 引入的新功能,实现了对象转值类型方法的统一。

对象如果没有实现这两个方法,默认使用 Object 对象的 toString()/valueOf() 方法, toString() 方法返回 [object Object]valueOf() 方法返回对象自身。

const obj = {};
obj.toString(); // [object Object]
obj.valueOf() === obj; // true

[] + [] = ''

[] + [] 表达式的结果为空字符串,我们通过这个结果观察一下对象转值的具体过程。

const a = [];

a.toString = function() {
  return 'array string';
}

a.valueOf = function() {
  return 123;
}

console.log( `${a}` ); // array string
console.log(+a); // 123

以上代码证明空数组 [] 没有实现 Symbol.toPrimitive 方法,不然 toString/valueOf 方法不会被调用。

const a = [];

a[Symbol.toPrimitive] = function(hint) {
  console.log(hint);
  return 123;
}

console.log(a + a);
// default
// default
// 246

以上代码证明 [] 参与加法运算产生的 hint 为 default

根据前面提到的类型转换的规则,[] + [] 表达式触发 [] 转值类型会依次调用 valueOf()toString() 方法。

然而,[].valueOf() 返回值是空数组自身,并不是值类型,因此继续尝试调用 toString() 方法。

[].toString() 的结果是空字符串 '' 。因此,[] + [] 表达式的计算结果是 ''

参考链接