Skip to main content

如何正确判断相等性

ES2015 中有四种相等算法:

  • 抽象(非严格)相等比较 (==)
  • 严格相等比较 (===): 用于  Array.prototype.indexOfArray.prototype.lastIndexOf, 和  casematching
  • 同值零:用于  %TypedArray%  和  ArrayBuffer  构造函数、以及MapSet操作,并将用于 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

非严格相等有非常复杂的转换规则,非常难以记忆,社区中有人将上面的规则总结成了图片,一图胜千言,如下图所示: 591.png

a = 1;

非严格相等并非带来了很多便利,通过隐式的自动转换,简化了部分场景的工作,比如 Number 和 String 的自动转换,简化了前端从表单,url 参数中获取值的比较问题,但自动转换带来的问题比便利还多。

二、严格相等比较

严格相等是另一种比较算法,其和非严格想等的区别是不会进行类型转换,类型不一致时直接返回 false,严格相等对应===操作符,因为使用三个等号,也被称作三等或者全等,严格相等示例如下:

let result1 = '55' === 55; // false,不相等,因为数据类型不同
let result2 = 55 === 55; // true,相等,因为数据类型相同值也相同

undefinednull 与自身严格相等

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

592.png

严格相等解决了非严格相等中隐式转换带来的问题,但也丢失了隐式转换带来的便利,对于类型可能不一致的情况下,比如从表单中获取的值都是字符串,保险的做法是,在比较前手动类型转换,代码示例如下:

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
非严格相等(\=\=)falsetrue
严格相等(\=\=\=)falsetrue
同值零(includes 等)truetrue
同值(Object.is)truefalse

六、引用数据类型相等比较

1. JSON.stringify(obj)

JSON.stringify(obj) === JSON.stringify(otherObj);

这种方法简单,一行代码就搞定,但是有缺点,如下:

  • 当对象里key值顺序不一样时,就会出错
  • 一些特殊类型的值,比如undefinedDateRegExp等会丢失或者变形

2. 递归

实现思路如下:

  1. 先判断两个对象的key值的长度。若长度不相等,则 return false
  2. 遍历对象obj1,检查对象obj2中是否有对应的key值, 没有,则return false
  3. 比较两个对象中这个key对应的值的类型是否相等,不相等,则return false
  4. 如果值的类型是undefinednumberstringboolean的一种,直接两个值比较,不同,则return false
  5. 如果值是null,那么 比较两个值是否相等。不等,则return false
  6. 如果值是对象,调用自身
  7. 如果值是数组,因为数组项可能是任意一种数据类型的,所以还是先比较长度,长度相等后再逐一比较数组的每一项。

此代码包含了如何比较两个对象相等 ,代码如下:

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 中判断两个值相等