让你彻底搞懂JS中复杂运算符==
让你彻底搞懂JS中复杂运算符==
大家知道,==
是JavaScript中比较复杂的一个运算符。它的运算规则奇怪,容易让人犯错,从而成为JavaScript中“最糟糕的特性”之一。
在仔细阅读了ECMAScript规范的基础上,我画了一张图,我想通过它你会彻底地搞清楚关于==
的一切。同时,我也试图通过此文向大家证明==
并不是那么糟糕的东西,它很容易掌握,甚至看起来很合理。
图1: ==
运算规则的图形化表示
==
运算规则的精确描述在此:The Abstract Equality Comparison Algorithm。但是,这么复杂的描述,你确定看完后脑子不晕?确定立马就能拿它指导实践?
肯定不行,规范毕竟是给JavaScript运行环境的开发人员看的(比如V8引擎的开发人员们),而不是给语言的使用者看的。而上图正是将规范中复杂的描述翻译成了更容易看懂的形式。
在详细介绍图1中的每个部分前,我们来复习一下JavaScript中关于类型的知识:
- JS中的值有两种类型:原始类型(Primitive)、对象类型(Object)。
- 原始类型包括:
Undefined
、Null
、Boolean
、Number
和String
等五种。 Undefined
类型和Null
类型的都只有一个值,即undefined
和null
;Boolean
类型有两个值:true
和false
;Number
类型的值有很多很多;String
类型的值理论上有无数个。- 所有对象都有
valueOf()
和toString()
方法,它们继承自Object
,当然也可能被子类重写。
现在考虑表达式:
x == y
其中x
和y
是上述六种类型中某一种类型的值。
当x
和y
的类型相同时,x == y
可以转化为x === y
,而后者是很简单的(唯一需要注意的可能是NaN
),所以下面我们只考虑x
和y
的类型不同的情况。
有和无
在图1中,JavaScript值的六种类型用蓝底色的矩形表示。它们首先被分成了两组:
String
、Number
、Boolean
和Object
(对应左侧的大矩形框)Undefined
和Null
(对应右侧的矩形框)
分组的依据是什么?我们来看一下,右侧的Undefined
和Null
是用来表示不确定、无或者空的,而右侧的四种类型都是确定的、有和非空。我们可以这样说:
左侧是一个存在的世界,右侧是一个空的世界。
所以,左右两个世界中的任意值做==
比较的结果都是false
是很合理的。(见图1中连接两个矩形的水平线上标的false
)
空和空
JavaScript中的undefined
和null
是另一个经常让我们崩溃的地方。通常它被认为是一个设计缺陷,这一点我们不去深究。不过我曾听说,JavaScript的作者最初是这样想的:
假如你打算把一个变量赋予对象类型的值,但是现在还没有赋值,那么你可以用
null
表示此时的状态(证据之一就是typeof null
的结果是object
);相反,假如你打算把一个变量赋予原始类型的值,但是现在还没有赋值,那么你可以用undefined
表示此时的状态。
不管这个传闻是否可信,它们两者做==
比较的结果是true
是很合理的。(见图1中右侧垂直线上标的true
)
在进行下一步之前,我们先来说一下图1中的两个符号:大写字母N
和P
。这两个符号并不是PN
结中正和负的意思。而是:
N
表示ToNumber操作,即将操作数转为数字。它是规范中的抽象操作,但我们可以用JavaScript中的Number()
函数来等价替代。P
表示ToPrimitive操作,即将操作数转为原始类型的值。它也是规范中的抽象操作,同样也可以翻译成等价的JavaScript代码。不过稍微复杂一些,简单说来,对于一个对象obj
:
ToPrimitive(obj)
等价于:先计算obj.valueOf()
,如果结果为原始值,则返回此结果;否则,计算obj.toString()
,如果结果是原始值,则返回此结果;否则,抛出异常。
注:此处有个例外,即Date
类型的对象,它会先调用toString()
方法,后调用valueOf()
方法。
在图1中,标有N
或P
的线表示:当它连接的两种类型的数据做==
运算时,标有N
或P
的那一边的操作数要先执行ToNumber
或ToPrimitive
变换。
真与假
从图1可以看出,当布尔值与其他类型的值作比较时,布尔值会转化为数字,具体来说
true -> 1 false -> 0
这一点也不需浪费过多口舌。想一下在C语言中,根本没有布尔类型,通常用来表示逻辑真假的正是整数1
和0
。
字符的序列
在图1中,我们把String
和Number
类型分成了一组。为什么呢?在六种类型中,String
和Number
都是字符的序列(至少在字面上如此)。字符串是所有合法的字符的序列,而数字可以看成是符合特定条件的字符的序列。所以,数字可以看成字符串的一个子集。
根据图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()
方法返回数字的只有Number
和Date
。
根据图1,当一个对象与一个非对象比较时,需要将对象转化为原始类型(虽然与布尔类型比较时,需要先将布尔类型变成数字类型,但是接下来还是要将对象类型变成原始类型)。这也是合理的,毕竟==
是不严格的相等比较,我们只需要取出对象的主要特征来参与运算,次要特征放在一边就行了。
万物皆数
我们回过头来看一下图1。里面标有N或P的那几条连线是没有方向的。假如我们在这些线上标上箭头,使得连线从标有N
或P
的那一端指向另一端,那么会得到(不考虑undefined
和null
):
图2: ==
运算过程中类型转化的趋势
发现什么了吗?对,在运算过程中,所有类型的值都有一种向数字类型转化的趋势。毕竟曾经有名言曰:
万物皆数。
举个栗子
前面废话太多了,这里还是举个例子,来证明图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
等,幸好这没有什么难度。
再次变形
其实,图一还不够完美。为什么呢?因为对象与字符串/数字比较时都由对象来转型,但是与同样是原始类型的布尔类型比较时却需要布尔类型转型。实际上,只要稍稍分析一下,全部让对象来转为原始类型也是等价的。所以我们得到了最终的更加完美的图形:
图3: 更完美的==
运算规则的图形化表示
有一个地方可能让你疑惑:为什么Boolean
与String
之间标了两个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中复杂运算符==的更多相关文章
- 帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)
作为一名前端工程师,必须搞懂JS中的prototype.__proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞 ...
- 彻底搞懂 JS 中 this 机制
彻底搞懂 JS 中 this 机制 摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog 目录 this 是什么 this 的四种绑定规 ...
- 一文搞懂 js 中的各种 for 循环的不同之处
一文搞懂 js 中的各种 for 循环的不同之处 See the Pen for...in vs for...of by xgqfrms (@xgqfrms) on CodePen. for &quo ...
- 晨叔技术晨报: 你真的搞懂JS中的“值传递”和“引用传递”吗?
晨叔周刊,每周一话题,技术天天涨. 本周的话题是JS的内存问题(加入本周话题,请点击传送门). 图 话题入口 今天的技术晨报来,就来谈谈JS中变量的,值传递和引用传递的问题.现在,对于很多的JSer来 ...
- 搞懂js中小数运算精度问题原因及解决办法
js小数运算会出现精度问题 js number类型 JS 数字类型只有number类型,number类型相当于其他强类型语言中的double类型(双精度浮点型),不区分浮点型和整数型. number类 ...
- 一文搞懂js中的typeof用法
基础 typeof 运算符是 javascript 的基础知识点,尽管它存在一定的局限性(见下文),但在前端js的实际编码过程中,仍然是使用比较多的类型判断方式. 因此,掌握该运算符的特点,对于写出好 ...
- 来一轮带注释的demo,彻底搞懂javascript中的replace函数
javascript这门语言一直就像一位带着面纱的美女,总是看不清,摸不透,一直专注服务器端,也从来没有特别重视过,直到最近几年,javascript越来越重要,越来越通用.最近和前端走的比较近,借此 ...
- 轻松搞懂Java中的自旋锁
前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...
- js中的运算符和条件语句
js中的运算符大体上可以分为4类:1算术运算符.2一元操作符.3比较运算符.4逻辑运算符. 算术运算符一般指的是加减乘除求余这五种操作符:+,-,*,/,%.通过算术运算符可以对js中的变量进行操作. ...
随机推荐
- Linux学习笔记-基本操作1
1>. 命令解析器2>. Linux快捷键3>. Linux 系统目录结构4>. 用户目录5>. 文件和目录操作6>. 文件和目录的属性7>. 文件权限, 用 ...
- harbor镜像仓库-01-搭建部署
harbor镜像仓库-01-搭建部署 dockerregistryharbor安装部署docker-compose harbor的https配置参考另一章节harbor镜像仓库-02-https访问配 ...
- canvas 实现签名效果
效果图 概述 在线签名,现在在很多场景下都能看到,而且在移动端见的比较多. 用canvas和svg都可以实现,而且跨平台能力也很好. canvas基于像素,提供 2D 绘制函数,提供的功能更原始,适合 ...
- JS脚本实现CSDN免登陆免关闭广告插件自动展开“阅读更多”内容
最近在CSDN查资料,总是弹出以下弹窗,然后就自动跳转到登录页面,蛋疼! 于是重新捣腾了一下,修改了原来的脚本,最新的脚本代码如下: 温馨提示:在打开CSDN页面后立刻执行以下脚本即可免登陆免关闭广告 ...
- Ubuntu14.04 + Text-Detection-with-FRCN(CPU)
操作系统: yt@yt-MS-:~$ cat /etc/issue Ubuntu LTS \n \l Python版本: yt@yt-MS-:~$ python --version Python pi ...
- 10-02 Java 形式参数和返回值的问题深入研究,链式编程
形式参数和返回值的问题: 1:形式参数和返回值的问题(理解) (1)形式参数: 类名:需要该类的对象 抽象类名:需要该类的子类对象 接口名:需要该接口的实现类对象 (2)返回值类型: 类名:返回的是该 ...
- android初探
随着nodejs的不断发展,前端的范围越来越大,所以,适当的了解移动端是非常有必要的,比如使用RN开发app,前端必须要和安卓工程师沟通共同开发,那么学习android的基本知识就很重要了,因为目前安 ...
- 理解web service 和 SOA
什么是SOA? SOA的全称为Service Oriented Architecture,即面向服务架构.这是一种架构理念.它的提出是在企业计算领域将耦合的系统划分为松耦合的无状态的服务.服务发布出来 ...
- #ifdef、#ifndef、#else、#endif执行条件编译
我们开发的程序不只在pc端运行,也要在移动端运行.这时程序就要根据机器的环境来执行选择性的编译,如对PC端编译PC端的程序,对移动端编译移动端的程序,这里我们就可以用两组条件编译. ...
- ES6箭头函数this指向
普通函数中的this: 1. this总是代表它的直接调用者(js的this是执行上下文), 例如 obj.func ,那么func中的this就是obj 2.在默认情况(非严格模式下,未使用 'us ...