判断对象全等
JavaScript 自带了==
和===
两种判断方式,前者会隐式转换类型导致代码出现问题,而后者则是根据指针地址进行判断。
在绝大多数情况这两种已经足够使用了,不过延伸下想判断两个对象或数组元素是否相同,则会显得不太友好,而且在 JavaScript 中有一些特殊的规则:NaN
和NaN
不相同、+0
和-0
相同,这就导致有的场景使用起来不便,下面动手实现一个equal
函数,它具备以下功能:
+0
和-0
不相同NaN
和NaN
相同{}
和{}
相同[]
和[]
相同new Set()
和new Set()
相同new Map()
和new Map()
相同new Date(111)
和new Date(111)
相同new String(1)
和new String(1)
之类的包装对象相同/a/
和/a/
相同 -() => {}
和() => {}
不相同- 其他情况一律
===
判断
第一版
在这一版中,我们先实现+0
和-0
、NaN
的判断
function equal(a, b) {
// 判断NaN
if (a === b) {
return 1 / a === 1 / b;
}
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
//... 留空
}
// 判断NaN
return a !== a && b !== b;
}
上面判断NaN
的思路为1 / 0
为Infinity
,而1/-0
为-Infinity
第二版
第一版中我们已经实现了基础的功能,下面就来实现一下怎么深层次判断对象和数组是否相同
function equal(a, b) {
// 判断NaN
if (a === b) {
return 1 / a === 1 / b;
}
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
if (a.constructor !== b.constructor) {
return false;
}
// 判断array
if (Array.isArray(a) && Array.isArray(b)) {
const len = a.length;
if (len !== b.length) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(a[i], b[i])) {
return false;
}
}
return true;
}
// 默认为对象,进行key和长度对比
const keys = Object.keys(a);
const len = keys.length;
if (Object.keys(b).length !== len) {
return false;
}
for (let i = 0; i < len; i++) {
const key = keys[i];
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
if (!equal(a[key], b[key])) {
return false;
}
}
return true;
}
}
上面代码中首先判断constructor
是否相同,constructor
可以从实例指向原型的构造函数,上面代码首先的意思是首先判断构造函数是否相同,如果不相同直接返回。
后面的话判断数组的成员数量、每个子属性是否相同,而判断对象也是判断length
之后判断b
下是否存在name
,之后统一对比值是否相同。
至于执行Object.prototype.hasOwnProperty
的原因则是hasOwnProperty
有可能被改写,例如:
var foo = {
hasOwnProperty: function () {
return false;
},
bar: 'Here be dragons',
};
foo.hasOwnProperty('bar'); // 始终返回 false
第三版
上面的代码基本实现了一个雏形,下面加入Set
、Map
的对比,Set
和Map
是 es6 是引入的新的数据结构,Set
是一组不重复的成员,Map
则是object
的升级版:重点解决了键名循环不稳定以及只能插入 string 键名的问题。
它们两者都是有序的,且都遵循Symbol.iterator
,所以我们不仅要对比name
、value
也要对比所在的位置是否相同,这里为了简化后面的工作提前写了一个函数
const iteration = (value, j) => {
let i = 0;
for (const iterator of value) {
if (j === i++) {
return iterator;
}
}
return undefined;
};
它的作用就是根据j
指定下标来运行迭代器的next
方法,例如:
iteration(new Set([1, 2, 3]), 1); // 2
// 判断set
if (a instanceof Set && b instanceof Set) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(iteration(a, i), iteration(b, i))) {
return false;
}
}
return true;
}
// 判断Map
if (a instanceof Map && b instanceof Map) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
const [nameA, valueA] = iteration(a.entries(), i);
const [nameB, valueB] = iteration(b.entries(), i);
if (!equal(nameA, nameB) || !equal(valueA, valueB)) {
return false;
}
}
return true;
}
可以发现Set
和Map
判断是否相似,首先对比成员数量,其次对比每一次当前位置的键名和键值。
第四版
写到这里我们已经实现了 80%的功能,剩下的一些判断还有
new Date(111)
和new Date(111)
相同new String(1)
和new String(1)
之类的包装对象相同/a/
和/a/
相同
// 判断regexp
if (a.constructor === RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a.toString !== Object.prototype.toString) {
return a.toString() === b.toString();
}
if (a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}
判断包装类型和Date
可以valueOf
和toString
方法实现,每个对象都会从Object.prototype
继承vlaueOf
和toString
方法。
通常情况下valueOf
返回包装对象传递的参数,而有的对象会更改valueOf
方法,例如Date
返回number
类型,所以上面判断valueOf
一则是为了Date
另外一方则是初始过滤传递的参数。
至于为什么也判断toString
则是为了考虑边界情况,例如:
var a = new Boolean(true);
var b = new Boolean(1);
a === b ?
如果没有toString
这个就相同了
最后
完整的把代码贴一下,如果对你有帮助也可以star
支持一下,有什么错误也欢迎指出。
const iteration = (value, j) => {
let i = 0;
for (const iterator of value) {
if (j === i++) {
return iterator;
}
}
return undefined;
};
function equal(a, b) {
// 判断NaN
if (a === b) {
return 1 / a === 1 / b;
}
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
// 判断对象
if (a && b && typeof a === 'object' && typeof b === 'object') {
if (a.constructor !== b.constructor) {
return false;
}
// 判断array
if (Array.isArray(a) && Array.isArray(b)) {
const len = a.length;
if (len !== b.length) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(a[i], b[i])) {
return false;
}
}
return true;
}
// 判断set
if (a instanceof Set && b instanceof Set) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
if (!equal(iteration(a, i), iteration(b, i))) {
return false;
}
}
return true;
}
// 判断Map
if (a instanceof Map && b instanceof Map) {
const len = a.size;
if (len !== b.size) {
return false;
}
for (let i = 0; i < len; i++) {
const [nameA, valueA] = iteration(a.entries(), i);
const [nameB, valueB] = iteration(b.entries(), i);
if (!equal(nameA, nameB) || !equal(valueA, valueB)) {
return false;
}
}
return true;
}
// 判断regexp
if (a.constructor === RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a.toString !== Object.prototype.toString) {
return a.toString() === b.toString();
}
if (a.valueOf !== Object.prototype.valueOf) {
return a.valueOf() === b.valueOf();
}
// 默认为对象,进行key和长度对比
const keys = Object.keys(a);
const len = keys.length;
if (Object.keys(b).length !== len) {
return false;
}
for (let i = 0; i < len; i++) {
const key = keys[i];
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
if (!equal(a[key], b[key])) {
return false;
}
}
return true;
}
}
// 判断NaN
return a !== a && b !== b;
}