- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
导读
在 JavaScript 中,"两个对象是否相等"远没有初看起来那么简单:即使两个对象拥有完全相同的属性和值,==与===也会返回false;用JSON.stringify()比较序列化字符串又暗藏多个陷阱;真正可靠的方案通常需要借助递归实现深度相等比较。本文基于 30-seconds-of-code 仓库中的 object-comparison.md 展开,逐层讲解引用比较、序列化比较与深度相等三种方案的原理、代码实现与边界情况,并关联仓库内js/comparison系列的其他文档与源码级佐证,帮助你写出可复用的对象比较工具。
为什么两个"内容相同"的对象不相等
JavaScript 中对象与数组都是引用类型(reference type),其相等性判断基于引用(reference)而非内容。这意味着只要两个对象在内存中不是同一个实例,无论其属性如何相似,==与===都会判定它们不相等:
const a = { name: 'John', age: 26 }; const b = { name: 'John', age: 26 }; a === b; // false这与原始值(primitive)的按值比较截然不同。仓库中的姊妹篇 equality.md 详细解释了双等号(==,宽松相等)与三等号(===,严格相等)的差异:==会先进行类型强制转换(type coercion)再比较值,而===同时比较类型与值,行为更可预测,因此日常开发中应优先使用===。
此外还存在第三个选择Object.is(),它与===行为基本一致,唯一的例外是NaN与+0/-0的处理(详见 object-is-triple-equals.md):
{} === {}; // false Object.is({}, {}); // false,对象依旧按引用比较 +0 === -0; // true Object.is(+0, -0); // false NaN === NaN; // false Object.is(NaN, NaN); // true不过要注意,Object.is()同样只解决"引用是否相同"的问题,无法比较两个内容相同但引用不同的对象。这正是下面两种方案试图解决的问题。
JSON.stringify() 比较法及其陷阱
JSON.stringify()经常被当作对象比较的"快捷方案":把两个对象序列化成字符串后,再比较字符串是否相等。
const equals = (a, b) => JSON.stringify(a) === JSON.stringify(b); const a = { name: 'John', age: 26 }; const b = { name: 'John', age: 26 }; equals(a, b); // true const c = { name: 'John' }; const d = { name: 'John', age: undefined }; equals(c, d); // true,但按语义应该为 false上面的例子揭示了该类方案最常见的陷阱:相似但不相等的值可能序列化出完全相同的字符串。undefined属性在序列化时会被直接丢弃,导致{ name: 'John' }与{ name: 'John', age: undefined }的序列化结果一致。除此以外,JSON.stringify()比较法还存在其他已知限制:
- 键的顺序:序列化结果依赖键的插入顺序(数字键另有排序规则),属性顺序不同但内容相同的对象会得到不同字符串;
- 函数、Symbol 等特殊值:会被序列化流程跳过或处理为特殊值,导致误判;
- 循环引用:遇到自引用对象会抛出
TypeError: Converting circular structure to JSON(仓库中的 deep-clone-structured-clone.md 同样提到了这一点)。
因此,JSON.stringify()只适合少数"恰好没有上述边界情况"的场景,不能作为通用对象比较方案。真正的解决方案是下面这种递归深度相等比较。
深度相等比较:手写递归 equals()
深度相等比较(deep equality)通过递归逐层比较两个对象每个嵌套层级的值,能够覆盖空值、特殊类型、多层级嵌套等大多数场景。这是社区中各类 shallow/deep equality 工具函数普遍采用的思路。
逐步拆解实现逻辑
实现equals(a, b)需要依次处理以下场景(步骤顺序即代码顺序):
第一步:恒等判断(identical)先检查a === b。这一步同时覆盖了原始值的相等判断,以及"两个引用指向同一个对象"的情况。
if (a === b) return true;第二步:Date 对象特殊处理如果双方都是Date实例,用Date.prototype.getTime()比较其时间戳(毫秒值)。两个表示同一时刻但不同实例的Date对象,在===下不相等,但时间值相等时语义上应当相等。
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();第三步:空值与原始值如果任意一方是null/undefined(即 falsy),或者双方都不是对象(原始值),则回退到严格比较a === b。这样能正确处理原始值相等、以及null/undefined与对象混合的情况。
if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b;第四步:原型检查如果两者原型(prototype)不同,直接返回false。这一步防止把具有相同形状但来源不同的对象误判为相等。
if (a.prototype !== b.prototype) return false;第五步:键数量与递归使用Object.keys()获取双方的可枚举键,先比较键的数量是否一致;再用Array.prototype.every()遍历a的每个键,确认该键在b中存在,并递归调用equals()比较对应值。
const keys = Object.keys(a); if (keys.length !== Object.keys(b).length) return false; return keys.every(k => equals(a[k], b[k]));完整实现与运行验证
将上述五步组合起来,就得到了一份完整的深度相等比较函数:
const equals = (a, b) => { if (a === b) return true; if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime(); if (!a || !b || (typeof a !== 'object' && typeof b !== 'object')) return a === b; if (a.prototype !== b.prototype) return false; const keys = Object.keys(a); if (keys.length !== Object.keys(b).length) return false; return keys.every(k => equals(a[k], b[k])); }; const a = { name: 'John', age: 26 }; const b = { name: 'John', age: 26 }; equals(a, b); // true,内容相同 const c = { name: 'John' }; const d = { name: 'John', age: undefined }; equals(c, d); // false,键数量不同,正确区分了 JSON.stringify 会误判的场景 const e = [1, 2, 3]; const f = { 0: 1, 1: 2, 2: 3 }; equals(e, f); // true,数组与数字键对象在此实现下被视为相等从源码验证边界行为
最后这个例子值得单独说明:equals([1, 2, 3], { 0: 1, 1: 2, 2: 3 })返回true。从该实现看,数组与数字索引对象拥有相同的键集合(['0', '1', '2'])且对应值相等,Object.keys()无法区分二者,因此被判定为相等。这正是"从代码结构可以推断"的行为特征:该实现更关注键与值的结构一致性,而非容器类型本身。如果你的业务要求严格区分数组与普通对象,可以在递归前增加Array.isArray()的类型一致性检查。
另一个值得留意的边界是原型链上的属性:Object.keys()只返回对象自身的可枚举键,因此该实现只比较自有属性,继承属性不会参与比较。这与仓库中 object-has-value-or-key.md 提到的"键检查只针对自身可枚举属性"的约定一致。
补充方案:单向匹配与对称匹配
深度相等是"双向、全量"的比较,但很多业务场景只需要部分匹配(例如表单校验、配置核对)。仓库中的 match-object-properties.md 提供了另一组工具函数,可作为深度相等的补充:
单向匹配:判断第一个对象的键是否为第二个对象键的"超集",且对应值相等。只遍历source的键,配合Object.prototype.hasOwnProperty()与===:
const matches = (obj, source) => Object.keys(source).every( key => obj.hasOwnProperty(key) && obj[key] === source[key] ); matches( { age: 25, hair: 'long', beard: true }, { hair: 'long', beard: true } ); // true对称匹配:双向比较两个对象的键集合完全一致且值相等,利用Set汇总双方键并校验集合大小:
const matchesSymmetric = (a, b) => { const keysA = Object.keys(a), keysB = Object.keys(b); const keys = new Set([...Object.keys(a), ...Object.keys(b)]); if (keys.size !== keysA.length || keys.size !== keysB.length) return false; return [...keys].every( key => a.hasOwnProperty(key) && b.hasOwnProperty(key) && a[key] === b[key] ); }; matchesSymmetric( { hair: 'long', beard: true }, { age: 25, hair: 'long', beard: true } ); // false,键集合大小不一致值得注意的是,matches/matchesSymmetric默认用===做值比较(即浅层值比较),而equals()用递归做深层值比较;两者在嵌套对象场景下行为不同,应根据数据深度选择。此外matchesWith/matchesSymmetricWith还支持传入自定义比较函数,用于正则、忽略大小写等特殊匹配规则。
关联:比较与克隆、数组比较
理解对象比较,还需要把它放入整个js/comparison系列中看待。仓库的 comparison.yaml 将该主题组织为一条完整的知识链,包含js/s/equality、js/s/object-comparison、js/s/array-comparison、js/s/date-comparison、js/s/object-is-triple-equals、js/s/match-object-properties、js/s/arrays-have-same-contents等条目。
其中与对象比较最密切的两个关联主题是:
- 克隆:对象按引用传递的机制决定了"复制"需要克隆。浅克隆只复制外层,嵌套对象仍是引用;深克隆(递归或
structuredClone())才能得到完全独立的副本。参见 shallow-deep-clone-object.md 与 deep-clone-structured-clone.md。需要注意的是,克隆与比较是两个方向的问题——深度相等比较是判断"两个对象内容是否一致",而深克隆是"生成一个内容一致但独立的对象",二者常配合使用(例如先克隆再比较,或在不可变数据流中校验结果)。 - 数组比较:数组是对象的一种特殊形态,arrays-have-same-contents.md 讨论了"忽略顺序、只看元素与频次"的比较策略,它使用
Array.prototype.every()与Set统计唯一值,思路与equals()的递归结构互相印证——都是"先核对规模(长度/键数量),再逐项确认"。
总结与选型建议
回顾三种方案,可以给出如下的选型建议:
| 方案 | 适用场景 | 主要局限 |
|---|---|---|
==/===/Object.is() | 引用相等、原始值比较 | 无法比较两个内容相同但引用不同的对象 |
JSON.stringify()比较 | 无undefined、无函数、无循环引用的扁平对象 | 序列化会丢弃/改写特殊值,键顺序影响结果 |
递归深度相等equals() | 嵌套对象、Date、多层级结构的通用比较 | 需要自行维护边界情况,且对数组与数字键对象不区分 |
实际工程中,推荐优先采用递归深度相等这类显式实现(或社区成熟的 deep-equal 类库),并在其中显式处理业务相关的边界类型(Date、Map、Set、RegExp、循环引用等)。如果比较只是"验证某个对象是否包含另一对象的属性子集",matches系列是更轻量的选择。将 object-comparison.md 与js/comparison系列的其他文档结合阅读,你就能系统掌握 JavaScript 值比较的全貌。
- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
相关推荐
30 Seconds of Code:JavaScript 中两个 Date 对象的比较(相等、先后与区间判断)完整指南
30 Seconds of Code:JavaScript 中两个 Date 对象的比较(相等、先后与区间判断)完整指南 JavaScript 的 Date 对
教程文档30 seconds of code:JavaScript 中 CSV 与数组、对象、JSON 的相互转换实战指南
30 seconds of code:JavaScript 中 CSV 与数组、对象、JSON 的相互转换实战指南 CSV(逗号分隔值) 是一种以纯文本存储表格
教程文档30 seconds of code:JavaScript 数组比较的完整实战指南
30 seconds of code:JavaScript 数组比较的完整实战指南 导读 在 JavaScript 中比较两个数组是否"相等",是开发中最高频也
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考