手写深度比较isEqual
实现一个深度比较函数,判断两个值是否完全相等,支持对象、数组、基本类型等多种数据类型的比较
问题
在 JavaScript 中,使用 === 或 == 只能比较基本类型和引用地址,无法深度比较对象和数组的内容。我们需要实现一个 isEqual 函数,能够递归比较两个值的所有层级,判断它们是否完全相等。
该函数需要处理:
- 基本类型(string、number、boolean、null、undefined)
- 对象(包括普通对象)
- 数组
- 特殊类型(Date、RegExp、NaN 等)
- 循环引用
解答
/**
* 深度比较两个值是否相等
* @param {*} value1 - 第一个值
* @param {*} value2 - 第二个值
* @param {WeakMap} cache1 - 用于检测循环引用的缓存
* @param {WeakMap} cache2 - 用于检测循环引用的缓存
* @returns {boolean} 是否相等
*/
function isEqual(value1, value2, cache1 = new WeakMap(), cache2 = new WeakMap()) {
// 1. 基本类型比较(包括 NaN)
if (Object.is(value1, value2)) {
return true;
}
// 2. 类型不同直接返回 false
if (typeof value1 !== typeof value2) {
return false;
}
// 3. 处理 null 和 undefined
if (value1 === null || value2 === null) {
return value1 === value2;
}
// 4. 处理 Date 类型
if (value1 instanceof Date && value2 instanceof Date) {
return value1.getTime() === value2.getTime();
}
// 5. 处理 RegExp 类型
if (value1 instanceof RegExp && value2 instanceof RegExp) {
return value1.toString() === value2.toString();
}
// 6. 非对象类型直接返回 false(此时已经过 Object.is 判断)
if (typeof value1 !== 'object') {
return false;
}
// 7. 检测循环引用
if (cache1.has(value1)) {
return cache1.get(value1) === value2;
}
if (cache2.has(value2)) {
return cache2.get(value2) === value1;
}
// 8. 缓存当前对象,用于循环引用检测
cache1.set(value1, value2);
cache2.set(value2, value1);
// 9. 处理数组
if (Array.isArray(value1) && Array.isArray(value2)) {
if (value1.length !== value2.length) {
return false;
}
for (let i = 0; i < value1.length; i++) {
if (!isEqual(value1[i], value2[i], cache1, cache2)) {
return false;
}
}
return true;
}
// 10. 处理普通对象
if (Array.isArray(value1) || Array.isArray(value2)) {
return false; // 一个是数组一个不是
}
const keys1 = Object.keys(value1);
const keys2 = Object.keys(value2);
// 键的数量不同
if (keys1.length !== keys2.length) {
return false;
}
// 递归比较每个键值对
for (let key of keys1) {
if (!keys2.includes(key)) {
return false;
}
if (!isEqual(value1[key], value2[key], cache1, cache2)) {
return false;
}
}
return true;
}
使用示例
// 基本类型比较
console.log(isEqual(1, 1)); // true
console.log(isEqual('hello', 'hello')); // true
console.log(isEqual(NaN, NaN)); // true
console.log(isEqual(null, null)); // true
// 对象比较
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
const obj3 = { a: 1, b: { c: 3 } };
console.log(isEqual(obj1, obj2)); // true
console.log(isEqual(obj1, obj3)); // false
// 数组比较
const arr1 = [1, 2, [3, 4]];
const arr2 = [1, 2, [3, 4]];
const arr3 = [1, 2, [3, 5]];
console.log(isEqual(arr1, arr2)); // true
console.log(isEqual(arr1, arr3)); // false
// Date 比较
const date1 = new Date('2024-01-01');
const date2 = new Date('2024-01-01');
const date3 = new Date('2024-01-02');
console.log(isEqual(date1, date2)); // true
console.log(isEqual(date1, date3)); // false
// RegExp 比较
console.log(isEqual(/abc/g, /abc/g)); // true
console.log(isEqual(/abc/g, /abc/i)); // false
// 循环引用
const circular1 = { a: 1 };
circular1.self = circular1;
const circular2 = { a: 1 };
circular2.self = circular2;
console.log(isEqual(circular1, circular2)); // true
// 复杂嵌套
const complex1 = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding'],
address: {
city: 'Beijing',
detail: { street: 'Main St' }
},
date: new Date('2024-01-01')
};
const complex2 = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding'],
address: {
city: 'Beijing',
detail: { street: 'Main St' }
},
date: new Date('2024-01-01')
};
console.log(isEqual(complex1, complex2)); // true
关键点
-
Object.is() 处理特殊值:使用
Object.is()可以正确比较NaN、+0和-0等特殊值 -
类型判断优先:先判断类型是否相同,不同类型直接返回 false,提高效率
-
特殊对象处理:Date 通过
getTime()比较时间戳,RegExp 通过toString()比较字符串表示 -
循环引用检测:使用 WeakMap 缓存已访问的对象,避免无限递归,正确处理循环引用的情况
-
数组与对象区分:使用
Array.isArray()明确区分数组和对象,避免混淆 -
键的完整性检查:不仅要比较键的数量,还要确保所有键都存在于两个对象中
-
递归比较:对于嵌套的对象和数组,递归调用
isEqual进行深度比较 -
性能优化:通过提前返回(如长度不等、键不存在等)减少不必要的递归调用
目录