如何正确判断相等性
ES2015 中有四种相等算法:
- 抽象(非严格)相等比较 (
==) - 严格相等比较 (
===): 用于Array.prototype.indexOf,Array.prototype.lastIndexOf, 和casematching - 同值零:用于  
%TypedArray%和ArrayBuffer构造函数、以及Map和Set操作,并将用于 ES2016/ES7 中的String.prototype.includes - 同值:用于所有其他地方
 
JavaScript 提供三种不同的值比较操作:
- 严格相等比较 (也被称作"strict equality", "identity", "triple equals"),使用 === (en-US) ,
 - 抽象相等比较 ("loose equality","double equals") ,使用 == (en-US)
 - 以及  
[Object.is](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is)(ECMAScript 2015/ ES6 新特性) 
一、非严格相等比较
非严格相等使用两个等号,也就是我们熟悉的双等,非严格相等表示语义相等,不要求类型一样,非严格相等在比较前会先将比较参数类型转换为一致,再进行比较,代码示例如下:
console.log([10] == 10); //true
console.log('10' == 10); //true
console.log([] == 0); //true
console.log(true == 1); //true
console.log([] == false); //true
console.log(![] == false); //true
console.log('' == 0); //true
console.log('' == false); //true
console.log(null == false); //false
console.log(!null == true); //true
console.log(null == undefined); //true
console.log(012 == 12); // false
console.log(012 == 10); // true
console.log(099 == 99); // true 这种情况是因为八进制中不可能出现9,所以看成一个十进制
console.log(09 == 9); // true 同上
非严格相等的的转换逻辑,可以总结为如下几条规则:
- 和 
Number比较时,另一个值会自动转换为Number - 和 
Boolean比较时,另一个值会转换为Number - 简单类型与引用类型比较,对象转化成其原始类型的值,再比较
 - 两个都为引用类型,则比较它们是否指向同一个对象
 undefined只和null相等- 存在 
NaN则返回false +0等于-0
- 如果 Type(x)和 Type(y)相同,返回 x\=\=\=y b 的结果
 - 如果 Type(x)和 Type(y)不同
 
-   如果 x 是 null,y 是 undefined,返回 true
-   如果 x 是 undefined,y 是 null,返回 true
-   如果 Type(x)是 Number,Type(y)是 String,返回 x\==ToNumber(y) 的结果
-   如果 Type(x)是 String,Type(y)是 Number,返回 ToNumber(x)\==y 的结果
-   如果 Type(x)是 Boolean,返回 ToNumber(x)\==y 的结果
-   如果 Type(y)是 Boolean,返回 x\==ToNumber(y) 的结果
-   如果 Type(x)是 String 或 Number 或 Symbol 中的一种并且 Type(y)是 Object,返回 x\==ToPrimitive(y) 的结果
-   如果 Type(x)是 Object 并且 Type(y)是 String 或 Number 或 Symbol 中的一种,返回 ToPrimitive(x)\==y 的结果
-   其他返回 false
非严格相等有非常复杂的转换规则,非常难以记忆,社区中有人将上面的规则总结成了图片,一图胜千言,如下图所示: 
a = 1;
非严格相等并非带来了很多便利,通过隐式的自动转换,简化了部分场景的工作,比如 Number 和 String 的自动转换,简化了前端从表单,url 参数中获取值的比较问题,但自动转换带来的问题比便利还多。
二、严格相等比较
严格相等是另一种比较算法,其和非严格想等的区别是不会进行类型转换,类型不一致时直接返回 false,严格相等对应===操作符,因为使用三个等号,也被称作三等或者全等,严格相等示例如下:
let result1 = '55' === 55; // false,不相等,因为数据类型不同
let result2 = 55 === 55; // true,相等,因为数据类型相同值也相同
undefined 和 null 与自身严格相等
let result1 = null === null; // true
let result2 = undefined === undefined; // true
let result3 = null === undefined; // false 类型不同
不同类型值判断规则如下,和前面的非严格相等对比,严格相等更符合直觉。
- 如果 Type(x)和 Type(y)不同,返回 false
 - 如果 Type(x)和 Type(y)相同
 
-   如果 Type(x)是 Undefined,返回 true
-   如果 Type(x)是 Null,返回 true
-   如果 Type(x)是 String,当且仅当 x,y 字符序列完全相同(长度相同,每个位置上的字符也相同)时返回 true,否则返回 false
-   如果 Type(x)是 Boolean,如果 x,y 都是 true 或 x,y 都是 false 返回 true,否则返回 false
-   如果 Type(x)是 Symbol,如果 x,y 是相同的 Symbol 值,返回 true,否则返回 false
-   如果 Type(x)是 Number 类型
    -   如果 x 是 NaN,返回 false
    -   如果 y 是 NaN,返回 false
    -   如果 x 的数字值和 y 相等,返回 true
    -   如果 x 是+0,y 是-0,返回 true
    -   如果 x 是-0,y 是+0,返回 true
    -   其他返回 false

严格相等解决了非严格相等中隐式转换带来的问题,但也丢失了隐式转换带来的便利,对于类型可能不一致的情况下,比如从表单中获取的值都是字符串,保险的做法是,在比较前手动类型转换,代码示例如下:
1 === Number('1'); // true 手动类型转换,类型防御
严格相等几乎总是正确的,但也有例外情况,比如 NaN 和正负 0 的问题。
在严格相等中,NaN 是不等于自己的,NaN 是(x !== x) 成立的唯一情况,在某些场景下其实是希望能够判断 NaN 的,可以使用 isNaN 进行判断,ECMAScript 2015 引入了新的 Number.isNaN,和 isNaN 的区别是不会对传入的参数做类型转换,建议使用语义更清晰的 Number.isNaN,但是要注意兼容性问题,判断 NaN 代码示例如下:
NaN === NaN; // false
isNaN(NaN); // true
Number.isNaN(NaN); // true
isNaN('aaa'); // true 自动转换类型 'aaa'转换为Number为NaN
Number.isNaN('aaa'); // false 不进行转换,类型不为Number,直接返回false
严格相等另一个例外情况是,无法区分+0 和-0,代码示例如下,在一些数学计算场景中是要区分语义的。
**+**0 **===** **-**0; *// true*
JavaScript 中很多系统函数都使用严格相等,比如数组的 indexOf,lastIndexOf 和 switch-case 等,需要注意,这些对于 NaN 无法返回正确结果,代码示例如下:
[NaN].indexOf(NaN); // -1 数组中其实存在NaN
[NaN].lastIndexOf(NaN); // -1
三、同值零
同值零是另一种相等算法,名字来源于规范的直译,规范中叫做 SameValueZero,同值零和严格相等功能一样,除了处理 NaN 的方式,同值零认为 NaN 和 NaN 相等,这在判断 NaN 是否在集合中的语义下是非常合理的。 ECMAScript 2016 引入的 includes 使用此算法,此外 Map 的键去重和 Set 的值去重,使用此算法,代码示例如下:
[NaN].incdudes(NaN); // true 注意和indexOf的区别,incdudes的语义更合理
new Set([NaN, NaN]); // [NaN] set中只会有个一个NaN,如果 NaN !== NaN的话,应该是[NaN, NaN]
new Map([
  [NaN, 1],
  [NaN, 2],
]); // {NaN => 2} 如果 NaN !== NaN的话,应该是 {NaN => 1, NaN => 2}
四、同值
同值是最后一种相等算法,其和同值零类似,但认为 +0 不等于 -0,ECMAScript 2015 带来的 Object.is 使用同值算法,代码示例如下:
Object.is(NaN, NaN); // true
Object.is(+0, -0); // false 📢 注意这里
Object.is(0, -0); // false
Object.is(0, +0); // true
Object.is(-0, -0); // true
Object.is(NaN, 0 / 0); // true
Object.is(NaN, NaN); // true
对于数组判断是否存在的场景,如果想区分+0 和-0,可以使用 ECMAScript 2015 引入的 find 方法,自行控制判断逻辑,代码示例如下:
[0].includes(-0); // 不能区分-0
[0].find((val) => Object.is(val, -0)); // 能区分+0和-0
五、区别
下面对比下四种算法的区别,区别如下表所示:
| 隐式转换 | NaN 和 NaN | +0 和 -0 | |
|---|---|---|---|
| 非严格相等(\=\=) | 是 | false | true | 
| 严格相等(\=\=\=) | 否 | false | true | 
| 同值零(includes 等) | 否 | true | true | 
| 同值(Object.is) | 否 | true | false | 
六、引用数据类型相等比较
1. JSON.stringify(obj)
JSON.stringify(obj) === JSON.stringify(otherObj);
这种方法简单,一行代码就搞定,但是有缺点,如下:
- 当对象里
key值顺序不一样时,就会出错 - 一些特殊类型的值,比如
undefined,Date,RegExp等会丢失或者变形 
2. 递归
实现思路如下:
- 先判断两个对象的
key值的长度。若长度不相等,则return false - 遍历对象
obj1,检查对象obj2中是否有对应的key值, 没有,则return false - 比较两个对象中这个
key对应的值的类型是否相等,不相等,则return false - 如果值的类型是
undefined、number、string、boolean的一种,直接两个值比较,不同,则return false - 如果值是
null,那么 比较两个值是否相等。不等,则return false - 如果值是对象,调用自身
 - 如果值是数组,因为数组项可能是任意一种数据类型的,所以还是先比较长度,长度相等后再逐一比较数组的每一项。
 
此代码包含了如何比较两个对象相等 ,代码如下:
const NUMBER_TAG = '[object Number]';
const STRING_TAG = '[object String]';
const BOOLEAN_TAG = '[object Boolean]';
const NULL_TAG = '[object Null]';
const UNDEFINED_TAG = '[object Undefined]';
const OBJECT_TAG = '[object Object]';
const ARRAY_TAG = '[object Array]';
const ERROR_TAG = '[object Error]';
const DATE_TAG = '[object Date]';
const REGEXP_TAG = '[object RegExp]';
const MAP_TAG = '[object Map]';
const SET_TAG = '[object Set]';
// 以下两种类型一般不比较相等性
const SYMBOL_TAG = '[object Symbol]';
const FUNCTION_TAG = '[object Function]';
const toString = Object.prototype.toString;
const getKeys = Object.keys;
// map转数组
function mapToArray(map) {
  let idx = -1;
  const result = new Array(map.size);
  map.forEach(function (value, key) {
    result[++idx] = [key, value];
  });
  return result;
}
// set转数组
function setToArray(set) {
  let idx = -1;
  const result = new Array(set.size);
  set.forEach(function (value) {
    result[++idx] = value;
  });
  return result;
}
/**
 * 比较两个变量是否相等,包括引用类型
 * @param  {[type]} value  [description]
 * @param  {[type]} other  [description]
 * @param  {[type]} vStack [description]
 * @param  {[type]} oStack [description]
 * @return {[type]}        [description]
 */
function eq(value, other, vStack, oStack) {
  const valueType = toString.call(value);
  const otherType = toString.call(other);
  // 类型不同直接返回false
  if (valueType !== otherType) {
    return false;
  }
  // 这里直接进行一个简单粗暴的判断, 这里默认+0与-0相等,不再进行处理
  // 同样适用于基本类型和引用类型,如果是同一个引用也可返回true
  if (value === other) {
    return true;
  }
  // 处理NaN不相等情况
  if (value !== value && other !== other) {
    return true;
  }
  // 处理其他情况
  switch (valueType) {
    case NUMBER_TAG:
    case DATE_TAG:
    case BOOLEAN_TAG:
      return +value === +other;
    case STRING_TAG:
    case REGEXP_TAG:
    case NULL_TAG:
    case UNDEFINED_TAG:
      return '' + value === '' + other;
    case ERROR_TAG:
      return value.name === other.name && value.message === other.message;
    case MAP_TAG:
      const valueMapArr = mapToArray(value);
      const otherMapArr = mapToArray(other);
      if (!eq(valueMapArr, otherMapArr, vStack, oStack)) {
        return false;
      }
      break;
    case SET_TAG:
      const valueSetArr = setToArray(value);
      const otherSetArr = setToArray(other);
      if (!eq(valueSetArr, otherSetArr, vStack, oStack)) {
        return false;
      }
      break;
    case ARRAY_TAG:
    case OBJECT_TAG:
      // 首先对比是否为循环引用
      vStack = vStack || [];
      oStack = oStack || [];
      let vStackLength = vStack.length;
      while (vStackLength--) {
        if (vStack[vStackLength] === value && oStack[vStackLength] === other) {
          return true;
        }
      }
      vStack.push(value);
      oStack.push(other);
      // 数组对比
      if (valueType === ARRAY_TAG) {
        let vLength = value.length;
        let oLength = other.length;
        if (vLength !== oLength) {
          return false;
        }
        while (vLength--) {
          if (!eq(value[vLength], other[vLength], vStack, oStack)) {
            return false;
          }
        }
      }
      // 对象对比
      if (valueType === OBJECT_TAG) {
        const vKeys = getKeys(value);
        const oKeys = getKeys(other);
        let vKeysLength = vKeys.length;
        if (vKeys.length !== oKeys.length) {
          return false;
        }
        while (vKeysLength--) {
          // 对象的key顺序可能不一样,但是对象仍然可能相等
          if (!oKeys.includes(vKeys[vKeysLength])) {
            return false;
          }
          let currentKey = vKeys[vKeysLength];
          if (!eq(value[currentKey], other[currentKey], vStack, oStack)) {
            return false;
          }
        }
      }
      break;
    default:
      return String(value) === String(other);
  }
  return true;
}
参考文档
JavaScript 中的相等性判断 - JavaScript | MDN 如何在 JavaScript 中判断两个值相等