如何正确判断相等性
ES2015 中有四种相等算法:
- 抽象(非严格)相等比较 (
==
) - 严格相等比较 (
===
): 用于Array.prototype.indexOf
,Array.prototype.lastIndexOf
, 和case
matching - 同值零:用于
%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 中判断两个值相等