你确定你知道 == 和 === 的区别吗?

1,492 阅读9分钟

前言

  • 在学习本章内容前,你需要一些前置知识,你可以移步到此专栏 你学了也不懂的JavaScript
  • 通过本篇文章的学习你将学习到 ===== 操作符的区别,以及如何让 a == 2 && a== 3true
  • 在平时的面试中,我们可能会遇到一个问题,就是 ===== 有啥区别,下面我们就来模拟一下场景:
  1. 面试官: 你知道宽松相等和严格相等有什么区别吗?
  2. 我的内心: 很好,这我知道,难不倒我,到我开始表现的时候了😊
  3. 我:这很简单啊,==不就是检查值是否相等嘛,===检查值和类型是否相等嘛;
  4. 面试官: ...... 你先回去等通知吧👋👋👋
  5. 此时门外一个人敲门进来了,我来我来,这个我会;
  6. == 允许在相等比较中进行强制类型转换,而 === 不允许;
  7. 面试官: 很好,你面试通过了,这边呢,工资给你开到3000...
  • 好了,水吹完了,那么接下来我们就开始去讲解一下 == 是怎么在比较中进行强制类型转化的,以及是怎么样的转换规则,通过这篇文章你将会学到很多奇奇怪怪的知识

性能

  • == 似乎比 === 做的事情更多,因为它还要对值的类型进行强制类型转换,但是实际上虽然强制类型转换确实要多花点时间,但仅仅是微秒级(百万分之一秒)的差别而已。
  • 如果两个值的类型不同,我们就需要考虑有没有强制类型转换的必要,有就用 ==,没有就用 ===,冉在乎性能。

抽象相等

  • ES5规范11.9.3 节的 抽象相等比较算法 定义了 == 运算符的行为,在使用 == 比较的时候,当使用 == 对x和y进行比较时,会返回 true或者 false的值,它们主要遵循以下的规则:
  1. 如果 xy 的类型相同:
    1. 如果 xundefined,返回 true;
    2. 如果 xnull,返回 true;
    3. 如果 xNumber 类型,则:
      1. 如果 xNaN,则返回 false;
      2. 如果 yNaN,则返回 false;
      3. 如果 xy 相同,则返回 true;
      4. 如果 x-0y+0,则返回 true;
      5. 如果 y-0x+0,则返回 true;
      6. 其他情况返回 false;
    4. 如果 xstring 类型,并且 xy 的完全相同的值(长度相同,对应位置的字符相同),则返回 true;
    5. 如果 xBoolean 类型,并且 xy 都是 true 或者 false,则返回 true,否则返回 false;
    6. 如果 xy 引用同一个对象,则返回 true,否则返回 false;
  2. 如果 xnullyundefined,则返回 true;
  3. 如果 ynullxundefined,则返回 true;
  4. 如果 xnumber 类型且 ystring 类型,则返回 x == ToNumber(y)的比较结果;
  5. 如果 ynumber 类型且 xstring 类型,则返回 y == ToNumber(x)的比较结果;
  6. 如果 xboolean 类型,返回 ToNumber(x) == y的比较结果;
  7. 如果 yboolean 类型,返回 ToNumber(y) == x的比较结果;
  8. 如果 xstring 类型或者 number 类型,并且 yobject 类型,返回 x == ToPrimitive(y) 的比较结果;
  9. 如果 ystring 类型或者 number 类型,并且 xobject 类型,返回 y == ToPrimitive(x) 的比较结果;
  10. 否则返回 false;
  • 抽象相等的这些规则正是隐式强制类型转换不受人喜爱的原因,但是认真一看规则,其实简单明了。

字符串和数字之间的相等比较

const a = 77;
const b = "77";

console.log(a === b); // false
console.log(a == b); // true
  • 因为没强制类型转换,所以 a === bfalse,77"77" 不相等。
  • a == b是宽松相等,即如果两个值的类型不同,则对其中之一或者两者都进行强制类型转换,具体怎么转换的,请看定义,它们是这样的:
  1. 如果 xnumber 类型且 ystring 类型,则返回 x == ToNumber(y)的比较结果;
  2. 如果 ynumber 类型且 xstring 类型,则返回 y == ToNumber(x)的比较结果;
  • 也就是说, a == b 在代码中实际上是这样的行为:
const a = 77;
const b = "77";

console.log(a === Number(b)); // true

其他类型和布尔值之间的相等比较

  • ==最容易出错的是一个地方是 truefalse于其他类型之间的相等比较,例如:
const a = "77";
const b = true;

console.log(a == b); // false
  • 我们都知道 "77" 是一个真值,为什么 == 的结果不是 true呢?因为规范是这样定义的:
  1. 如果 xboolean 类型,返回 ToNumber(x) == y的比较结果;
  2. 如果 yboolean 类型,返回 ToNumber(y) == x的比较结果;
  • 首先 bboolean 类型,所以 ToNumber(b)b 的类型强制转换为 1,变成 1 == '77',二者的类型仍然不同,"77" 根据规则被强制类型转换为 77,最后变成 1 == 77,所以结果输出为 false;

null 和 undefined 之间的相等比较

  • nullundefined 之间的 == 也设计隐式强制类型转换,ES5规范 是这样规定的:
  1. 如果 xnullyundefined,则返回 true;
  2. 如果 ynullxundefined,则返回 true;
  • ==nullundefined 相等(它们也与其自身相等),除此之外其他值都不和它们两个相等。
  • 这也就是说,在==nullundefined 是一回事,可以相互进行隐式强制类型转换:
var a = null;
var b = undefined;

console.log(a == b); // true
console.log(a == null); // true
console.log(b == null); // true

console.log(a == false); // false
console.log(b == false); // false
console.log(a == ""); // false
console.log(b == ""); // false
console.log(a == 0); // false
console.log(b == 0); // false
  • null he undefined 之间的强制类型转换是安全可靠的,上例中除 nullundefined 以外的其他值均为无法返回 true 的结果。

对象和非对象之间的相等比较

  • 对于对象和基本类型之间的相等比较,ES5规范是遵循这样的规则:
  1. 如果 xstring 类型或者 number 类型,并且 yobject 类型,返回 x == ToPrimitive(y) 的比较结果;
  2. 如果 ystring 类型或者 number 类型,并且 xobject 类型,返回 y == ToPrimitive(x) 的比较结果;
  • 例如:
var a = 77;
var b = [77];

console.log(a == b); // true
  • [77] 首先调用了 ToPrimitive 抽象操作,返回 "77",变成 "77" == 77,然后又变成 77 == 77,最后二者相等。其中代码的转变过程是以下的形式:
var a = 77;
var b = [77];

console.log(a === Number(b.toString())); // true
  • 我们再来看一个 string 类型和 object 类型的例子:
var a = "abc";
var b = Object(a);

console.log(a === b); // false
console.log(a == b); // b
  • a == b 结果为 true,因为 b 通过 TOPrimitive 进行强制类型转换(拆封),并返回基本数据类型值 "abc",与 a 相等。

  • 但是规则总有特例,原因是 == 算法中其他优先级更高的原则。例如:

var foo = null;
var bar = Object(foo);

console.log(foo == bar); // false

var a = undefined;
var b = Object(a);

console.log(a == b); // false

var c = NaN;
var d = Object(NaN);

console.log(c == d); // false
  • 因为没有对应的封装对象,所以 nullundefined 不能够被封装和 Object() 均返回一个空对象,你也可以理解成空对象调用 toString() 方法返回的值是 "[object Object]",不等于 nullundefined
  • NaN能够被封装为数字封装对象,但拆封之后 NaN == NaN 返回false,因为 NaN 不等于 NaN

希望你永远不会用到

  • 在上面的讲解中,我们已经介绍了 == 中的隐式强制类型转换,现在来看一下那些需要特别注意和避免的比较少见的情况。

返回其他的数字

  • 首先来看看更改内置原生模型会导致哪些奇怪的结果:
Number.prototype.valueOf = function () {
  return 3;
};

console.log(new Number(77) == 3); // true
  • 2 == 3 不会有这样的问题,因为 23 都是数字基本类型值,不会调用 Number.prototype.valueOf() 方法。而 Number(2) 涉及 ToPrimitive 强制类型转换,因此会调用 valueOf()
  • 再来看一种情况,这个你可能面试题里经常看到,a == 2 && a == 5 在什么情况下为 true?
if (a == 2 && a == 3) {
  // ...
}
  • 你也许觉得不可能,因为 a 不会同时等于 23 。但 同时似乎说的不对,因为 a == 2a == 3 之前执行。如果让 a..valueOf() 每次调用都产生副作用,比如第一次返回 2,第二次返回 3,就会出现这样的情况。这实现起来很简单:
const a = {
  value: 1,
};

a.valueOf = function () {
  return this.value++;
};

if (a == 1 && a == 2 && a == 3) {
  console.log("嗨,没想到吧,意不意外,惊不惊喜"); // 这里正常输出了
}

假值的相等比较

  • ==中的隐式强制类型转换最为让人不满的地方是假值的相等比较下面分别列出了常规和非常规的情况:
console.log("0" == null); // false
console.log("0" == undefined); // false
console.log("0" == false); // true
console.log("0" == NaN); // false
console.log("0" == 0); // true
console.log("0" == ""); // false

console.log(false == null); // false
console.log(false == undefined); // false
console.log(false == NaN); // false
console.log(false == 0); // true
console.log(false == ""); // true
console.log(false == []); // true
console.log(false == {}); // false

console.log("" == null); // false
console.log("" == undefined); // false
console.log("" == NaN); // false
console.log("" == 0); // true
console.log("" == []); // true
console.log("" == {}); // false

console.log(0 == null); // false
console.log(0 == undefined); // false
console.log(0 == NaN); // false
console.log(0 == []); // true
console.log(0 == {}); // false
  • 接下来我们挑一些有代表性的例子来具体讲解一下:
  1. 其中这些例子中最好辨认的是在 == 比较中,NaN 与任何值相比都是 false,NaN也是;
  2. "0" == false 中会对 false 先转换成数字类型,即 0,变成了 "0" == 0,再对 "0" 转换,最终变成了 0 == 0,所以输出为 true;
  3. false == []中,会对 []进行 ToPrimitive 类型转换,转换成原始类型 "",这时候变成了 false == "",所以输出结果为 true,至于为什么是 true,我想你应该懂了吧
  • 接下来还有一些例子:
console.log([] == ![]); // true
console.log("" == [null]); // true
console.log("" == [undefined]); // true
  • 喔喔喔,这是什么,怎么都输出了 true,它们到底都干了啥?

  • 在第一个中, ! 运算符对 [] 做了取反操作,因为 [] 为真值,而 ![] 也就变成了 false了,所以 [] == ![] 变成了 [] == false,前面我们有讲过这个比较的结果,所以最后的结果也很正常了。

  • 在第二、三个例子中,[null].toString()最后返回 "",而难懂的是 String(null) 返回的是 "null",而 String([Null]) 去返回的是 "",这就是难懂的地方,这也是 js 令人深入理解的原因。

  • 所以 ===== 选择哪一个取决于是否允许在相等比较中发生强制类型转换。

  • 好了,本篇的内容讲解到此结束了,有什么不了解的可以在评论区留下你的疑问。

参考文章

  • 书籍 `你不知道的JavaScript.
  • ES5规范