目的

现在发现在学习JS的过程中,以前的学习大多都浮于表层,只是了解了一个功能的实现,而没有深入理解一个功能时怎样实现的,这也反映了我们学习过程中遇到的一个很普遍的问题:看问题只看表层。这个问题导致了我们在学习的过程中往往会一知半解,并没有深刻体会到一个知识的本质是什么。而在学习的过程中,只有懂了最基础最底层的东西才能对表层的东西有更深刻的理解,甚至决定了你在这条路上能够走多远。这正是How和Why的精髓所在,回想自己以前的学习过程,能做到举一反三的前提正是因为理解了一个问题的本质,所以在学习JS这门语言的过程中,我们更应该关注的是为什么这个表达式的结果是这样的,而不仅仅是记住结果,例如:为什么0.1+0.2 !== 0.3 这些问题都应该从ECMA规范出发,这正是作为一个前端小白的我想写这篇文章的目的。

开始

一丶相等运算符

例子:

  1. 0 == null //false

在第一次看到这个问题的时候,我的想法是true,但是事实却不是这样,为什么呢?
在ECMA规范里,对于相等运算符有如下12步运算:

  1. 如果x不是正常值(比如抛出一个错误),中断执行。
  2. 如果y不是正常值,中断执行。
  3. 如果Type(x)Type(y)相同,执行严格相等运算x === y
  4. 如果xnullyundefined,返回true
  5. 如果xundefinedynull,返回true
  6. 如果Type(x)是数值,Type(y)是字符串,返回x == ToNumber(y)的结果。
  7. 如果Type(x)是字符串,Type(y)是数值,返回ToNumber(x) == y的结果。
  8. 如果Type(x)是布尔值,返回ToNumber(x) == y的结果。
  9. 如果Type(y)是布尔值,返回x == ToNumber(y)的结果。
  10. 如果Type(x)是字符串或数值或Symbol值,Type(y)是对象,返回x == ToPrimitive(y)的结果。
  11. 如果Type(x)是对象,Type(y)是字符串或数值或Symbol值,返回ToPrimitive(x) == y的结果。
  12. 返回false

所以,对于上面的例子,在前十一条中,我们并没有找到相关的运算规则,所以直到第十二条,我们找到了它的结果是false。这里0的类型是Number,而null的类型是Null。
另外,对于关于相等运算符的规范中,第四条和第五条尤其值得我们注意。
注意:== 趋向于把类型不同的指转换为 number 类型进行比较,而不是 boolean 类型
那么来看看下面这些例子你懂了吗?

  1. console.log([10] == 10); //true
  2. console.log('10' == 10); //true
  3. console.log([] == 0); //true
  4. console.log(true == 1); //true
  5. console.log([] == false); //true
  6. console.log(![] == false); //true
  7. console.log('' == 0); //true
  8. console.log('' == false); //true
  9. console.log(null == false); //false
  10. console.log(!null == true); //true
  11. console.log(null == undefined); //true

可能你对上面的某几个例子还有疑问,对于第五个例子,左边是对象,右边是布尔值,需要把他们都转化为数值进行比较,对于左侧,不能直接把对象化为数值,那么需要先使用toString方法,将其变为“”,而空字符串转化为数值为0,右边的布尔值转化为数值也为0,所以为true。对于第六个例子,两边都是布尔值,我们需要转化为数值进行比较,那么先把左面转化为真正的布尔值表现形式,首先,[]是一个对象,其转化为布尔值为true,取反,则为false,false变为数值则为0,而右边变为数值也为0。那么第七个和第八个也就解释的通了。对于第九个例子,我们可以依据第十二条判断其为false,所以null==true返回的其实也是false,对于第十个!null是布尔值,而null转化为布尔值为false,取反,为true,所以结果为true。

二丶数组的空位

例子:

  1. const a1 = [undefined, undefined, undefined];
  2. const a2 = [, , ,];
  3. a1.length // 3
  4. a2.length // 3
  5. a1[0] // undefined
  6. a2[0] // undefined
  7. a1[0] === a2[0] // true

表面上看来两个数组并没有什么差别,但实际上,两个数组差别很大。

  1. 0 in a1 // true
  2. 0 in a2 // false
  3. a1.hasOwnProperty(0) // true
  4. a2.hasOwnProperty(0) // false
  5. Object.keys(a1) // ["0", "1", "2"]
  6. Object.keys(a2) // []
  7. a1.map(n => 1) // [1, 1, 1]
  8. a2.map(n => 1) // [, , ,]

前三种运算说明a2取不到属性名,最后一种运算说明a2无法遍历。
原因是什么呢?
根据ECMA规范:
“数组成员可以省略。只要逗号前面没有任何表达式,数组的length属性就会加1,并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员,则不会导致数组length属性增加。”
这就解释了为什么in运算符、数组的hasOwnProperty方法、Object.keys方法,都取不到空位的属性名。因为这个属性名根本就不存在,规格里面没说要为空位分配属性名(位置索引),只说要为下一个元素的位置索引加1。
而对于in方法和hasownproperty方法,prop值可以是数组的索引,两种方法的差别是in方法可以识别原型对象。所以前六个就很好解释了。
对于后两个,我们引入另一个例子:

  1. const arr = [, , ,];
  2. arr.map(n => {
  3. console.log(n);
  4. return 1;
  5. }) // [, , ,]

上面代码中,arr是一个全是空位的数组,map方法遍历成员时,发现是空位,就直接跳过,不会进入回调函数。因此,回调函数里面的console.log语句根本不会执行,整个map方法返回一个全是空位的新数组。

后记

我怎么会写这种傻逼玩意!使用JS千万不要用==,你永远记不住它诡异的类型转化。
image.pngimage.png
image.png