
es-toolkit isEqual 深度指南从深比较语义到源码实现【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkites-toolkit 的isEqual是一个用于执行深度相等比较的断言函数它能递归比较对象、数组、Date、RegExp、Map、Set、TypedArray 等任意结构的两个值只要内容一致即返回true与引用是否相同无关。本文以官方参考文档 docs/ja/reference/predicate/isEqual.md 为骨架结合 src/predicate/isEqual.ts 及其底层实现 src/predicate/isEqualWith.ts 的源码完整讲解其使用方式、特殊值语义、循环引用处理等细节帮助你把它安全地用于单元测试、数据快照对比与业务断言。为什么需要 isEqualJavaScript 的只能比较引用与原始值无法判断两个结构相同但引用不同的对象是否内容相等。isEqual补上了这一缺口两个对象只要递归内容相同就判定相等即使它们是完全独立的实例单测断言、接口响应比对、状态快照对比等场景下可直接使用无需手写递归比较逻辑。在 es-toolkit 中isEqual定义于 src/predicate/isEqual.ts实现非常轻量——它将全部逻辑委托给带自定义比较函数的isEqualWith// src/predicate/isEqual.ts import { isEqualWith } from ./isEqualWith.ts; import { noop } from ../function/noop.ts; export function isEqual(a: any, b: any): boolean { return isEqualWith(a, b, noop); }也就是说isEqual等价于不提供任何自定义比较逻辑的isEqualWith默认走内置的完整深度比较算法。两者都从 src/predicate/index.ts 导出可一并按需引入。基本使用从es-toolkit/predicate子路径导入即可import { isEqual } from es-toolkit/predicate; const result isEqual(a, b);参数与返回值项目说明a(unknown)参与比较的第一个值b(unknown)参与比较的第二个值返回值 (boolean)两个值深度相等时返回true否则返回false原始类型的比较原始类型按值比较行为符合直觉isEqual(1, 1); // true isEqual(hello, hello); // true isEqual(true, true); // true isEqual(100n, 100n); // true特殊值的处理isEqual对NaN与0/-0采用了宽松的 SameValueZero 语义这与Object.is、都不完全相同isEqual(NaN, NaN); // true 为 falseObject.is 为 true isEqual(0, -0); // true 为 trueObject.is 为 false从源码看数值分支的判断是a b || Object.is(a, b)见 src/predicate/isEqualWith.ts 中case number分支NaN NaN为false但Object.is(NaN, NaN)为true0 -0为true两条规则叠加后恰好覆盖NaN与正负零两种情况。被Object包装的原始值如new Number(1)则走对象比较路径通过valueOf()取值后用 SameValueZero 语义的eq判断见 src/compat/util/eq.ts。对象与数组的深度比较嵌套对象和数组会逐层递归展开属性顺序不影响结果但键集合必须完全一致// 深对象比较 const obj1 { a: 1, b: { c: 2, d: [3, 4] } }; const obj2 { a: 1, b: { c: 2, d: [3, 4] } }; isEqual(obj1, obj2); // true // 数组比较 const arr1 [1, 2, [3, 4]]; const arr2 [1, 2, [3, 4]]; isEqual(arr1, arr2); // true // 键不同则不等 isEqual({ a: 1, b: 2 }, { a: 1, c: 2 }); // false对应的单元测试覆盖在 src/predicate/isEqual.spec.ts 中包括深层相等对象返回 true不同值返回 false不同键返回 false不同长度数组返回 false等场景。对象比较的源码细节在 src/predicate/isEqualWith.ts 的objectTag分支中普通对象的比较遵循以下步骤若Object.is(a, b)为真直接返回true同一引用的快捷路径通过getTag本质是Object.prototype.toString.call见 src/compat/_internal/getTag.ts获取内部标签若两者标签不一致直接返回false使用areObjectsEqual(a.constructor, b.constructor)或双方均为纯对象isPlainObject来判定实例是否同类类不同则返回false收集键列表[...Object.keys(a), ...getSymbols(a)]长度不同返回false对每个键用Object.hasOwn(b, propKey)校验存在性再递归比较对应值。值得注意的细节普通对象比较不仅包含字符串键还包含可枚举的 Symbol 键。getSymbols见 src/compat/_internal/getSymbols.ts通过Object.getOwnPropertySymbols过滤出propertyIsEnumerable为真的 Symbol 属性因此两个带有相同可枚举 Symbol 属性的对象也会被视为相等。内置对象类型的比较isEqual覆盖了大多数内置对象类型各自采用最贴合其语义的比较策略// Date按时间值比较 const date1 new Date(2020-01-01); const date2 new Date(2020-01-01); isEqual(date1, date2); // true isEqual(new Date(2020-01-01), new Date(2021-01-01)); // false // RegExp比较 source 与 flags const regex1 /hello/g; const regex2 /hello/g; isEqual(regex1, regex2); // true isEqual(/hello/g, /hello/i); // false // Map键值对逐一比较 const map1 new Map([[key, value]]); const map2 new Map([[key, value]]); isEqual(map1, map2); // true // Set元素两两匹配不考虑顺序 const set1 new Set([1, 2, 3]); const set2 new Set([1, 2, 3]); isEqual(set1, set2); // true各类型的判定规则可在 src/predicate/isEqualWith.ts 的areObjectsEqual中逐一对应Date / Boolean / SymbolObject.is(a.valueOf(), b.valueOf())即比较内部原始值RegExpa.source b.source a.flags b.flags字面量与标志位都必须相同Map先比较size再遍历a.entries()要求每个键在b中存在且值深度相等Set先比较size再将a的每个元素在b中做深度匹配匹配成功即从候选池移除因此不依赖插入顺序ArrayBuffer / DataView比较byteLengthDataView 还需比较byteOffset随后包装为Uint8Array逐字节比较TypedArray 系列Uint8Array、Int32Array、Float64Array等与普通数组先比较长度再逐元素递归数组与 Buffer 之间会通过isBuffer交叉校验防止类型混判Error比较name与messagearguments对象先被规整为普通对象标签再按对象逻辑比较函数仅按引用相等a b判断。循环引用的处理深度比较最容易踩的坑是循环引用导致无限递归。isEqual内部通过一个Map栈解决在递归进入复合结构前将a → b与b → a的映射写入stack若再次遇到已入栈的同一对象对直接依据映射关系判定递归结束含异常路径后通过finally从栈中清除对应项。相关逻辑位于 src/predicate/isEqualWith.ts 的stack.set(a, b)、stack.set(b, a)与stack.delete处这也是isEqualWith自定义比较器第六个参数stack的来源——它把内部栈暴露出来供高级自定义逻辑感知循环结构。在单元测试中使用官方文档特别强调isEqual常用于单元测试场景例如手动断言接口响应与期望值是否完全一致import { isEqual } from es-toolkit/predicate; function testApiResponse() { const expected { status: 200, data: { message: success } }; const actual { status: 200, data: { message: success } }; if (isEqual(expected, actual)) { console.log(テスト合格); // 测试通过 } else { console.log(テスト失敗); // 测试失败 } }相比逐个字段断言isEqual让整体结构一致的判断一目了然而在 Vitest/Jest 中它也可作为自定义匹配器的底层实现。仓库的 src/predicate/isEqual.spec.ts 本身就是用 Vitest 书写的真实测试样例覆盖了原始值、NaN、±0、Date、RegExp、深对象、数组与 ArrayBuffer 等全部核心路径可直接作为你编写测试的参照。进阶自定义比较逻辑 isEqualWith如果你需要对特定类型采用自定义规则例如字符串忽略大小写、数值做容差比较请使用isEqualWith。它接受第三个参数areValuesEqual自定义比较函数返回boolean时以该结果为准返回undefined时回退到默认深度比较且自定义逻辑同样作用于对象内部的所有嵌套值import { isEqualWith } from es-toolkit/predicate; const customizer (a, b) { if (typeof a string typeof b string) { return a.toLowerCase() b.toLowerCase(); } }; isEqualWith(Hello, hello, customizer); // true isEqualWith({ a: Hello }, { a: hello }, customizer); // true自定义函数最多可接收六个参数x、y待比较的两个值、property取值所用键、xParent、yParent各自的父对象以及stack循环引用内部栈足以支撑按路径或按上下文定制比较规则的复杂场景。相关定义与示例见 src/predicate/isEqualWith.ts。小结isEqual是 es-toolkit 中覆盖最全面的深度相等断言之一原始值遵循与Object.is叠加的 SameValueZero 语义对象、数组、Map、Set、TypedArray、ArrayBuffer、Error 等复合结构均有专属的比较策略可枚举 Symbol 键被纳入对象键集合循环引用通过内部栈安全化解。无论是编写单元测试、比对 API 响应还是在业务中判断两个数据快照是否一致都可以直接引入使用需要定制时则无缝升级到isEqualWith。【免费下载链接】es-toolkitA modern JavaScript utility library thats 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考