在JavaScript开发过程中,判断两个对象是否相等是高频出现的操作,直接使用双等号或三等号只能判断两个变量是否指向同一个对象引用,无法对比对象内部属性的具体内容,因此需要实现深度比较的逻辑。深度比较会逐层遍历对象的所有属性,对比每个属性的值是否完全一致,包括嵌套的子对象、数组等复杂结构。

方法一:基础递归实现深度比较
最直观的深度比较方式是使用递归遍历两个对象的所有属性,先判断两个值的类型,再分情况处理。如果都是对象类型就继续递归对比,如果是基础类型就直接比较值是否相等。
function deepEqual(obj1, obj2) {
// 先判断两个值是否全等,基础类型直接返回结果
if (obj1 === obj2) {
return true;
}
// 如果有一个不是对象或者是null,直接返回不相等
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
return false;
}
// 获取两个对象的所有自身属性名
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
// 属性数量不同直接不相等
if (keys1.length !== keys2.length) {
return false;
}
// 遍历属性逐个对比
for (let key of keys1) {
// 判断obj2是否也有这个属性,再递归对比属性值
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
return false;
}
}
return true;
}
// 测试示例
const a = { name: '张三', age: 20, info: { city: '北京' } };
const b = { name: '张三', age: 20, info: { city: '北京' } };
const c = { name: '李四', age: 20 };
console.log(deepEqual(a, b)); // 输出 true
console.log(deepEqual(a, c)); // 输出 false
方法二:优化后的通用深度比较方法
基础递归方法没有处理数组、日期、正则等特殊对象类型,优化后的方法会先判断值的构造函数,对不同类型做针对性处理,同时排除原型链的干扰,适用性更强。
function deepEqualOptimized(val1, val2) {
// 基础类型全等直接返回
if (val1 === val2) return true;
// 判断是否为对象类型,排除null
if (typeof val1 !== 'object' || val1 === null || typeof val2 !== 'object' || val2 === null) {
return false;
}
// 获取构造函数,处理数组、日期等特殊类型
const constructor1 = val1.constructor;
const constructor2 = val2.constructor;
// 构造函数不同直接不相等
if (constructor1 !== constructor2) return false;
// 处理数组类型
if (constructor1 === Array) {
if (val1.length !== val2.length) return false;
for (let i = 0; i < val1.length; i++) {
if (!deepEqualOptimized(val1[i], val2[i])) return false;
}
return true;
}
// 处理日期类型,转成时间戳对比
if (constructor1 === Date) {
return val1.getTime() === val2.getTime();
}
// 处理正则类型,转成字符串对比
if (constructor1 === RegExp) {
return val1.toString() === val2.toString();
}
// 处理普通对象
const keys1 = Object.keys(val1);
const keys2 = Object.keys(val2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (!Object.prototype.hasOwnProperty.call(val2, key)) return false;
if (!deepEqualOptimized(val1[key], val2[key])) return false;
}
return true;
}
// 测试示例
const arr1 = [1, 2, { x: 3 }];
const arr2 = [1, 2, { x: 3 }];
const date1 = new Date('2024-01-01');
const date2 = new Date('2024-01-01');
console.log(deepEqualOptimized(arr1, arr2)); // 输出 true
console.log(deepEqualOptimized(date1, date2)); // 输出 true
方法三:支持循环引用的深度比较
如果被比较的对象存在循环引用,前两种方法会陷入无限递归导致栈溢出,这种方法会维护一个已遍历对象的映射表,遇到已经对比过的对象直接返回结果,避免循环递归。
function deepEqualWithCycle(val1, val2, map = new WeakMap()) {
if (val1 === val2) return true;
if (typeof val1 !== 'object' || val1 === null || typeof val2 !== 'object' || val2 === null) {
return false;
}
// 检查是否已经对比过这两个对象,避免循环引用
if (map.has(val1) && map.get(val1) === val2) return true;
// 记录已对比的对象对
map.set(val1, val2);
const constructor1 = val1.constructor;
const constructor2 = val2.constructor;
if (constructor1 !== constructor2) return false;
if (constructor1 === Array) {
if (val1.length !== val2.length) return false;
for (let i = 0; i < val1.length; i++) {
if (!deepEqualWithCycle(val1[i], val2[i], map)) return false;
}
return true;
}
if (constructor1 === Date) {
return val1.getTime() === val2.getTime();
}
if (constructor1 === RegExp) {
return val1.toString() === val2.toString();
}
const keys1 = Object.keys(val1);
const keys2 = Object.keys(val2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (!Object.prototype.hasOwnProperty.call(val2, key)) return false;
if (!deepEqualWithCycle(val1[key], val2[key], map)) return false;
}
return true;
}
// 测试示例:存在循环引用的对象
const objA = { name: '测试' };
const objB = { name: '测试' };
objA.self = objA;
objB.self = objB;
console.log(deepEqualWithCycle(objA, objB)); // 输出 true
三种方法的适用场景总结
可以根据实际需求选择合适的方法,以下是三种方法的特性对比:
| 方法名称 | 优势 | 不足 | 适用场景 |
|---|---|---|---|
| 基础递归实现 | 逻辑简单,容易理解 | 不支持数组、日期等特殊类型,无法处理循环引用 | 仅对比普通纯对象,无复杂类型的简单场景 |
| 优化后的通用方法 | 支持数组、日期、正则等常见特殊类型,适用性广 | 无法处理循环引用的情况 | 大部分常规开发场景,对象无循环引用 |
| 支持循环引用的方法 | 可以处理存在循环引用的复杂对象 | 逻辑相对复杂,需要额外维护映射表 | 对象结构复杂,可能存在循环引用的场景 |
在实际开发中,如果项目中已经引入了工具库,也可以直接使用工具库提供的方法,比如lodash的isEqual方法,其底层实现已经处理了各种边界情况,稳定性和适用性都更好。如果只需要轻量级的实现,可以根据上述三种方法按需选择。