你盼世界,我盼望你无bug
。Hello 你们好!我是霖呆呆!javascript
那年我十八岁,单纯,善良,懵懂,青涩,阳光,可爱...前端
如今的我...在面对JS
类型转换的时候,依旧是...vue
我觉得了解了toString()
和valueOf()
以后,我就是那个最懂你的男人...java
直到我在你的内心看到一个叫作Symbol.toPrimitive
的人...面试
这我的,他掌握着你转换的核心,甚至在必要的时候可以彻底替代toString()
和valueOf()
。segmentfault
我奔溃了...发疯似得去找谷哥和度娘,求他们告诉我打败Symbol.toPrimitive
的法门,最后,他们只说了一句话:数组
"看完霖呆呆的这篇文章再来一波三连就能够了啊!!!"
浏览器
😂😂😂函数
抱歉,狗改不了吃屎,呸,秉性难移顽梗不化,忍不住写了个小短片哈 😄。post
其实也是想要告诉你们,上一篇文章是重点,这篇文章也是重点,因此都要好好看哦。
有不少人以为花了这么多的时间和这么多的精力来看呆呆的这两篇类型转换,就只为了弄懂这一个小小的知识点,感受好亏啊,远没有刷一篇各个知识点都覆盖的面试总结的成就感高。其实我想说,面试总结类的文章当然能够帮助咱们查漏补缺,可是对于JS
基础知识的掌握我认为也是十分重要的。就像今天看到Minute老哥的一篇《非科班二本前端大厂面试的心路历程和总结(腾讯、头条、阿里、京东) | 掘金技术征文》文章里说的同样:
OK👌,玩归玩,闹归闹,类型转换把你教。
来看看经过阅读你能够学习到:
toString
和valueOf
Symbol.toPrimitive
==
比较时的类型转换+、-、*、/、%
的类型转换在正式阅读以前,我推荐你看一下本系列的上一篇《从206个console.log()彻底弄懂数据类型转换的前世此生(上)》;这样有利于你阅读本篇文章。
让咱们来回顾一下以前提到的toPrimitive
执行流程:
看完了上篇的对象转字符串,不知道你对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'
复制代码
(固然,这种确定是不推荐的哈,这会影响全部的对象)
(经过重写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
复制代码
这样看下来流程就很清晰了,它确实是按照咱们预期的方向走的。
若是你理解了上面一题的话,咱再来看看这里:
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
复制代码
精彩精彩👏,我仿佛已经看到了彻底弄懂对象转换原始值机制的曙光!!!
(数组在进行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()
的返回值。
在我正为本身弄懂了toPrimitive
而感到骄傲的时候,我得知了一个叫作Symbol.toPrimitive
的家伙。
看这家伙的样子,让我想起了之前见到过的一些老大哥:Symbol.hasInstance
、Symbol.toStringTag
。
他们都有着酷酷的纹身:Symbol
,而且以前的老大哥是可以让咱们作一些自定义的事情,不知道这家伙是否是和我想的同样,也可以帮助咱们重写toPrimitive
🤔️?
了解了事情的真相以后,我知道了本身仍是不笨的,给猜对了。
Symbol.toPrimitive
就是比重写toString()
和valueOf()
更屌的一个属性。
若是你在一个对象里重写了它的话,那么甚至都不会执行重写的toString()
和valueOf()
了。
(Symbol.toPrimitive
也被叫作@@toPrimitive
)
(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)
仍是数字,代表,最后仍是会给返回值作一层对应的转换的。
(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()
了,而是判断它的返回值,若是是原始值那就返回,不然就抛出错误。
(带参数的Symbol.toPrimitive
)
你觉得Symbol.toPrimitive
仅仅是这么简单吗?
No😺,它居然还能接收参数!!!
它接收一个字符串类型的参数:hint
,表示要转换到的原始值的预期类型。
且参数的取值为如下字符串的其中一个:
"number"
"string"
"default"
嗯😺?霖呆呆我一惊,这怎么和以前介绍的toPrimitive
那么像啊:
toPrimitive(obj, 'number')
toPrimitive(obj, 'string')
复制代码
也就是说传入了以后,就是告诉Symbol.toPrimitive
要转换成哪一个类型咯?
这么屌的功能,赶忙来试试:
var b = {
toString () {
console.log('toString')
return '1'
},
valueOf () {
console.log('valueOf')
return [1, 2]
},
[Symbol.toPrimitive] (hint) {
console.log('symbol')
if (hint === 'string') {
console.log('string')
return '1'
}
if (hint === 'number') {
console.log('number')
return 1
}
if (hint === 'default') {
console.log('default')
return 'default'
}
}
}
console.log(String(b))
console.log(Number(b))
复制代码
这道题重写了toString、valueOf、Symbol.toPrimitive
三个属性,经过上面👆的题目咱们已经知道了只要有Symbol.toPrimitive
在,前面两个属性就被忽略了,因此咱们不用管它们。
而对于Symbol.toPrimitive
,我将三种hint
的状况都写上了,若是按照个人设想的话,在调用String(b)
的时候应该是要打印出string
的,调用Number(b)
打印出number
,结果也正如我所预想的同样:
'string'
'1'
'number'
1
复制代码
那么这里面的"default"
是作什么的呀?它是何时执行的呢?
开始个人想法是若是没有if (hint === 'string')
这一个判断的时候,是否是就会执行"default"
了呢?
因而我把if (hint === 'string')
和'number'
这两个判断的内容给去掉了,发现它仍是不会执行"default"
:
var b = {
toString () {
console.log('toString')
return '1'
},
valueOf () {
console.log('valueOf')
return [1, 2]
},
[Symbol.toPrimitive] (hint) {
console.log('symbol')
// if (hint === 'string') {
// console.log('string')
// return '1'
// }
// if (hint === 'number') {
// console.log('number')
// return 1
// }
if (hint === 'default') {
console.log('default')
return 'default'
}
}
}
console.log(String(b))
console.log(Number(b))
// 'symbol'
// 'undefined'
// 'symbol'
// NaN
复制代码
能够看到,执行结果居然和题2.2
中那个没有返回值的b
有点像。
因此也就是说,这个hint
它是在调用Symbol.toPrimitive
的时候就已经肯定了的,后面并不会改变。
好比String(b)
时传的是string
,Number(b)
时传的是number
。
而default
这个状况,它涉及到+
运算符,在第四节中会说到。
小伙子(姑娘),据说你已经掌握Symbol.toPrimitive
了?
OK👌,让咱们来作个题巩固一下:
class Person {
constructor (name) {
this.name = name
}
[Symbol.toPrimitive] (hint) {
if (hint === 'default') {
console.log('default')
return 'default'
}
if (hint === 'string') {
console.log('string')
return '1'
}
if (hint === 'number') {
console.log('number')
return 1
}
}
}
let p1 = new Person('p1');
let p2 = new Person('p2');
console.log(String(p1))
console.log(Number(p2))
console.log(p1)
console.log(p2)
复制代码
我把原来的对象,换成了如今的class
,你不用想多,其实用它生成的实例就是一个对象,且能使用Symbol.toPrimitive
。
因此这里的结果为:
'string'
'1'
'number'
1
Person{ name: 'p1' }
Person{ name: 'p2' }
复制代码
注意:这里的p一、p2
为何是没有表现出Symbol.toPrimitive
函数的呢?
别忘了《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》这里说的,定义在class
中的全部方法都至关因而定义在其原型对象上,也就是Person.prototype
上,因此这里p一、p2
虽然是遵循Symbol.toPrimitive
,可是使用的倒是它原型链上的。
咱来总结一下哈。
toString、valueOf、Symbol.toPrimitive
方法,Symbol.toPrimitive
的优先级是最高的Symbol.toPrimitive
函数返回的值不是基础数据类型(也就是原始值),就会报错Symbol.toPrimitive
接收一个字符串参数hint
,它表示要转换到的原始值的预期类型,一共有'number'、'string'、'default'
三种选项String()
调用时,hint
为'string'
;使用Number()
时,hint
为'number'
hint
参数的值从开始调用的时候就已经肯定了说实话,这回是真的有些膨胀了,如今无论是toPrimitive
的执行机制,仍是Symbol.toPrimitive
的自定义咱都给搞懂了。
上面👆整了这么多题,你却是给👴来点实际会考的东西啊。
好哦,其实在实际中咱们被考的比较多的可能就是用==
来比较判断两个不一样类型的变量是否相等。
而全等===
的状况比较简单,通常不太会考,由于全等的条件就是:若是类型相等值也相等才认为是全等,并不会涉及到类型转换。
可是==
的状况就相对复杂了,先给你们看几个比较眼熟的题哈:
console.log([] == ![]) // true
console.log({} == true) // false
console.log({} == "[object Object]") // true
复制代码
怎样?这几题是否是常常看到呀 😁,下面就让咱们一个一个来看。
首先,咱们仍是得清楚几个概念,这个是硬性规定的,不看的话咱无法继续下去啊。
当使用==
进行比较的时候,会有如下转换规则(判断规则):
2 == 3
确定是为false
的了null、undefined
,则另外一方必须为null或者undefined
才为true
,也就是null == undefined
为true
或者null == null
为true
,由于undefined
派生于null
String
,是的话则把String
转为Number
再来比较Boolean
,是的话则将Boolean
转为Number
再来比较ToNumber
的转换形式来进行比较(实际上它的hint
是default
,也就是toPrimitive(obj, 'default')
,可是default
的转换规则和number
很像,具体能够看3.10
)在一些文章中,会说道:
若是其中一方为Object,且另外一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
(摘自《神三元-(建议收藏)原生JS灵魂之问, 请问你能接得住几个?(上)》中的3. == 和 ===有什么区别?
)
这样认为其实也能够,由于想一想toPrimitive(obj, 'number')
的过程:
valueOf()
方法valueOf()
方法的返回值是基本数据类型则直接返回,若不是则继续调用toString()
toString()
的返回值是基本数据类型则返回,不然报错。能够看到,首先是会执行valueOf()
的,可是引用类型执行valueOf()
方法,除了日期类型,其它状况都是返回它自己,也就是说执行完valueOf()
以后,仍是一个引用类型而且是它自己。那么咱们是否是就能够将valueOf()
这一步给省略掉,认为它是直接执行toString()
的,这样作起题来也快了不少。
(虽然能够将它省略,可是你得知道实际是有这么一步的,这一点咱们在题目3.6
会验证)
为了方便记忆,我画了一张后面三个规则的转换图,接下来咱们只须要按着这张图的转换规则来作题就能够了 😁。
(理解类型相同
、null、undefined
的状况)
来点简单的吧
console.log(1 == 1)
console.log(1 == 2)
console.log(null == 0)
console.log(null == false)
console.log(null == {})
console.log(undefined == 0)
console.log(undefined == false)
console.log(undefined == {})
console.log(null == null)
console.log(undefined == undefined)
console.log(undefined == null)
复制代码
谨记开头的转换规则来作题哦 😁。
因此这里的答案为:
console.log(1 == 1) // true
console.log(1 == 2) // false
console.log(null == 0) // false
console.log(null == false) // false
console.log(null == {}) // false
console.log(undefined == 0) // false
console.log(undefined == false) // false
console.log(undefined == {}) // false
console.log(null == null) // true
console.log(undefined == undefined) // true
console.log(undefined == null) // true
复制代码
能够看到,undefined、null
除了和它自身以及对方相等以外,和其它的比较都为false
。
(其实以前我老是觉得null == 0
或者null == false
是为true
的,由于以前可能会使用!flag
这种方式来判断某个值是否是truly
,固然越到后面越知道这种方式实际上是很不严谨的哈)
(理解一方为String
,另外一方为Number
的状况)
如果这种状况的话,会把String
转成Number
再来比较:
console.log('11' == 11)
console.log('1a' == 11)
console.log('11n' == 11)
console.log('0x11' == 17)
console.log('false' == 0)
console.log('NaN' == NaN)
复制代码
这里可能会有几个陷阱,你们要当心了。
答案:
console.log('11' == 11) // true
console.log('1a' == 11) // false
console.log('11n' == 11) // false
console.log('0x11' == 17) // true
console.log('false' == 0) // false
console.log('NaN' == NaN) // false
复制代码
'11' == 11
没啥问题,字符串转为了数字'1a'
转为数字以后是NaN
'11n'
转为数字以后也是NaN
,可能你们会当作是bigInt
类型的,可是注意了这里是字符串'0x11'
,以0x
开头的十六进制,因此转换为数字以后是17
'false'
是一个字符串哦,并非false
,因此结果是假值'NaN'
也是字符串,不过这里要是真的NaN
的话,那也是false
,由于NaN
这个六亲不认的连它本身都不全等(也就是NaN===NaN
的结果为false
),只有用Object.is(NaN, NaN)
才会被判断为true
)(理解一方为Boolean
的状况)
这种状况会将Boolean
转为Number
来比较,而经过上篇咱们知道,Boolean
转Number
那是至关简单的,只有两种状况:
因此若是有一方为Boolean
的时候应该会很好作吧...
console.log(true == 1)
console.log(false == 0)
console.log(true == '1')
console.log(false == '0')
console.log(true == '0')
console.log(true == 'false')
console.log(false == null)
复制代码
是挺简单的哈:
console.log(true == 1) // true
console.log(false == 0) // true
console.log(true == '1') // true
console.log(false == '0') // true
console.log(true == '0') // false
console.log(true == 'false') // false
console.log(false == null) // false
复制代码
true
和false
转为数字就是 0
和1
true
转为数字为1
,以后另外一边是字符串1
,依靠准则三,一方为字符串,则将这个字符串转为数字而后进行比较,因此结果为1 == 1
的结果,也就是true
false
转为数字0
,以后后面的"0"
也被转为数字0
,因此结果为true
true
被转换为了1
,'0'
被转换为了0
,因此结果为false
true
被转换为了1
,"false"
被转换为了NaN
,因此结果为false
null
和false
自己就是不相等的。其实这里不知道有没有和我同样对true == '0'
有疑问的呢 🤔️?
由于咱们可能见过这么一段代码:
if ('0') {
console.log('我会被执行')
}
复制代码
这里if
内的内容是会被执行的,由于字符串'0'
转换为布尔确实是true
,那么我就总会认为true == '0'
是对的。
因此这里要注意了,'0'
确实是会被转换为true
,也就是:
if (true) {
console.log('我会被执行')
}
复制代码
但在这道题中是将它与true
来作比较,那么就要遵循「有布尔先将布尔转换为数字」的规则。
因此其实也就是一个转换顺序的问题,true == '0'
是先执行的布尔转数字的。
可是你不要觉得是一个写法顺序的问题 😂,也就是说就算把true
和'0'
换个位置结果也是同样的:
console.log('0' == true) // false
复制代码
(一方为对象的状况)
在第三节的开头那里呆呆已经说了,当一方有为对象的时候,实际是会将对象执行相似ToNumber
操做以后再进行比较的,可是又因为对象的valueOf()
基本都是它自己,因此咱们能够认为省略了这一步,不过为了让你们心服口服,我这里仍是得来验证一下:
var b = {
valueOf: function () {
console.log('b.valueOf')
return '1'
},
toString: function () {
console.log('b.toString')
return '2'
}
}
var c = {
valueOf: function () {
console.log('c.valueOf')
return {}
},
toString: function () {
console.log('c.toString')
return '2'
}
}
console.log(b == 1)
console.log(c == 2)
复制代码
这道题中,b
的valueOf()
返回的是一个基本数据类型
c
的valueOf()
返回的是一个引用类型。
所以结果为:
'b.valueOf'
true
'c.valueOf'
'c.toString'
true
复制代码
因此咱们能够获得这张图:
下面作两道题让咱们练习一下哈。
(一方为非数组对象的状况)
console.log({} == true)
console.log({} == false)
console.log({} == 1)
console.log({} == '1')
console.log({} == 0)
console.log({} == Symbol(1))
console.log({} == null)
console.log({} == {})
复制代码
哇,乍一看感受好多啊,这...我怎么比的过来。
这时候你只要记得,有一方是Object
时,把这个Object
转为字符串再来比较就能够了。
而引用类型转字符串不知道你们还记得吗?
分为了数组
和非数组
两种状况,大体就是:
[] => ''
,['1, 2'] => '1, 2'
而后咱们再来看看上面那道题👆,{}
转为字符串实际上是"[object Object]"
因此能够看出上面的执行结果全为false
。
其中可能比较难理解的是:
{} == true
,转换过程为:{} == true
"[object Object]" == true // 对象转字符串
"[object Object]" == 1 // 布尔值转数字(准则四,一方为布尔,转换为数字)
NaN == 1 // 字符串转数字(准则三,一方为字符串另外一方为数字则将字符串转数字)
// 结果为false
复制代码
{} == 1
,转换过程为:{} == 1
"[object Object]" == 1 // 对象转字符串
NaN == 1 // 字符串转数字(准则三,一方为字符串另外一方为数字则将字符串转数字)
// 结果为 false
复制代码
{} == {}
: 这个你就理解为对象是引用类型,那么这两个对象都有本身独立的堆空间,确定就是不相等的了。(一方为数组的状况)
console.log([] == 0)
console.log([1] == 1)
console.log(['1'] == 1)
console.log([] == 1)
console.log(['1', '2'] == 1)
console.log(['1', '2'] == ['1', '2'])
console.log([{}, {}] == '[object Object],[object Object]')
console.log([] == true)
console.log([] == Symbol('1'))
复制代码
题目解析:
console.log([] == 0)
[] == 0
'' == 0 // []空数组转为字符串为空字符串
0 == 0 // 空字符串转为数字为0
// true
console.log([1] == 1)
[1] == 1
'1' == 1 // [1]非空数组且数组长度为1,转换为字符串为'1'
1 == 1 // '1'字符串转换为数字1
// true
console.log(['1'] == 1) // 转换过程和上面一个同样
// true
console.log([] == 1)
[] == 1
'' == 1 // 空数组转为字符串为''
0 == 1 // 空字符串转为数字为0
// false
console.log(['1', '2'] == 1)
['1', '2'] == 1
'1,2' == 1 // ['1', '2']数组转为字符串为'1,2'
NaN == 1 // '1,2'字符串转为数字为NaN
console.log(['1', '2'] == ['1', '2']) // 引用地址不一样
// false
console.log([{}, {}] == '[object Object][object Object]')
[{}, {}] == '[object Object][object Object]'
// [{},{}]数组中的每一项也就是{}转为字符串为'[object Object]',而后进行拼接
'[object Object],[object Object]' == '[object Object],[object Object]'
// true
console.log([] == true)
[] == true
[] == 1 // 有一项为布尔,所以将布尔true转为数字1
'' == 1 // 有一项为数组, 所以将[]转为空字符串
0 == 1 // 空字符串转为数字0
// false
console.log([] == Symbol('1'))
[] == Symbol('1')
'' == Symbol('1')
// false
复制代码
(理解!
运算符的转换)
当咱们使用!
的时候,实际上会将!
后面的值转换为布尔类型来进行比较,这也就是我在题3.1
说到过的不严谨的状况。
并且我发现这种转换是不会通过ToNumber()
的,而是直接转换为了布尔值,让咱们来验证一下:
var b = {
valueOf: function () {
console.log('b.valueOf')
return '1'
},
toString: function () {
console.log('b.toString')
return '2'
}
}
console.log(!b == 1)
console.log(!b == 0)
复制代码
这里的执行结果是:
false
true
复制代码
能够看到,!b
它在转换的过程当中并无通过valueOf
或者toString
,而是直接转为了false
再来作几道题哈:
console.log(!null == !0)
console.log(!undefined == !0)
console.log(!!null == !!0)
console.log(!{} == {})
console.log(!{} == [])
console.log(!{} == [0])
复制代码
答案:
console.log(!null == !0) // true
console.log(!undefined == !0) // true
console.log(!!null == !!0) // true
console.log(!{} == {}) // false
console.log(!{} == []) // true
console.log(!{} == [0]) // true
复制代码
能够看到,刚刚还不相等的null
和0
在分别加上了!
以后,就变为相等了。
前面三个输出结果应该都没有什么问题,来看看后面三个:
!{} == {}
:
!{}
,转换以后为false
false == {}
,一方有布尔的状况,将布尔转换为数字,即0 == {}
0 == '[object Object]'
0 == NaN
false
!{} == []
:
!{}
,转换以后为false
false == []
,一方有布尔,将布尔转换为数字,即0 == []
,0 == '0'
0 == 0
true
而!{} == [0]
的转换流程和!{} == []
同样。
如今你能弄懂开始说的那几道题了吗?
让咱们再来看看,此次确定以为很简单:
var b = {
valueOf() {
console.log('valueOf')
return []
},
toString () {
console.log('toString')
return false
}
}
console.log(![] == [])
console.log(![] == b)
复制代码
![] == []
:
![]
转换为布尔类型,[]
为true
,那么![]
就是false
[]
转为数字是为0
,0
与false
比较,将false
也转换为0
,因此结果为true
![] == b
:
![]
转为了false
b
会先执行valueOf
,而后执行toString
,返回的也是false
true
答案:
true
'valueOf'
'toString'
true
复制代码
(理解==
比较时对象的Symbol.toPrimitive
函数的hint
参数)
var b = {
[Symbol.toPrimitive] (hint) {
console.log(hint)
if (hint === 'default') {
return 2
}
}
}
console.log(b == 2)
console.log(b == '2')
复制代码
经过上面👆几个案例,咱们均可以看出对象在进行==
比较时会通过相似于ToNumber
的转换过程:
valueOf()
toString()
但其在进行从新Symbol.toPrimitive
接收到的参数会是"default"
,并非"number"
。
因此这里的答案为:
'default'
true
'default'
true
复制代码
(函数在使用==
时的转换)
函数其实也是一个对象,因此在进行==
比较时也和普通对象同样处理便可。
可是我只想要提醒一点,在进行==
比较时要注意是比较函数自己仍是比较函数的返回值。
例如在这道题中:
function f () {
var inner = function () {
return 1
}
inner.valueOf = function () {
console.log('valueOf')
return 2
}
inner.toString = function () {
console.log('toString')
return 3
}
return inner
}
console.log(f() == 1)
console.log(f()() == 1)
复制代码
f()
表示的是inner
这个函数,因此f() == 1
至关因而inner == 1
,所以此时就涉及到了inner
函数的类型转换,就会触发inner.valueOf()
,返回2
,所以第一个是false
f()()
表示的是inner()
调用以后的返回值,也就是1
,因此此时是1 == 1
进行比较,并不会涉及到inner
函数的类型转换,也就不会触发inner.valueOf()
,所以第二个为true
。结果:
'valueOf'
false
true
复制代码
作完了这十一道题,相信你对==
的比较应该比以前更了解了吧 😁,让咱们来总结一波。
当使用==
进行比较的时候,会有如下转换规则(判断规则):
2 == 3
确定是为false
的了null、undefined
,则另外一方必须为null或者undefined
才为true
,也就是null == undefined
为true
或者null == null
为true
,由于undefined
派生于null
String
,是的话则把String
转为Number
再来比较Boolean
,是的话则将Boolean
转为Number
再来比较ToNumber
的转换形式来进行比较(也就是toPrimitive(obj, 'defalut')
当一方有为对象的时候,实际是会将对象执行ToNumber
操做以后再进行比较的,可是又因为对象的valueOf()
基本都是它自己,因此咱们能够认为省略了这一步。
这里我贴上一张流程图,感受画的挺不错的,你们能够对照着看一下:
(图片来源:segmentfault.com/a/119000001…)
除了在==
的比较中会进行类型转换以外,其它的运算符号也会有。
好比标题上常见的这五种。
这里我主要是分两类来讲:
-、*、/、%
这四种都会把符号两边转成数字来进行运算+
因为不只是数字运算符,仍是字符串的链接符,因此分为两种状况:(四种简单运算符的类型转换)
先来讲说除了+
号之外的其它四种运算符的转换,因为基本数据类型应该都清楚,因此就不作说明了,这里主要是想说一下对象运算时的状况:
var b = {}
console.log(b - '2')
console.log(b * '2')
console.log(b / '2')
console.log(b % '2')
console.log(b - [])
console.log(b - {})
复制代码
b
是一个对象,在进行这类运算的时候,把两端都转换为数字进行计算,而咱们知道对象{}
转为数字是NaN
,因此答案全都是NaN
。
答案:
NaN
NaN
NaN
NaN
NaN
NaN
复制代码
(四种运算符的实际转换-重写toString()
和valueOf()
)
咱们将上面👆那道题的b
对象重写一下它们的toString()
和valueOf()
方法,想一想,若是它是遵循ToNumber()
转换的话,那么如下的结果会是什么呢?
var b = {
valueOf () {
console.log('valueOf')
return {}
},
toString () {
console.log('toString')
return 1
}
}
console.log(b - '2')
console.log(b * '2')
console.log(b / '2')
console.log(b % '2')
console.log(b - [])
console.log(b - {})
复制代码
在调用b
的时候,会先执行valueOf()
方法,若是该方法返回的是一个基本数据类型则返回,不然继续调用toString()
方法,很显然这里的valueOf()
返回的仍是一个引用类型,因此总会调用toString()
,所以答案为:
'valueOf'
'toString'
-1
'valueOf'
'toString'
2
'valueOf'
'toString'
0.5
'valueOf'
'toString'
1
'valueOf'
'toString'
1
'valueOf'
'toString'
NaN
复制代码
这里要说一下的是最后两个输出结果。
b - []
:
b
输出的是1
,由于[]
转换为数字咱们知道是0
,因此结果为1
b - {}
b
为1
,{}
转为数字为NaN
,因此结果为NaN
(四种运算符的实际转换-重写Symbol.toPrimitive
)
4.1
那道题咱们除了重写toString
和valueOf
咱们还能够从新什么呢?
嘻嘻,怎么能忘了Symbol.toPrimitive
,若是从新了它,那你以为它接收到的hint
参数会是什么呢?
var b = {
[Symbol.toPrimitive] (hint) {
if (hint === 'default') {
console.log('default')
return 'default'
}
if (hint === 'number') {
console.log('number')
return 1
}
if (hint === 'string') {
console.log('string')
return '2'
}
}
}
console.log(b - '2')
console.log(b * '2')
console.log(b / '2')
console.log(b % '2')
console.log(b - [])
console.log(b - {})
复制代码
既然是会把运算符两边都转换为数字进行计算,那么hint
接收到的确定就是'number'
了呀,没错,因此这里b
老是会返回1
,所以答案为:
'number'
-1
'number'
2
'number'
0.5
'number'
1
'number'
1
'number'
NaN
复制代码
yeah~感受没啥难度。
(+
号对于对象的转换)
+b
的状况就至关于转为数字+
号两边有值则判断两边值的类型,若两边都为数字则进行数字计算,如有一边是字符串,就会把另外一边也转换为字符串进行链接var b = {}
console.log(+b)
console.log(b + 1)
console.log(1 + b)
console.log(b + '')
复制代码
依照着这个规则,咱们能够得出答案:
NaN
'[object Object]1'
'1[object Object]'
'[object Object]'
复制代码
('+'
运算符与String()
的区别)
一样的,咱们给上题加上Symbol.toPrimitive
看一下:
var b = {
[Symbol.toPrimitive] (hint) {
if (hint === 'default') {
console.log('default')
return '我是默认'
}
if (hint === 'number') {
console.log('number')
return 1
}
if (hint === 'string') {
console.log('string')
return '2'
}
}
}
console.log(+b)
console.log(b + 1)
console.log(1 + b)
console.log(b + '')
console.log(String(b))
复制代码
由于+b
走的是转换数字的路线,因此它的hint
确定就是number
。
但是对于b + 1
这种字符串链接的状况,走的却不是string
,而是default
。
因此能够看到答案为:
var b = {
[Symbol.toPrimitive] (hint) {
if (hint === 'default') {
console.log('default')
return '我是默认'
}
if (hint === 'number') {
console.log('number')
return 1
}
if (hint === 'string') {
console.log('string')
return '2'
}
}
}
console.log(+b) // number
console.log(b + 1) // default
console.log(1 + b) // default
console.log(b + '') // default
console.log(String(b)) // string
'number'
1
'default'
'我是默认1'
'default'
'1我是默认'
'default'
'我是默认'
'string'
'2'
复制代码
能够看到b + 1
和String(b)
这两种促发的转换规则是不同的
{} + 1
字符串链接时hint
为default
String({})
时hint
为string
鉴于我不知道上面👆的default
和number、string
有什么区别,因此我以为应该要重写一下toString
和valueOf()
来看看会发生什么。
var b = {
valueOf () {
console.log('valueOf')
return {}
},
toString () {
console.log('toString')
return 1
},
}
console.log(+b) // number
console.log(b + 1) // default
console.log(String(b)) // string
复制代码
此时的结果为:
'valueOf'
'toString'
1
'valueOf'
'toString'
2
'toString'
'1'
复制代码
我发现default
的转换方式和number
很像,都是先执行判断有没有valueOf
,有的话执行valueOf
,而后判断valueof
后的返回值,如果是引用类型则继续执行toString
。(这点其实在题目3.10
中也说到了)
(日期对象的数据转换)
以前咱们有提到过,日期对象的转换比较特殊。(在引用类型调用valueOf()中)
valueOf
返回的是它自己,也就是引用类型valueOf
返回的是一个数字类型的毫秒数var date = new Date()
console.log(date.valueOf())
console.log(date.toString())
console.log(+date)
console.log('' + date)
复制代码
因此咱们能够看到这里的答案是:
var date = new Date()
console.log(date.valueOf()) // 1585742078284
console.log(date.toString()) // Wed Apr 01 2020 19:54:38 GMT+0800 (中国标准时间)
console.log(+date) // 1585742078284
console.log('' + date) // Wed Apr 01 2020 19:54:38 GMT+0800 (中国标准时间)
复制代码
+date
是转换为数字,因此结果和date.valueOf()
结果一致。
可是咱们会发现这里的'' + date
和上面的'' + {}
就会有所不一样了。
虽然一样都是被转换为字符串,可是还记得'' + {}
的转换顺序吗?它的转换方式是遵循ToNumber
的,也就是会先执行valueOf()
,再执行toString()
,因为{}.valueOf
等于它自己,是引用类型,因此会继续执行toString()
。
而date
进行+
号字符串链接不会遵循这种转换规则,而是优先调用toString()
。
对于几种经常使用运算符的类型转换:
-、*、/、%
这四种都会把符号两边转成数字来进行运算+
因为不只是数字运算符,仍是字符串的链接符,因此分为两种状况:+b
这种状况至关于转换为数字)对象的+
号类型转换:
+
号字符串链接的时候,toPrimitive
的参数hint
是default
,可是default
的执行顺序和number
同样都是先判断有没有valueOf
,有的话执行valueOf
,而后判断valueof
后的返回值,如果是引用类型则继续执行toString
。(相似题4.5
和4.6
)+
号字符串链接的时候,优先调用toString()
方法。(相似题4.7
)mm...
不知道您看到如今还好不?应该还没炸吧
铺垫了这么久,是时候展现正在的技术了!
下面让咱们来作几道综合题检验一下[阴笑~]
console.log([] == [])
console.log([] == ![])
console.log({} == 1)
复制代码
==
号两边都是引用类型则判断是否为同一引用[] == ![]
这个在3.9
中说的很详细了{} == 1
,简单来讲两边都转换为数字,{}
转换为数字为NaN
,因此结果为false
。详细来讲:一方为对象,将对象转换为字符串进行比较,即"[object Object]" == 1
;一方有字符串,将字符串转换为数字进行比较,即NaN == 1
,因此结果为false
答案为:
console.log([] == []) // false
console.log([] == ![]) // true
console.log({} == 1) // false
复制代码
console.log({} + "" * 1)
console.log({} - [])
console.log({} + [])
console.log([2] - [] + function () {})
复制代码
{} + "" * 1
:
"" * 1
,结果为0
,由于""
转换为数字是0
{} + 0
,将{}
转换为字符串是"[object Object]"
,0
转换为字符串是"0"
"[object Object]0"
{} - []
:
-
号两边转换为数字,{}
为NaN
,[]
为0
,因此结果为NaN
{} + []
:
{}
转为字符串为"[object Object]"
,[]
转为字符串为""
,因此结果为"[object Object]"
[2] - [] + function () {}
:
-
号两边转换为数字分别为2
和0
,因此[2] - []
结果为2
2 + function () {}
,两边转换为字符串拼接为"2function () {}"
,由于函数是会转换为源代码字符串的。答案为:
console.log({} + "" * 1) // "[object Object]0"
console.log({} - []) // NaN
console.log({} + []) // "[object Object]"
console.log([2] - [] + function () {}) // "2function () {}"
复制代码
这道题相信你们看的不会少,除了重写valueOf()
你还会哪些解法呢?
解法一:重写valueOf()
这个解法是利用了:当对象在进行==
比较的时候实际是会先执行valueOf()
,如果valueOf()
的返回值是基本数据类型就返回,不然仍是引用类型的话就会继续调用toString()
返回,而后判断toString()
的返回值,如果返回值为基本数据类型就返回,不然就报错。
如今valueOf()
每次返回的是一个数字类型,因此会直接返回。
// 1
var a = {
value: 0,
valueOf () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
解法二:重写valueOf()
和toString()
var a = {
value: 0,
valueOf () {
return {}
},
toString () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
原理就是解法一的原理,只不过用到了当valueOf()
的返回值是引用类型的时候会继续调用toString()
。
这里你甚至均可以不用重写valueOf()
,由于除了日期对象其它对象在调用valueOf()
的时候都是返回它自己。
也就是说你也能够这样作:
var a = {
value: 0,
toString () {
return ++this.value
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
解法三:重写Symbol.toPrimitive
想一想是否是还能够用Symbol.toPrimitive
来解呢?
结合题3.10
咱们知道,当对象在进行==
比较的时候,Symbol.toPrimitive
接收到的参数hint
是"defalut"
,那么咱们只须要这样重写:
var a = {
value: 0,
[Symbol.toPrimitive] (hint) {
if (hint === 'default') {
return ++this.value
}
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
这样结果也是能够的。
解法四:定义class并重写valueOf()
固然你还能够用class
来写:
class A {
constructor () {
this.value = 0
}
valueOf () {
return ++this.value
}
}
var a = new A()
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
解法五:利用数组转为字符串会隐式调用join()
什么 ? 还有别的解法吗?并且我看解法五的题目有点没看懂啊。
让咱们回过头去看看题1.3
,那里提到了当数组在进行转字符串的时候,调用toString()
的结果其实就是调用join
的结果。
那和这道题有什么关系?来看看答案:
let a = [1, 2, 3]
a['join'] = function () {
return this.shift()
}
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
由于咱们知道,对象若是是数组的话,当咱们不重写其toString()
方法,在转换为字符串类型的时候,默认实现就是将调用join()
方法的返回值做为toString()
的返回值。
因此这里咱们重写了a
的join
方法,而此次重写作了两件事情:
a
执行a.shift()
方法,咱们知道这会影响原数组a
的,将第一项去除因此当咱们在执行a == 1
这一步的时候,因为隐式调用了a['join']
方法,因此会执行上面👆说的那两件事情,后面的a == 2
和a == 3
同理。
解法六:定义class继承Array并重写join()
对于解法五咱们一样能够用class
来实现
class A extends Array {
join = this.shift
}
var a = new A(1, 2, 3)
if (a == 1 && a == 2 && a == 3) {
console.log('成立')
}
复制代码
这种写法比较酷🆒,可是第一次看可能不太能懂。
A
这个类经过extends
继承于Array
,这样经过new A
建立的就是一个数组A
重写了join
方法,join = this.shift
就至关因而join = function () { return this.shift() }
a == xxx
的时候,都会隐式调用咱们自定义的join
方法,执行和解法五同样的操做。这道题看着和上面那道有点像,不过这里判断的条件是全等的。
咱们知道全等的条件:
false
,这点和==
不一样,==
会发生隐式类型转换而对于上面👆一题的解法咱们都是利用了==
会发生隐式类型转换这一点,显然若是再用它来解决这道题是不能实现的。
想一想当咱们在进行a === xxx
判断的时候,实际上就是调用了a
这个数据而已,也就是说咱们要在调用这个数据以前,作一些事情,来达到咱们的目的。
不知道这样说有没有让你想到些什么 🤔️?或许你和呆呆同样会想到Vue
大名鼎鼎的数据劫持 😁。
想一想在Vue2.x
中不就是利用了Object.defineProperty()
方法从新定义data
中的全部属性,那么在这里咱们一样也能够利用它来劫持a
,修改a
变量的get
属性。
var value = 1;
Object.defineProperty(window, "a", {
get () {
return this.value++;
}
})
if (a === 1 && a === 2 && a === 3) {
console.log('成立')
}
复制代码
这里实际就作了这么几件事情:
Object.defineProperty()
方法劫持全局变量window
上的属性a
a
的时候将value
自增,并返回自增后的值(其实我还想着用Proxy
来进行数据劫持,代理一下window
,将它用new Proxy()
处理一下,可是对于window
对象好像没有效果...)
解法二
怎么办 😂,一碰到这种题我又想到了数组...
var arr = [1, 2, 3];
Object.defineProperty(window, "a", {
get () {
return this.arr.shift()
}
})
if (a === 1 && a === 2 && a === 3) {
console.log('成立')
}
复制代码
中了shift()
的毒...固然,这样也是能够实现的。
解法三
还有就是EnoYao大佬那里看来的骚操做:
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if (aᅠ == 1 && a == 2 && ᅠa == 3) {
console.log("成立");
}
复制代码
说来惭愧...a
的先后隐藏的字符我打不来 😂...
现须要实现如下函数:
function f () {
/* 代码 */
}
console.log(f(1) == 1)
console.log(f(1)(2) == 3)
console.log(f(1)(2)(3) == 6)
复制代码
首先看到这道题的时候让我想到了题目3.11
,只不过这里是有传参的,而且返回值像是一个累计的过程。
也就是说会收集每次传递进来的参数而后进行一个累加并返回(这个很容易想到reduce
方法)。
而且f(1)(2)
这样的写法很像是偏应用,函数返回了一个函数。
那咱们是否是能够在函数f
内用一个变量数组来存放参数集合,而后返回一个函数(我命名为inner
),这个inner
函数的做用是收集传递进来的参数将它添加到参数集合中。
以后就和3.11
很像,在每次进行==
比较的时候,f
返回的inner
函数会进行隐式类型转换,也就是会调用inner
的valueOf()
和toString()
方法,那咱们只须要重写这两个方法,并返回用reduce
累加的参数的和就能够了。
代码也很简单,一块儿来看看:
function f () {
let args = [...arguments]
var add = function () {
args.push(...arguments)
return add
}
add.valueOf = function () {
return args.reduce((cur, pre) => {
return cur + pre
})
}
return add
}
console.log(f(1) == 1)
console.log(f(1)(2) == 3)
console.log(f(1)(2)(3) == 6)
复制代码
固然,上面👆的valueOf()
换成toString()
也是能够的,由于咱们已经知道了,对象==
比较时类型转换的顺序其实就是先通过valueOf
再到toString
。
气氛不要这么凝重嘛...让咱们最后来看道简单有趣的题。
这道有趣的题是从LINGLONG的一篇《【js小知识】[]+ {} =?/{} +[] =?(关于加号的隐式类型转换)》那里看来的。
(PS: pick一波玲珑,这位小姐姐的文章写的都挺好的,不过热度都不高,你们能够支持一下呀 😁)
OK👌,来看看题目是这样的:
在控制台(好比浏览器的控制台)输入:
{}+[]
复制代码
的结果会是什么 🤔️?
咦~这道题上面不是作过了吗(题目5.2
里的第三个console.log()
)?
console.log({}+[]) // "[object Object]"
复制代码
可是注意这里的题目,是要在控制台输出哦。
此时我把这段代码在控制台输出结果发现答案居然和预期的不同:
{}+[]
0
复制代码
也就是说{}
被忽略了,直接执行了+[]
,结果为0
。
知道缘由的我眼泪掉了下来,原来它和以前提到的1.toString()
有点像,也是由于JS
对于代码解析的缘由,在控制台或者终端中,JS
会认为大括号{}
开头的是一个空的代码块,这样看着里面没有内容就会忽略它了。
因此只执行了+[]
,将其转换为0
。
若是咱们换个顺序的话就不会有这种问题:
(图片来源:juejin.im/post/5e6055…)
为了证明这一点,咱们能够把{}
当成空对象来调用一些对象的方法,看会有什么效果:
(控制台或者终端)
{}.toString()
复制代码
如今的{}
依旧被认为是代码块而不是一个对象,因此会报错:
Uncaught SyntaxError: Unexpected token '.'
复制代码
解决办法能够用一个()
将它扩起来:
({}).toString
复制代码
不过这东西在实际中用的很少,我能想到的一个就是在项目中(好比我用的vue
),而后定义props
的时候,若是其中一个属性的默认值你是想要定义为一个空对象的话,就会用到:
props: {
target: {
type: Object,
default: () => ({})
}
}
复制代码
整完了整完了...啊...
知识无价,支持原创。
参考文章:
你盼世界,我盼望你无bug
。这篇文章就介绍到这里。
能够看到,当咱们了解了类型转换的原理以后发现并非太难。关键就是在于半懂不懂的时候最可怕。就像没了解以前,我能很快的说出{} == 1
的结果是false
,可是如今脑壳里会先转上一圈:
"[object Object]" == 1
NaN == 1
,因此结果为false
对于一些简单的题甚至会迟钝一下...这些都是还不熟形成的,但愿你们可以多多练习早日避免呆呆
这种呆
的状态。
用心创做,好好生活。这篇文章出了以后这段时间可能不会再出这种都是题目的文章了,作多了你们也会麻木不想看,因此后面会出一些原理性的文章,敬请期待吧,嘻嘻😁。
若是你以为文章对你有帮助的话来个赞👍哦,谢谢啦~ 😁。
喜欢霖呆呆的小伙还但愿能够关注霖呆呆的公众号 LinDaiDai
或者扫一扫下面的二维码👇👇👇.
我会不定时的更新一些前端方面的知识内容以及本身的原创文章🎉
你的鼓励就是我持续创做的主要动力 😊.
相关推荐:
《【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理)》
《【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)》
《【何不三连】比继承家业还要简单的JS继承题-封装篇(牛刀小试)》