求值的基础
JavaScript 中存在的六种值类型:Null, Undefined, Boolean, Number, String, Symbol。
既然是对象,就说明它存在,不能转为 Null 和 Undefined 类型。
同理,对象一定非空,转 Boolean 类型为 true
对象不会自动发生 Symbol 转换,除非使用强制类型转换函数 Symbol()
比较常见的情况是,对象转 Number 和 String。
根据 EMACScript 规范,对象转值(toPrimitive)类型会产生三种的 hint,分别是 number 、 string 、 default
我们可以根据 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 方法,会继续尝试调用 toString 和 valueOf 方法。
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() 的结果是空字符串 ‘’ 。因此,[ ] + [ ] 表达式的计算结果是 ‘’。