目的
现在发现在学习JS的过程中,以前的学习大多都浮于表层,只是了解了一个功能的实现,而没有深入理解一个功能时怎样实现的,这也反映了我们学习过程中遇到的一个很普遍的问题:看问题只看表层。这个问题导致了我们在学习的过程中往往会一知半解,并没有深刻体会到一个知识的本质是什么。而在学习的过程中,只有懂了最基础最底层的东西才能对表层的东西有更深刻的理解,甚至决定了你在这条路上能够走多远。这正是How和Why的精髓所在,回想自己以前的学习过程,能做到举一反三的前提正是因为理解了一个问题的本质,所以在学习JS这门语言的过程中,我们更应该关注的是为什么这个表达式的结果是这样的,而不仅仅是记住结果,例如:为什么0.1+0.2 !== 0.3 这些问题都应该从ECMA规范出发,这正是作为一个前端小白的我想写这篇文章的目的。
开始
一丶相等运算符
例子:
0 == null //false
在第一次看到这个问题的时候,我的想法是true,但是事实却不是这样,为什么呢?
在ECMA规范里,对于相等运算符有如下12步运算:
- 如果
x
不是正常值(比如抛出一个错误),中断执行。 - 如果
y
不是正常值,中断执行。 - 如果
Type(x)
与Type(y)
相同,执行严格相等运算x === y
。 - 如果
x
是null
,y
是undefined
,返回true
。 - 如果
x
是undefined
,y
是null
,返回true
。 - 如果
Type(x)
是数值,Type(y)
是字符串,返回x == ToNumber(y)
的结果。 - 如果
Type(x)
是字符串,Type(y)
是数值,返回ToNumber(x) == y
的结果。 - 如果
Type(x)
是布尔值,返回ToNumber(x) == y
的结果。 - 如果
Type(y)
是布尔值,返回x == ToNumber(y)
的结果。 - 如果
Type(x)
是字符串或数值或Symbol
值,Type(y)
是对象,返回x == ToPrimitive(y)
的结果。 - 如果
Type(x)
是对象,Type(y)
是字符串或数值或Symbol
值,返回ToPrimitive(x) == y
的结果。 - 返回
false
。
所以,对于上面的例子,在前十一条中,我们并没有找到相关的运算规则,所以直到第十二条,我们找到了它的结果是false。这里0的类型是Number,而null的类型是Null。
另外,对于关于相等运算符的规范中,第四条和第五条尤其值得我们注意。
注意:==
趋向于把类型不同的指转换为 number 类型进行比较,而不是 boolean 类型
那么来看看下面这些例子你懂了吗?
console.log([10] == 10); //true
console.log('10' == 10); //true
console.log([] == 0); //true
console.log(true == 1); //true
console.log([] == false); //true
console.log(![] == false); //true
console.log('' == 0); //true
console.log('' == false); //true
console.log(null == false); //false
console.log(!null == true); //true
console.log(null == undefined); //true
可能你对上面的某几个例子还有疑问,对于第五个例子,左边是对象,右边是布尔值,需要把他们都转化为数值进行比较,对于左侧,不能直接把对象化为数值,那么需要先使用toString方法,将其变为“”,而空字符串转化为数值为0,右边的布尔值转化为数值也为0,所以为true。对于第六个例子,两边都是布尔值,我们需要转化为数值进行比较,那么先把左面转化为真正的布尔值表现形式,首先,[]是一个对象,其转化为布尔值为true,取反,则为false,false变为数值则为0,而右边变为数值也为0。那么第七个和第八个也就解释的通了。对于第九个例子,我们可以依据第十二条判断其为false,所以null==true返回的其实也是false,对于第十个!null是布尔值,而null转化为布尔值为false,取反,为true,所以结果为true。
二丶数组的空位
例子:
const a1 = [undefined, undefined, undefined];
const a2 = [, , ,];
a1.length // 3
a2.length // 3
a1[0] // undefined
a2[0] // undefined
a1[0] === a2[0] // true
表面上看来两个数组并没有什么差别,但实际上,两个数组差别很大。
0 in a1 // true
0 in a2 // false
a1.hasOwnProperty(0) // true
a2.hasOwnProperty(0) // false
Object.keys(a1) // ["0", "1", "2"]
Object.keys(a2) // []
a1.map(n => 1) // [1, 1, 1]
a2.map(n => 1) // [, , ,]
前三种运算说明a2取不到属性名,最后一种运算说明a2无法遍历。
原因是什么呢?
根据ECMA规范:
“数组成员可以省略。只要逗号前面没有任何表达式,数组的length属性就会加1,并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员,则不会导致数组length属性增加。”
这就解释了为什么in运算符、数组的hasOwnProperty方法、Object.keys方法,都取不到空位的属性名。因为这个属性名根本就不存在,规格里面没说要为空位分配属性名(位置索引),只说要为下一个元素的位置索引加1。
而对于in方法和hasownproperty方法,prop值可以是数组的索引,两种方法的差别是in方法可以识别原型对象。所以前六个就很好解释了。
对于后两个,我们引入另一个例子:
const arr = [, , ,];
arr.map(n => {
console.log(n);
return 1;
}) // [, , ,]
上面代码中,arr是一个全是空位的数组,map方法遍历成员时,发现是空位,就直接跳过,不会进入回调函数。因此,回调函数里面的console.log语句根本不会执行,整个map方法返回一个全是空位的新数组。
后记
我怎么会写这种傻逼玩意!使用JS千万不要用==,你永远记不住它诡异的类型转化。