在 JavaScript 中,判断对象相等有多种方式,各有不同用途和特点。
1. 相等性比较运算符
1.1 严格相等 ===和 宽松相等 ==
js
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;
console.log(obj1 === obj2); // false
console.log(obj1 === obj3); // true
console.log(obj1 == obj2); // false
console.log(obj1 == obj3); // true
特点:
- 比较的是引用地址,不是内容
- 只有指向同一内存地址的对象才相等
- 适用于判断是否是同一个对象
2. 深度比较方法
2.1 JSON.stringify(简单对象)
js
function simpleDeepEqual(obj1, obj2) {
return JSON.stringify(obj1) === JSON.stringify(obj2);
}
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
const obj3 = { b: 2, a: 1 }; // 属性顺序不同
const obj4 = { a: 1, b: null };
console.log(simpleDeepEqual(obj1, obj2)); // true
console.log(simpleDeepEqual(obj1, obj3)); // false!顺序不同
console.log(simpleDeepEqual(obj1, { a: 1, b: 2 })); // true
局限性:
- 属性顺序敏感
- 不能处理
undefined、function、Symbol、循环引用 undefined会被忽略,NaN会变成null- 日期对象会转为字符串
js
// JSON.stringify 的问题
console.log(JSON.stringify({ a: undefined })); // "{}"
console.log(JSON.stringify({ a: function() {} })); // "{}"
console.log(JSON.stringify({ a: Symbol() })); // "{}"
console.log(JSON.stringify({ a: NaN })); // '{"a":null}'
console.log(JSON.stringify(new Date())); // 日期字符串
2.2 递归深度比较
js
function deepEqual(obj1, obj2) {
// 1. 基本类型比较
if (obj1 === obj2) return true;
// 2. 检查 null
if (obj1 == null || obj2 == null) return false;
// 3. 检查类型
if (typeof obj1 !== typeof obj2) return false;
if (obj1.constructor !== obj2.constructor) return false;
// 4. 特殊类型处理
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime();
}
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString();
}
// 5. 数组比较
if (Array.isArray(obj1)) {
if (obj1.length !== obj2.length) return false;
for (let i = 0; i < obj1.length; i++) {
if (!deepEqual(obj1[i], obj2[i])) return false;
}
return true;
}
// 6. 对象比较
if (typeof obj1 === 'object') {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (!obj2.hasOwnProperty(key)) return false;
if (!deepEqual(obj1[key], obj2[key])) return false;
}
return true;
}
// 7. 其他类型
return false;
}
优化版本:
js
function deepEqual(obj1, obj2, visited = new WeakMap()) {
// 处理循环引用
if (visited.has(obj1) && visited.get(obj1) === obj2) {
return true;
}
visited.set(obj1, obj2);
// 基本类型
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return obj1 === obj2;
}
// 不同类型
if (obj1.constructor !== obj2.constructor) return false;
// 特殊对象
if (obj1 instanceof Date) return obj1.getTime() === obj2.getTime();
if (obj1 instanceof RegExp) return obj1.toString() === obj2.toString();
if (obj1 instanceof Map || obj2 instanceof Map) {
if (obj1.size !== obj2.size) return false;
for (let [key, val] of obj1) {
if (!obj2.has(key)) return false;
if (!deepEqual(val, obj2.get(key), visited)) return false;
}
return true;
}
if (obj1 instanceof Set || obj2 instanceof Set) {
if (obj1.size !== obj2.size) return false;
for (let val of obj1) {
if (!obj2.has(val)) return false;
}
return true;
}
// 普通对象/数组
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let key of keys1) {
if (!obj2.hasOwnProperty(key)) return false;
if (!deepEqual(obj1[key], obj2[key], visited)) return false;
}
return true;
}
3. 浅比较
3.1 浅比较(只比较第一层)
js
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) return true;
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) {
if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
return false;
}
}
return true;
}
// 使用
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(shallowEqual(obj1, obj2)); // false(b 属性引用不同)
console.log(shallowEqual(obj1, obj1)); // true
3.2 React 风格的浅比较
js
function reactShallowEqual(objA, objB) {
if (Object.is(objA, objB)) return true;
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!Object.is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
4. 使用第三方库
4.1 Lodash
js
// 安装: npm install lodash
import _ from 'lodash';
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(_.isEqual(obj1, obj2)); // true
console.log(_.isEqualWith(obj1, obj2, customizer)); // 自定义比较
4.2 deep-diff
js
// 安装: npm install deep-diff
import diff from 'deep-diff';
const differences = diff(obj1, obj2);
if (!differences) {
console.log('对象相同');
} else {
console.log('差异:', differences);
}
5. ES6+ 新特性比较
5.1 Object.is
js
// Object.is 用于比较值
console.log(Object.is(0, -0)); // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is({}, {})); // false(引用比较)
// 与 === 的区别
console.log(+0 === -0); // true
console.log(NaN === NaN); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(NaN, NaN)); // true
5.2 Map 和 Set
js
// Set 自动去重
const set1 = new Set([1, 2, 3]);
const set2 = new Set([1, 2, 3]);
console.log(set1 === set2); // false
console.log(_.isEqual(set1, set2)); // true
// Map
const map1 = new Map([['a', 1]]);
const map2 = new Map([['a', 1]]);
console.log(map1 === map2); // false
console.log(_.isEqual(map1, map2)); // true
6. 特殊对象比较
6.1 函数比较
js
function func1() { return 1; }
function func2() { return 1; }
function func3 = func1;
console.log(func1 === func2); // false
console.log(func1 === func3); // true
console.log(func1.toString() === func2.toString()); // true(不推荐)
// 函数比较实用方法
function functionsEqual(fn1, fn2) {
return fn1.toString() === fn2.toString();
}
6.2 类实例比较
js
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
equals(other) {
if (!(other instanceof Person)) return false;
return this.name === other.name && this.age === other.age;
}
}
const p1 = new Person('Alice', 30);
const p2 = new Person('Alice', 30);
const p3 = new Person('Bob', 25);
console.log(p1 === p2); // false
console.log(p1.equals(p2)); // true
console.log(p1.equals(p3)); // false
7. 性能比较
js
function performanceTest() {
const obj1 = { a: 1, b: 2, c: { d: 3, e: { f: 4 } } };
const obj2 = { a: 1, b: 2, c: { d: 3, e: { f: 4 } } };
const obj3 = { a: 1, b: 2, c: { d: 3, e: { f: 5 } } };
const iterations = 10000;
// 1. JSON.stringify
console.time('JSON.stringify');
for (let i = 0; i < iterations; i++) {
JSON.stringify(obj1) === JSON.stringify(obj2);
}
console.timeEnd('JSON.stringify');
// 2. 递归深度比较
console.time('递归深度比较');
for (let i = 0; i < iterations; i++) {
deepEqual(obj1, obj2);
}
console.timeEnd('递归深度比较');
// 3. Lodash
console.time('Lodash');
for (let i = 0; i < iterations; i++) {
_.isEqual(obj1, obj2);
}
console.timeEnd('Lodash');
// 4. 浅比较
console.time('浅比较');
for (let i = 0; i < iterations; i++) {
shallowEqual(obj1, obj2);
}
console.timeEnd('浅比较');
}
// 结果通常:
// 浅比较最快
// JSON.stringify 次之
// Lodash 再次
// 递归深度比较最慢
8. 实用工具函数
8.1 通用深度比较
js
function isDeepEqual(obj1, obj2, options = {}) {
const {
strict = true, // 严格比较
checkSymbols = false, // 检查 Symbol
skipKeys = [], // 跳过的键
} = options;
// 快速路径
if (obj1 === obj2) return true;
if (obj1 == null || obj2 == null) return false;
const type1 = typeof obj1;
const type2 = typeof obj2;
if (type1 !== type2) return false;
// 基本类型
if (type1 !== 'object' && type1 !== 'function') {
if (strict) {
return Object.is(obj1, obj2);
}
return obj1 == obj2;
}
// 构造函数比较
if (obj1.constructor !== obj2.constructor) return false;
// 特殊对象处理
if (obj1 instanceof Date) {
return obj1.getTime() === obj2.getTime();
}
if (obj1 instanceof RegExp) {
return obj1.toString() === obj2.toString();
}
if (obj1 instanceof Map) {
if (obj1.size !== obj2.size) return false;
for (let [key, val] of obj1) {
if (!obj2.has(key) || !isDeepEqual(val, obj2.get(key), options)) {
return false;
}
}
return true;
}
if (obj1 instanceof Set) {
if (obj1.size !== obj2.size) return false;
for (let val of obj1) {
if (!obj2.has(val)) return false;
}
return true;
}
// 收集所有键
const keys1 = [...Object.keys(obj1)];
const keys2 = [...Object.keys(obj2)];
if (checkSymbols) {
keys1.push(...Object.getOwnPropertySymbols(obj1));
keys2.push(...Object.getOwnPropertySymbols(obj2));
}
// 过滤跳过的键
const filteredKeys1 = keys1.filter(key => !skipKeys.includes(key));
const filteredKeys2 = keys2.filter(key => !skipKeys.includes(key));
if (filteredKeys1.length !== filteredKeys2.length) return false;
for (let key of filteredKeys1) {
if (!obj2.hasOwnProperty(key)) return false;
if (!isDeepEqual(obj1[key], obj2[key], options)) return false;
}
return true;
}
8.2 比较指定路径
js
function compareByPath(obj1, obj2, paths) {
for (let path of paths) {
const val1 = get(obj1, path);
const val2 = get(obj2, path);
if (!isDeepEqual(val1, val2)) {
return false;
}
}
return true;
}
function get(obj, path) {
return path.split('.').reduce((acc, key) => acc && acc[key], obj);
}
// 使用
const obj1 = { a: 1, b: { c: 2, d: 3 }, e: 4 };
const obj2 = { a: 1, b: { c: 2, d: 5 }, e: 4 };
console.log(compareByPath(obj1, obj2, ['a', 'b.c'])); // true
console.log(compareByPath(obj1, obj2, ['a', 'b.d'])); // false
9. 常见场景
9.1 React/PureComponent
js
// React 的 shouldComponentUpdate
class MyComponent extends React.PureComponent {
// 自动进行浅比较
}
// 自定义 shouldComponentUpdate
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
}
9.2 Redux
js
// Redux reducer
function reducer(state = initialState, action) {
switch (action.type) {
case 'UPDATE_DATA':
// 深度比较
if (_.isEqual(state.data, action.data)) {
return state; // 相同,不更新
}
return { ...state, data: action.data };
default:
return state;
}
}
9.3 缓存(Memoization)
js
// 使用深度比较的缓存
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
for (let [cachedKey, cachedValue] of cache) {
if (isDeepEqual(JSON.parse(cachedKey), args)) {
return cachedValue;
}
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
10. 最佳实践总结
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 比较引用是否相同 | ===或 Object.is |
最快,最直接 |
| 简单对象深度比较 | JSON.stringify |
简单,但有限制 |
| 复杂对象深度比较 | Lodash 的 _.isEqual |
可靠,功能完整 |
| 性能关键,比较第一层 | 浅比较 | 最快,适合不可变数据 |
| 自定义比较逻辑 | 自定义比较函数 | 灵活可控 |
| 比较类实例 | 实现 equals方法 |
面向对象,语义清晰 |
选择指南:
- 引用比较用
===,值比较用深度比较 - 性能优先用浅比较 ,精度优先用深度比较
- 生产环境用 Lodash ,简单场景用 JSON.stringify
- 特殊对象要特殊处理(Date、RegExp、Map、Set 等)
- 注意边界情况(循环引用、Symbol、undefined 等)
根据具体需求选择合适的方法,平衡性能、准确性和可维护性。