众所周知,==操作是JavaScript中比较复杂的一个运算符。 它的运算规则很奇怪,让人防不胜防。

比较操作符会为两个不同类型的操作数转换类型,然后进行严格比较。当两个操作数都是对象时,JavaScript会比较其内部引用,当且仅当他们的引用指向内存中的相同对象(区域)时才相等,即他们在栈内存中的引用地址相同。

==运算的精确描述在这里:The Abstract Equality Comparison Algorithm
这里将这些规则描述成更加通俗易懂的形式,搞清这些规则之前我们先来看一些基础知识:
大家都知道JavaScript是一种弱类型语言。这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。

JS中的值类型有两种类型: 原始类型(Primitive)、对象类型(Object)
最新的 ECMAScript 标准定义了 8 种数据类型:


面向对象语言通常认为,’一切皆对象’,js为了实现这个目标,在类型系统上做出了一些妥协。
为部分基础类型系中的值类型设定对应的包装类

等值监测中的相等问题

在三种类型(数值、布尔值和字符串)中,如果两个被比较的值类型不同,那么:
1.有任何一个是数字时,会将另外一个转换为数字进行比较
2.有任何一个是布尔值时,它会被转为数字进行比较(由于第一条规则的存在,所以另一个数据也将被转为数字)
3.有任何一个是对象(包含函数)时,将调用该对象的valueOf()方法来将其转换为值数据进行比较,且在多数情况下该值数据作为数字值处理

tips: 对象转为值类型的过程有点复杂,后面细说

对象转值类型

对象到字符串和对象到数字的转换都是通过调用待转换对象的一个方法来完成的。而 JavaScript 对象有两个不同的方法来执行转换,一个是 toString,一个是 valueOf。注意这个跟上面所说的 ToStringToNumber 是不同的,这两个方法是真实暴露出来的方法。

所有的对象除了 null 和 undefined 之外的任何值都具有 toString 方法,通常情况下,它和使用 String 方法返回的结果一致。toString 方法的作用在于返回一个反映这个对象的字符串,然而这才是情况复杂的开始。

Object.prototype.toString
https://es5.github.io/#x15.2.4.2
当 toString 方法被调用的时候,下面的步骤会被执行:

  1. 如果 this 值是 undefined,就返回 [object Undefined]
  2. 如果 this 的值是 null,就返回 [object Null]
  3. 让 O 成为 ToObject(this) 的结果
  4. 让 class 成为 O 的内部属性 [[Class]] 的值
  5. 最后返回由 “[object “ 和 class 和 “]” 三个部分组成的字符串

通过规范,我们至少知道了调用 Object.prototype.toString 会返回一个由 “[object “ 和 class 和 “]” 组成的字符串,而 class 是要判断的对象的内部属性。

Object.prototype.toString 方法会根据这个对象的[[class]]内部属性,返回由 “[object “ 和 class 和 “]” 三个部分组成的字符串。举个例子:

  1. Object.prototype.toString.call({a: 1}) // "[object Object]"
  2. ({a: 1}).toString() // "[object Object]"
  3. ({a: 1}).toString === Object.prototype.toString // true

可以看出当调用对象的 toString 方法时,其实调用的是 Object.prototype 上的 toString 方法。
然而 JavaScript 下的很多类根据各自的特点,定义了更多版本的 toString 方法。例如:

  1. 数组的 toString 方法将每个数组元素转换成一个字符串,并在元素之间添加逗号后合并成结果字符串。
  2. 函数的 toString 方法返回源代码字符串。
  3. 日期的 toString 方法返回一个可读的日期和时间字符串。
  4. RegExp 的 toString 方法返回一个表示正则表达式直接量的字符串。

而另一个转换对象的函数是 valueOf,表示对象的原始值。默认的 valueOf 方法返回这个对象本身,数组、函数、正则简单的继承了这个默认方法,也会返回对象本身。日期是一个例外,它会返回它的一个内容表示: 1970 年 1 月 1 日以来的毫秒数。

对象到值的隐式转换规则
如果对象试图转为字符串,则先尝试该对象的toString()方法,然后再尝试valueOf()方法,否则(转数字)先尝试调用valueOf()方法,再尝试调用toString()。

这两个方法都既可能返回预期的值,也可能返回非值对象。
比如重写某一个方法。 如果toString或者valueOf返回非值的对象,则视为值无效,并按上述规则调用另一个方法。如果两个方法都返回无效结果值,则抛出异常。

  1. let a = new String('1');
  2. a.toString = function(){return {};}
  3. a.valueOf = function(){return {};}
  4. console.log(+a); //Uncaught TypeError: Cannot convert object to primitive value

在ecma中,null值被认为是这样的非对象原始值,因此它可以作为toString()、valueOf()的返回值并进入后续的值运算,不会报异常。

  1. let a = new String('1');
  2. a.toString = function(){return null;}
  3. console.log(+a); //0

直接的值运算不受包装类的方法影响
值运算时候并不需要”对象=>值”的转换,因此也就不需要隐式地调用这些包装类的方法。也就是

  1. 1+"2"

转换的预期

什么才是转换的预期。 由于valueOf和toString的调用顺序与转换的预期有关,所以这个话题指的一谈。
并没有文档来描述当操作数或参数与运算符活函数界面上的设计不一致时,js内核对这个值设定为哪种预期,并用该预期来绝盾构上述调用顺序。

一些情况下显而易见:
如parseInt(x)

  1. 按照特定规则返回比较结果,例如,undefined与null总是相等的。

javascript总是尽量用数字值比较来实现等值检测,主要原因是因为js内部的数据格式适合这一操作,同样的原因,字符串检测通常会存在非常大的开销,严格来说,必须对字符串中的每一个字符进行比较,才能判断字符串是否相等。
tips: 在现实中未必是这样,脚本引擎通常有能力对运算结果做优化,使它们在引擎内部指向同一个字符串(地址引用),因而不必总是如此比较,

  1. // 下面的比较结果是:true
  2. new Number(10) == 10; // Number.toString() 返回的字符串被再次转换为数字
  3. 10 == '10'; // 字符串被转换为数字
  4. 10 == '+10 '; // 同上
  5. 10 == '010'; // 同上
  6. isNaN(null) == false; // null 被转换为数字 0
  7. // 0 当然不是一个 NaN(译者注:否定之否定)
  8. // 下面的比较结果是:false
  9. 10 == 010;
  10. 10 == '-10';

内置类型的构造函数

内置类型(比如 NumberString)的构造函数在被调用时,使用或者不使用 new 的结果完全不同。

  1. new Number(10) === 10; // False, 对象与数字的比较
  2. Number(10) === 10; // True, 数字与数字的比较
  3. new Number(10) + 0 === 10; // True, 由于隐式的类型转换

使用内置类型 Number 作为构造函数将会创建一个新的 Number 对象, 而在不使用 new 关键字的 Number 函数更像是一个数字转换器。
另外,在比较中引入对象的字面值将会导致更加复杂的强制类型转换。
最好的选择是把要比较的值显式的转换为三种可能的类型之一。

转换为字符串

  1. '' + 10 === '10'; // true

将一个值加上空字符串可以轻松转换为字符串类型。

转换为数字

  1. +'10' === 10; // true

使用一元的加号操作符,可以把字符串转换为数字。

转换为布尔型

通过使用 操作符两次,可以把一个值转换为布尔型。

  1. !!'foo'; // true
  2. !!''; // false
  3. !!'0'; // true
  4. !!'1'; // true
  5. !!'-1' // true
  6. !!{}; // true
  7. !!true; // true

逻辑运算符||和&&

它们的返回值是两个操作数中的一个(且仅一个)
根据es5规范11.11节,&&和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个值

  1. let a = 1;
  2. let b = 'a';
  3. let c = null;
  4. a||b //1
  5. a&&b // 'a'
  6. c||b //'a'
  7. c&&b //null

||和&& 首先会对第一个操作数执行条件判断,如果不是布尔值,就进行ToBoolean强制类型转换,然后在执行条件判断。

有一个坑常常被提起,[]+{} 和 {}+[], 他们返回不同的结果,分别是”[Object Object]”和0

安全运用隐式强制类型转换

在使用过程中要对==两边的值进行认真的推敲,以下两个原则可以避免出错。
如果两边的值中有true或者false, 千万不要使用==
如果两边的值中有[]、””、0,尽量不要使用==

这时最好使用===来避免不经意的强制转换。