CruxF / Blog

个人博客,记载学习的点点滴滴,怕什么技术无穷,进一寸有一寸的欢喜:sparkles:
63 stars 19 forks source link

CV大佬今日让你彻底的、永久的搞懂JS“==”运算 #11

Open CruxF opened 6 years ago

CruxF commented 6 years ago

前言

相信很多戳进来的人都会很好奇,CV到底是个啥东东?很厚脸皮的说那就是:ctrl+c和ctrl+v重度使用者:joy:。简单说一下copy的原因:有一篇我看了好几遍都感到头疼却重要的文章,后来一不小心去看了他的参考文章,也是该篇的原文,才豁然开朗。为了避免这种好文有一天可能莫名其妙的消失不见,因此心中就起了“歹意”,废了点时间将其抄下来。

正文

大家知道,==是JavaScript中比较复杂的一个运算符。它的运算规则奇怪,容易让人犯错,从而成为JavaScript中“最糟糕的特性”之一。

在仔细阅读了ECMAScript规范的基础上,我画了一张图,我想通过它你会彻底地搞清楚关于==的一切。同时,我也试图通过此文向大家证明==并不是那么糟糕的东西,它很容易掌握,甚至看起来很合理。先上图1 image

==运算规则的精确描述在此:The Abstract Equality Comparison Algorithm。但是,这么复杂的描述,你确定看完后脑子不晕?确定立马就能拿它指导实践?

肯定不行,规范毕竟是给JavaScript运行环境的开发人员看的(比如V8引擎的开发人员们),而不是给语言的使用者看的。而上图正是将规范中复杂的描述翻译成了更容易看懂的形式。在详细介绍图1中的每个部分前,我们来复习一下JS中关于类型的知识:

现在考虑表达式:

x == y

其中x和y是上述六种类型中某一种类型的值。当x和y的类型相同时,x == y可以转化为x === y,而后者是很简单的(唯一需要注意的可能是NaN),所以下面我们只考虑x和y的类型不同的情况。

一、有和无

在图1中,JavaScript值的六种类型用蓝底色的矩形表示。它们首先被分成了两组:

分组的依据是什么?我们来看一下,右侧的Undefined和Null是用来表示不确定、无或者空的,而右侧的四种类型都是确定的、有和非空。我们可以这样说:左侧是一个存在的世界,右侧是一个空的世界。

所以,左右两个世界中的任意值做==比较的结果都是false是很合理的。(见图1中连接两个矩形的水平线上标的false)

二、空和空

JavaScript中的undefined和null是另一个经常让我们崩溃的地方。通常它被认为是一个设计缺陷,这一点我们不去深究。不过我曾听说,JavaScript的作者最初是这样想的:

假如你打算把一个变量赋予对象类型的值,但是现在还没有赋值,那么你可以用null表示此时的状态(证据之一就是typeof null 的结果是'object');相反,假如你打算把一个变量赋予原始类型的值,但是现在还没有赋值,那么你可以用undefined表示此时的状态。

不管这个传闻是否可信,它们两者做==比较的结果是true是很合理的。(见图1中右侧垂直线上标的true)。在进行下一步之前,我们先来说一下图1中的两个符号:大写字母N和P。这两个符号并不是PN结中正和负的意思。而是:

注意:此处有个例外,即Date类型的对象,它会先调用toString()方法,后调用valueOf()方法。

- 在图1中,标有N或P的线表示:当它连接的两种类型的数据做==运算时,标有N或P的那一边的操作数要先执行ToNumber或ToPrimitive变换。<br><br>

### 三、真与假
从图1可以看出,当布尔值与其他类型的值作比较时,布尔值会转化为数字,具体来说

true -> 1 false -> 0

这一点也不需浪费过多口舌。想一下在C语言中,根本没有布尔类型,通常用来表示逻辑真假的正是整数1和0。<br><br>

### 四、字符的序列
在图1中,我们把String和Number类型分成了一组。为什么呢?在六种类型中,String和Number都是字符的序列(至少在字面上如此)。字符串是所有合法的字符的序列,而数字可以看成是符合特定条件的字符的序列。所以,数字可以看成字符串的一个子集。<br>

根据图1,在字符串和数字做==运算时,需要使用ToNumber操作,把字符串转化为数字。假设x是字符串,y是数字,那么:

x == y -> Number(x) == y

那么字符串转化为数字的规则是怎样的呢?规范中描述得很复杂,但是大致说来,就是把字符串两边的空白字符去掉,然后把两边的引号去掉,看它能否组成一个合法的数字。如果是,转化结果就是这个数字;否则,结果是NaN。例如:

Number('123') // 结果123 Number('1.2e3') // 结果1200 Number('123abc') // 结果NaN Number('\r\n\t123\v\f') // 结果123

当然也有例外,比如空白字符串转化为数字的结果是0。即

Number('') // 结果0 Number('\r\n\t \v\f') // 结果0


### 五、单纯与复杂
原始类型是一种单纯的类型,它们直接了当、容易理解。然而缺点是表达能力有限,难以扩展,所以就有了对象。对象是属性的集合,而属性本身又可以是对象。所以对象可以被构造得任意复杂,足以表示各种各样的事物。<br>

但是,有时候事情复杂了也不是好事。比如一篇冗长的论文,并不是每个人都有时间、有耐心或有必要从头到尾读一遍,通常只了解其中心思想就够了。于是论文就有了关键字、概述。JavaScript中的对象也一样,我们需要有一种手段了解它的主要特征,于是对象就有了toString()和valueOf()方法。

toString()方法用来得到对象的一段文字描述;而valueOf()方法用来得到对象的特征值。


当然,这只是我自己的理解。顾名思义,toString()方法倾向于返回一个字符串。那么valueOf()方法呢?根据[规范中的描述](http://es5.github.io/#x9.1),它倾向于返回一个数字——尽管内置类型中,valueOf()方法返回数字的只有Number和Date<br>

根据图1,当一个对象与一个非对象比较时,需要将对象转化为原始类型(虽然与布尔类型比较时,需要先将布尔类型变成数字类型,但是接下来还是要将对象类型变成原始类型)。这也是合理的,毕竟==是不严格的相等比较,我们只需要取出对象的主要特征来参与运算,次要特征放在一边就行了。<br><br>

### 六、万物皆数
我们回过头来看一下图1。里面标有N或P的那几条连线是没有方向的。假如我们在这些线上标上箭头,使得连线从标有N或P的那一端指向另一端,那么会得到(不考虑undefined和null),看下图2
![image](https://user-images.githubusercontent.com/20301892/44207118-8a266480-a18e-11e8-94d9-12fc6c24a8d1.png)

发现什么了吗?对,在运算过程中,所有类型的值都有一种向数字类型转化的趋势。毕竟曾经有名言曰:**万物皆数。** <br><br>

### 七、举个栗子
前面废话太多了,这里还是举个例子,来证明图1确实是方便有效可以指导实践的。例,计算下面表达式的值:

[' '] == false

首先,两个操作数分别是对象类型、布尔类型。根据图1,需要将布尔类型转为数字类型,而false转为数字的结果是0,所以表达式变为:

[''] == 0

两个操作数变成了对象类型、数字类型。根据图1,需要将对象类型转为原始类型:
- 首先调用[].valueOf(),由于数组的valueOf()方法返回自身,所以结果不是原始类型,继续调用[].toString()。
- 对于数组来说,toString()方法的算法,是将每个元素都转为字符串类型,然后用逗号','依次连接起来,所以最终结果是空字符串'',它是一个原始类型的值。

此时,表达式变为:

' ' == 0

两个操作数变成了字符串类型、数字类型。根据图1,需要将字符串类型转为数字类型,前面说了空字符串变成数字是0。于是表达式变为:

0 == 0

到此为止,两个操作数的类型终于相同了,结果明显是true。从这个例子可以看出,要想掌握==运算的规则,除了牢记图1外,还需要记住那些内置对象的toString()和valueOf()方法的规则。包括Object、Array、Date、Number、String、Boolean等,幸好这没有什么难度。<br><br>

### 八、再次变形
其实,图一还不够完美。为什么呢?因为对象与字符串/数字比较时都由对象来转型,但是与同样是原始类型的布尔类型比较时却需要布尔类型转型。实际上,只要稍稍分析一下,全部让对象来转为原始类型也是等价的。所以我们得到了最终的更加完美的图形:
![image](https://user-images.githubusercontent.com/20301892/44207290-39633b80-a18f-11e8-96bf-fb87646f76b9.png)
有一个地方可能让你疑惑:为什么Boolean与String之间标了两个N?虽然按照规则应该是由Boolean转为数字,但是下一步String就要转为数字了,所以干脆不如两边同时转成数字。<br><br>

### 九、总结一下
前面说得很乱,根据我们得到的最终的图3,我们总结一下==运算的规则:
- undefined == null,结果是true。且它俩与所有其他值比较的结果都是false。
- String == Boolean,需要两个操作数同时转为Number。
- String/Boolean == Number,需要String/Boolean转为Number。
- Object == Primitive,需要Object转为Primitive(具体通过valueOf和toString方法)。

瞧见没有,一共只有4条规则!是不是很清晰、很简单。<br><br>

### 十、小练习
为了更好的巩固一下以上所学,我们一起来分析以下的代码结果
```js
1、[]==[]

2、[]==![]

3、{}==!{}

4、{}==![]

5、![]=={}

6、[]==!{}

看了上面的题目,不知道你们有何感想?是不是觉得自己之前的内容没有看懂- -,下面通过分析上面的习题,来更加充分理解之前的内容。

题目1: []==[]为false

因为左边的[]和右边的[]看起来长的一样,但是他们引用的地址并不同,
这个是同一类型的比较,并且除了“==”没有其他运算符,所以相对没那么麻烦。

题目2: []==![]为true

!的优先级较==高,先运算==右侧的操作数:[]是对象,
会转换成true,然后因为!再转成false(加!的一定是转换成boolean);

== 的左操作数是[],数组(对象除了日期对象,都是对象到数字的转换),
碰到==要先调用自己的valueOf()方法=>[](还是本身),
然后调用自己的toString()方法=>空字符串=>false 
(或者空字符串转成0,然后再转成false,但是终归会是false)

false==false,因此结果为true。

题目3: {}==!{}为false

和题目2的分析过程类似,先计算右边结果为false;然后再通过
valueOf()方法=>{}=>toString()方法=>object=>true,得到左边结果为true。

关于valueOf()方法和toString()方法的调用顺序和作用再说几句,下面来看两者的执行顺序这块

如果preferredType为Object,即:
1.调用 obj.valueOf(),如果执行结果是原始值,返回之;
2. 否则调用 obj.toString(),如果执行结果是原始值,返回之;
3. 否则抛异常。

如果preferredType为String,即:
1. 否调用 obj.toString(),如果执行结果是原始值,返回之;
2. 否则调用 obj.valueOf(),如果执行结果是原始值,返回之;
3. 否则抛异常。

下面再来看看两者的作用

// toString()用来返回对象的字符串表示
var obj = {};
console.log(obj.toString());//[object Object]
var arr2 = [];
console.log(arr2.toString());//""空字符串
var date = new Date();
console.log(date.toString());//Sun Feb 28 2016 13:40:36 GMT+0800 (中国标准时间)

// valueOf方法返回对象的原始值,可能是字符串、数值或bool值等,看具体的对象
var obj = {
  name: "obj"
};
console.log(obj.valueOf());//Object {name: "obj"}
var arr1 = [1];
console.log(arr1.valueOf());//[1]
var date = new Date();
console.log(date.valueOf());//1456638436303

题目4: {}==![]为false

相比到了这里大家都知道怎么来计算了,
右边结果为false(原因看题2),左边结果
为true(原因看题3)。所以最终结果为false。

题目5: ![]=={}为fasle 题目6: []==!{}为true

题5和题6相信大家根据前面几道题的分析都能够正确的将它解答出来,因此就不细说了。看到这里,相信你们都搞明白这些让人迷糊的自动类型转换方式了吧。

stone1314 commented 5 years ago

{}==!{}为false 和题目2的分析过程类似,先计算右边结果为false;然后再通过 valueOf()方法=>{}=>toString()方法=>object=>true,得到左边结果为true。

很不明白左边为什么是true?