让你彻底搞懂JS中复杂运算符==

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

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

图1: ==运算规则的图形化表示

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

肯定不行,规范毕竟是给JavaScript运行环境的开发人员看的(比如V8引擎的开发人员们),而不是给语言的使用者看的。而上图正是将规范中复杂的描述翻译成了更容易看懂的形式。

在详细介绍图1中的每个部分前,我们来复习一下JavaScript中关于类型的知识:

  • JS中的值有两种类型:原始类型(Primitive)、对象类型(Object)。
  • 原始类型包括:UndefinedNullBooleanNumberString等五种。
  • Undefined类型和Null类型的都只有一个值,即undefinednullBoolean类型有两个值:truefalseNumber类型的值有很多很多;String类型的值理论上有无数个。
  • 所有对象都有valueOf()toString()方法,它们继承自Object,当然也可能被子类重写。

现在考虑表达式:

x == y 

其中xy是上述六种类型中某一种类型的值。

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

有和无

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

  • StringNumberBooleanObject (对应左侧的大矩形框)
  • UndefinedNull (对应右侧的矩形框)

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

左侧是一个存在的世界,右侧是一个的世界。

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

空和空

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

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

不管这个传闻是否可信,它们两者做==比较的结果是true是很合理的。(见图1中右侧垂直线上标的true)

在进行下一步之前,我们先来说一下图1中的两个符号:大写字母NP。这两个符号并不是PN结中正和负的意思。而是:

  • N表示ToNumber操作,即将操作数转为数字。它是规范中的抽象操作,但我们可以用JavaScript中的Number()函数来等价替代。
  • P表示ToPrimitive操作,即将操作数转为原始类型的值。它也是规范中的抽象操作,同样也可以翻译成等价的JavaScript代码。不过稍微复杂一些,简单说来,对于一个对象obj

ToPrimitive(obj)等价于:先计算obj.valueOf(),如果结果为原始值,则返回此结果;否则,计算obj.toString(),如果结果是原始值,则返回此结果;否则,抛出异常。

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

在图1中,标有NP的线表示:当它连接的两种类型的数据做==运算时,标有NP的那一边的操作数要先执行ToNumberToPrimitive变换。

真与假

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

true -> 1 false -> 0 

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

字符的序列

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

根据图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 

单纯与复杂

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

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

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

当然,这只是我自己的理解。顾名思义,toString()方法倾向于返回一个字符串。那么valueOf()方法呢?根据规范中的描述,它倾向于返回一个数字——尽管内置类型中,valueOf()方法返回数字的只有NumberDate

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

万物皆数

我们回过头来看一下图1。里面标有N或P的那几条连线是没有方向的。假如我们在这些线上标上箭头,使得连线从标有NP的那一端指向另一端,那么会得到(不考虑undefinednull):

图2: ==运算过程中类型转化的趋势

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

万物皆数。

举个栗子

前面废话太多了,这里还是举个例子,来证明图1确实是方便有效可以指导实践的。

例,计算下面表达式的值:

[''] == false 

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

[''] == 0 

两个操作数变成了对象类型、数字类型。根据图1,需要将对象类型转为原始类型:

  • 首先调用[].valueOf(),由于数组的valueOf()方法返回自身,所以结果不是原始类型,继续调用[].toString()
  • 对于数组来说,toString()方法的算法,是将每个元素都转为字符串类型,然后用逗号,依次连接起来,所以最终结果是空字符串'',它是一个原始类型的值。

此时,表达式变为:

'' == 0 

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

0 == 0 

到此为止,两个操作数的类型终于相同了,结果明显是true

从这个例子可以看出,要想掌握==运算的规则,除了牢记图1外,还需要记住那些内置对象的toString()valueOf()方法的规则。包括ObjectArrayDateNumberStringBoolean等,幸好这没有什么难度。

再次变形

其实,图一还不够完美。为什么呢?因为对象与字符串/数字比较时都由对象来转型,但是与同样是原始类型的布尔类型比较时却需要布尔类型转型。实际上,只要稍稍分析一下,全部让对象来转为原始类型也是等价的。所以我们得到了最终的更加完美的图形:

图3: 更完美的==运算规则的图形化表示

有一个地方可能让你疑惑:为什么BooleanString之间标了两个N?虽然按照规则应该是由Boolean转为数字,但是下一步String就要转为数字了,所以干脆不如两边同时转成数字。

总结一下

前面说得很乱,根据我们得到的最终的图3,我们总结一下==运算的规则:

  • undefined == null,结果是true。且它俩与所有其他值比较的结果都是false
  • String == Boolean,需要两个操作数同时转为Number
  • String/Boolean == Number,需要String/Boolean转为Number
  • Object == Primitive,需要Object转为Primitive(具体通过valueOf()toString()方法)。

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

最后,我需要@一下Belleve大神,为什么呢?因为整篇文章的思考,都是在看到他在《Javascript 中 == 和 === 区别是什么?》中的回答后做出的。当时他贴了一张图:

我看后觉得太复杂了,于是想能不能用一种更简单的方式来描述一下==运算,使大家更清晰更容易掌握。于是就有了此文,当然我不知道自己成功了没有。

让你彻底搞懂JS中复杂运算符==的更多相关文章

  1. 帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)

    作为一名前端工程师,必须搞懂JS中的prototype.__proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞 ...

  2. 彻底搞懂 JS 中 this 机制

    彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...

  3. 一文搞懂 js 中的各种 for 循环的不同之处

    一文搞懂 js 中的各种 for 循环的不同之处 See the Pen for...in vs for...of by xgqfrms (@xgqfrms) on CodePen. for &quo ...

  4. 晨叔技术晨报: 你真的搞懂JS中的“值传递”和“引用传递”吗?

    晨叔周刊,每周一话题,技术天天涨. 本周的话题是JS的内存问题(加入本周话题,请点击传送门). 图 话题入口 今天的技术晨报来,就来谈谈JS中变量的,值传递和引用传递的问题.现在,对于很多的JSer来 ...

  5. 搞懂js中小数运算精度问题原因及解决办法

    js小数运算会出现精度问题 js number类型 JS 数字类型只有number类型,number类型相当于其他强类型语言中的double类型(双精度浮点型),不区分浮点型和整数型. number类 ...

  6. 一文搞懂js中的typeof用法

    基础 typeof 运算符是 javascript 的基础知识点,尽管它存在一定的局限性(见下文),但在前端js的实际编码过程中,仍然是使用比较多的类型判断方式. 因此,掌握该运算符的特点,对于写出好 ...

  7. 来一轮带注释的demo,彻底搞懂javascript中的replace函数

    javascript这门语言一直就像一位带着面纱的美女,总是看不清,摸不透,一直专注服务器端,也从来没有特别重视过,直到最近几年,javascript越来越重要,越来越通用.最近和前端走的比较近,借此 ...

  8. 轻松搞懂Java中的自旋锁

    前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...

  9. js中的运算符和条件语句

    js中的运算符大体上可以分为4类:1算术运算符.2一元操作符.3比较运算符.4逻辑运算符. 算术运算符一般指的是加减乘除求余这五种操作符:+,-,*,/,%.通过算术运算符可以对js中的变量进行操作. ...

随机推荐

  1. execution表达式--小坑

    之前使用的都是eclipse,今天开始复习spring了,开始接触IDEA,在复习过程中,出了点小事, 复习到AOP开发时,有半自动和全自动之说,之前我看那期视频里面没讲过全自动,我就自己再那敲Dem ...

  2. AWD比赛常规套路

    作者:Leafer   本文属于Arctic shell原创内容计划文章,转载请注明原文地址! 比赛环境:纯净win10,最新版kali,securecrt或者WinSCP 在进入服务器后应进行的常规 ...

  3. mongodb初始化并使用node.js实现mongodb操作封装

    mongodb的下载只要在https://www.mongodb.com/网站就能够下载 下载后安装只用一直点next就可以,注意最好使用默认路径安装到C盘,然后在任意位置建立一个文件夹用于储存你的数 ...

  4. Vue2.5开发去哪儿网App 首页开发

    主页划 5 个组件,即 header  icon  swiper recommend weekend 一. header区域开发 1. 安装 stylus npm install stylus --s ...

  5. 课程一(Neural Networks and Deep Learning),第一周(Introduction to Deep Learning)—— 0、学习目标

    1. Understand the major trends driving the rise of deep learning.2. Be able to explain how deep lear ...

  6. 剑指offer十九之顺时针打印矩阵

    一.题目 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2, ...

  7. Android内存管理篇 - 从updateOomAdjLocked看lowmemorykiller之外的Android进程回收机制

    提起android的进程回收机制,大家所熟知的是Android的lowmemroykiller的机制.当系统可用内存低于某个阀值时,即会杀死这个阀值对应的Adj值的所有应用.但是本篇文章并为是要介绍L ...

  8. 牛客网Java刷题知识点float数据在内存中是怎么存储的

    不多说,直接上干货! float类型数字在计算机中用4个字节存储. 遵循IEEE-754格式标准: 一个浮点数有2部分组成:底数m和指数e (1)底数部分 使用二进制数来表示此浮点数的实际值 (2)指 ...

  9. POJ2418 Hardwood Species—二叉查找树应用

    1. Hardwood Species原题描述   Time Limit: 10000MS   Memory Limit: 65536K Total Submissions: 14326   Acce ...

  10. MYSQL用户权限管理(Grant,Revoke)

    MySQL可以为不同的用户分配严格的.复杂的权限.这些操作大多都可以用SQL指令Grant(分配权限)和Revoke(回收权限)来实现. Grant可以把指定的权限分配给特定的用户,如果这个用户不存在 ...