从206个console.log完全弄懂数据类型转换的前世今生下|博客系列
# 1. 重写toString和valueOf
看完了上篇的对象转字符串,不知道你对toPrimititve的转换流程掌握了多少呢?
如果你感觉之前的那些例子还不太具有说明性,也就是说你还是没有感觉到JS确实是按我画的那个流程图来进行转换的话,你可以看看这里。
我们在上篇的6.1中提到过了,大部分的对象都是可以通过原型链查找到Object.prototype上的toString或者valueOf方法,然后使用它们。
但是你想想,如果我这个对象本身就有toString或者valueOf方法的话是不是就可以不用Object.prototype上的了,这其实就是我们常听到的重写。
你也许可以用这样的方式来覆盖原型链上的这两个方法:
let obj = {
toString () {
return '1'
},
valueOf () {
return 1
}
}
甚至你还可以直接修改Object.prototype上的方法:
Object.prototype.toString = function () {
return 1
}
var b = {}
console.log(String(b)) // '1'
(当然,这种肯定是不推荐的哈,这会影响所有的对象)
# 1.1 题目一
(通过重写toString()和valueOf()来判断我们之前的toPrimitive流程是否正确)
上面👆两个例子我只是想告诉你,既然我们可以重写对象上的toString()和valueOf,那如果我们在重写的函数里面再加上console.log(xxx),不就可以知道对象转原始值的具体过程是不是按我们设想的方式执行下去了吗?
比如这样:
var b = {
toString () {
console.log('toString')
return 1
},
valueOf () {
console.log('valueOf')
return [1, 2]
},
}
console.log(Number(b))
想一下这里的执行结果 🤔️?
既然是用Number()方法来进行转换的话,那也就是执行了伪代码toPrimitive(obj, 'number')了。
那也就是说会先调用valueOf()函数,然后判断这个函数的返回值是否为原始值,再决定是继续调用toString()还是返回。
- 很显然,由于这里的
valueOf()被重写了,所以调用valueOf()之后返回的是一个引用类型[1, 2],所以它会继续执行toString() - 也就是执行
[1, 2].toString(),但是这时候的toString()也是被重写了的并且返回了数字1,所以我们根本没必要管[1, 2].toString()的结果了,而是直接将1返回。
所以整个过程结束之后,答案为:
'valueof'
'toString'
1
这样看下来流程就很清晰了,它确实是按照我们预期的方向走的。
# 1.2 题目二
如果你理解了上面一题的话,咱再来看看这里:
var b = {
toString () {
console.log('toString')
return { name: 'b' }
},
valueOf () {
console.log('valueOf')
return [1, 2]
},
}
console.log(String(b))
这次我是用的String()方法将b转为字符串。
而且要注意了,重写的toString()和valueOf都是返回的引用数据类型。那你可以想想到最后的结果会是什么吗?
来看看过程分析:
- 执行重写的
toString()方法,返回引用类型{name: 'b'} - 继续执行重写的
valueOf()方法,返回引用类型[1,2] - 哇,很难受,都经过两轮转换了还是引用类型,得抛错了。
没错,这里的转换过程最终是失败了的,因为还记得流程图中,最后一步了若还不是原始值的话,就会抛异常了。
所以结果为:
'toString'
'valueOf'
Cannot convert object to primitive value at String
# 1.3 题目三
(数组在进行ToString时的不同之处)
我们都知道,当数组在进行转字符串的时候,会把里面的每一项都转为字符串然后再进行","拼接返回。
那么为什么会有","拼接这一步呢?难道toString()在调用的时候还会调用join()方法吗?
为了验证我的想法💡,我做了一个实验,重写了一个数组的join()方法:
var arr = [1, 2]
arr['join'] = function () {
let target = [...this]
return target.join('~')
}
console.log(String(arr))
重写的join函数中,this表示的就是调用的这个数组arr。
然后将返回值改为"~"拼接,结果答案竟然是:
"1~2"
也就是说在String(arr)的过程中,它确实是隐式调用了join方法。
但是当我们重写了toString()之后,就不会管这个重写的join了:
var arr = [1, 2]
arr['toString'] = function () {
let target = [...this]
return target.join('*')
}
arr['join'] = function () {
let target = [...this]
return target.join('~')
}
console.log(String(arr)) // "1*2"
可以看出toString()的优先级还是比join()高的。
现在我们又可以得出一个结论:
对象如果是数组的话,当我们不重写其toString()方法,在转换为字符串类型的时候,默认实现就是将调用join()方法的返回值作为toString()的返回值。
# 2. Symbol.toPrimitive
在我正为自己弄懂了toPrimitive而感到骄傲的时候,我得知了一个叫做Symbol.toPrimitive的家伙。
看这家伙的样子,让我想起了以前见到过的一些老大哥:Symbol.hasInstance、Symbol.toStringTag。
他们都有着酷酷的纹身:Symbol,并且之前的老大哥是能够让我们做一些自定义的事情,不知道这家伙是不是和我想的一样,也能够帮助我们重写toPrimitive 🤔️?
了解了事情的真相之后,我知道了自己还是不笨的,给猜对了。
Symbol.toPrimitive就是比重写toString()和valueOf()更屌的一个属性。
如果你在一个对象里重写了它的话,那么甚至都不会执行重写的toString()和valueOf()了。
(Symbol.toPrimitive也被叫做@@toPrimitive)
# 2.1 题目一
(Symbol.toPrimitive的基本使用-返回值为一个原始值)
var b = {
toString () {
console.log('toString')
return { name: 'b' }
},
valueOf () {
console.log('valueOf')
return [1, 2]
},
[Symbol.toPrimitive] () {
console.log('symbol')
return '1'
}
}
console.log(String(b))
console.log(Number(b))
这道题中,我把刚刚提到的三个属性都给重写了,你感觉结果会是什么?
😄记住刚刚说的话,Symbol.toPrimitive的优先级是最高的,所以这里只会执行它里面的内容。
因此结果为:
'symbol'
'1'
'symbol'
1
并且大家可以看到,虽然Symbol.toPrimitive的返回值是"1",但是最终的结果String(b)还是字符串,Number(b)还是数字,表明,最后还是会给返回值做一层对应的转换的。
# 2.2 题目二
(Symbol.toPrimitive的返回值为引用类型,或者没有返回值?)
如果它的返回值是引用类型,或者干脆没有返回值,就会继续执行valueOf或者toString吗?
结果并不会…来看看这里,我定义了对象b和c,并且重写了这三个属性:
var b = {
toString () {
console.log('b.toString')
return { name: 'b' }
},
valueOf () {
console.log('b.valueOf')
return [1, 2]
},
[Symbol.toPrimitive] () {
console.log('b.symbol')
}
}
var c = {
toString () {
console.log('c.toString')
return { name: 'c' }
},
valueOf () {
console.log('c.valueOf')
return [1, 2]
},
[Symbol.toPrimitive] () {
console.log('c.symbol')
return [1, 2]
}
}
console.log(String(b))
console.log(String(c))
执行结果:
'b.symbol'
'undefined'
'c.symbol'
TypeError: Cannot convert object to primitive value
at String
过程分析:
String(b)过程中打印出了b.symbol,说明还是执行了Symbol.toPrimitive方法的,但是这个方法并没有返回值,且也没有继续执行valueOf()或者toString()了,而是返回了字符串"undefined"String(c)过程中也打印了c.symbol,但是Symbol.toPrimitive的返回值是一个对象,却报错了。
所以从这道题,我们可以看出:
Symbol.toPrimitive它可谓是一夫当关,万夫莫开,只要有它在,就不会继续往下走了,它的返回结果就是作为最终的返回结果。
而且通过String(c)我们可以看出来:如果返回的是一个对象的话,也不会继续执行valueOf()、toString()了,而是判断它的返回值,如果是原始值那就返回,否则就抛出错误。
# 2.3 题目三
(带参数的Symbol.toPrimitive)
你以为Symbol.toPrimitive仅仅是这么简单吗?
No😺,它竟然还能接收参数!!!
它接收一个字符串类型的参数:hint,表示要转换到的原始值的预期类型。
且参数的取值为以下字符串的其中一个:
"number""string""default"
嗯😺?我一惊,这怎么和之前介绍的toPrimitive那么像啊:
toPrimitive(obj, 'number')
toPrimitive(obj, 'string')
也就是说传入了之后,就是告诉Symbol.toPrimitive要转换成哪个类型咯?
这么屌的功能,赶紧来试试:
