需要的前置知识:四则运算相关,原始类型和对象类型相关。
js的数据类型分为两种,原始类型和对象类型。
原始类型存储的都是值,是没有函数可以调用的,比如 undefined.toString() 会抛出错误。
但为什么 '1'.toString() 是可以使用的。
其实是 . 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。在该临时对象调用函数返回函数操作结果后,将该对象丢弃。
装箱转换
每一种基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。
全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱。
var symbolObject = (function(){ return this; }).call(Symbol("a"));
console.log(typeof symbolObject); // object
console.log(symbolObject instanceof Symbol); // true
console.log(symbolObject.constructor Symbol); // true
或者使用内置的 Object 函数,也可以显式调用装箱能力。
var symbolObject = Object((Symbol("a"));
console.log(typeof symbolObject); // object
console.log(symbolObject instanceof Symbol); // true
console.log(symbolObject.constructor Symbol); // true
每一个原始类型装箱对象 console.dir 的时候,会发现有个 [[PrimitiveValue]] 标记,他会显示该对象内部指向的原始值。
原始类型在使用方法时会进行装箱转换,同样,对象在获取原始值时也会进行拆箱转换。
拆箱转换
js 是弱类型语言,所以在进行值加减乘除的操作的时候,会进行隐式的转换,然后进行运算。
类型转换中涉及到三种方法 toNumber, toString 和 toPrimitive 。
这里我们只说 toPrimitive ,也就是隐式转换里对象类型到原始类型的转换(即 拆箱转换 )。
ToPrimitive(input [, PreferredType])
1.如果没有传入PreferredType参数,则让hint的值为'default'
2.否则,如果PreferredType值为String,则让hint的值为'string'
3.否则,如果PreferredType值为Number,则让hint的值为'number'
4.如果input对象有@@toPrimitive方法,则让exoticToPrim的值为这个方法,否则让exoticToPrim的值为undefined
5.如果exoticToPrim的值不为undefined,则
a.让result的值为调用exoticToPrim后得到的值
b.如果result是原值,则返回
c.抛出TypeError错误
6.否则,如果hint的值为'default',则把hint的值重新赋为'number'
7.返回 OrdinaryToPrimitive(input,hint)
OrdinaryToPrimitive(input,hint)
1.如果hint的值为'string',则
a.调用input对象的toString()方法,如果值是原值则返回
b.否则,调用input对象的valueOf()方法,如果值是原值则返回
c.否则,抛出TypeError错误
2.如果hint的值为'number',则
a.调用input对象的valueOf()方法,如果值是原值则返回
b.否则,调用input对象的toString()方法,如果值是原值则返回
c.否则,抛出TypeError错误
上面一堆 ES 的文档说明可能看起来有点懵,不要慌,先来举几个栗子。
在对象拆箱转换的时候会调用 [@@toPrimitive]() 方法,该方法有个 PreferredType 参数,参数为 "string" 或 "number",如果不传,默认为 "default"。
PreferredType 的值会决定 OrdinaryToPrimitive 方法的 hint 参数。
如果 hint 是 "number",[@@toPrimitive]() 会首先尝试 valueOf,若值不是原始值再尝试 toString。如果 toString 也拿不到原始值,则抛出一个 TypeError。
对应 例子1 ,可以将 例子1 中的 valueOf 或者 toString 的 return 值改为原始值再试一下。
// 栗子1
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o * 2
// valueOf
// toString
// Uncaught TypeError: Cannot convert object to primitive value
// 这个先调用 valueOf , 然后调 toString , 然后报错
如果 hint 是 "string" 或 "default",[@@toPrimitive]() 将会调用 toString。如果 toString 原始值不存在,则调用 valueOf。如果 valueOf 也不存在,则抛出一个 TypeError。(例子2)
// 栗子2
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o + ""
// toString
// valueOf
// Uncaught TypeError: Cannot convert object to primitive value
// 这个先调用 toString, 然后调 valueOf, 然后报错
当在希望是字符串操作,也即发生对象到字符串的转换时,传入内部函数 ToPrimitive 的参数值即为 string,当在希望是数值操作,传入内部函数 ToPrimitive 的参数值即为 number,当在一些不确定需要将对象转换成什么基础类型的场景下,传入内部函数 ToPrimitive 的参数值即为 default。(例子3)
// 栗子3
const b = {
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`)
return {}
},
toString() {
console.log('toString')
return 1
},
valueOf() {
console.log('valueOf')
return 2
}
}
alert(b) // hint: string
b + '' // hint: default
b + 500 // hint: default
;+b // hint: number
b * 1 // hint: number
在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}
console.log(o + "")
// toPrimitive
// hello
// 来几个测试题
var o = { [Symbol.toStringTag]: 'MyObject' }
console.log(o + ''); // [object MyObject]
// + '' hint 为 string 调用 toString 方法, [Symbol.toStringTag] 改变了 [Object Object]
console.log( o * 2 ) // NaN
// hint 为 number, 调用 valueOf 方法, 返回对象本身, 再调用 toString方法. 返回字符串'[object MyObject ]'
// '[object MyObject ]' 转 Number 为 NaN, NaN * 2 还是 NaN
[] + []
// hint 为 number,所以先调用valueOf(),结果还是[ ],不是原始值,所以继续调用toString(),结果是“”原始值,将“”回。
// 加号两边结果都是String类型,所以进行字符串拼接,结果是“”。
[] + {}
// ?? 可以自己试下,结果是不是符合你的预期。