一、这个方法在比较什么
util.isDeepStrictEqual(val1,val2)返回布尔值,表示两个值之间是否存在深度严格相等。它的比较规则与assert.deepStrictEqual()一致,但后者在不等时抛出AssertionError,前者只返回false。
“严格”在这里有明确含义:原始值使用Object.is()比较,因此NaN与NaN相等,0与-0不相等。类型不同直接判不等,1和'1'不相等,1和1n也不相等。“深度”意味着递归检查嵌套对象和数组的自有可枚举属性,包括Symbol键。
二、原型与构造函数默认参与比较
一个在使用中容易被忽略的默认行为是:深度严格相等检查包含对对象原型和构造函数的比较。两个来自不同类的实例,即使属性相同,默认也会返回false。
class Foo { constructor(a) { this.a = a; } }
class Bar { constructor(a) { this.a = a; } }
const foo = new Foo(1);
const bar = new Bar(1);
util.isDeepStrictEqual(foo, bar); // false,构造函数不同
util.isDeepStrictEqual(foo, bar, true); // true,skipPrototype 为 true
第三个参数skipPrototype是一个布尔值,默认false。当设为true时,原型和构造函数的差异被跳过,只要可枚举属性的深度严格相等即可。这个选项在Node.js较新版本中提供,如果项目需要兼容较老的Node.js版本,使用前应当确认目标版本是否支持。Bun运行时曾出现过忽略该第三个参数的兼容性问题,说明这一选项在非Node.js环境中未必可用。
三、与assert.deepStrictEqual的职责分工
两者共享同一套比较算法,官方文档在util.isDeepStrictEqual的描述中直接指向assert.deepStrictEqual()了解详情。区别仅在于失败时的行为:util版本返回false,assert版本抛出异常并附带差异信息。
选择依据是使用场景。在if判断、缓存键比较、去重逻辑中,需要布尔返回值,使用util.isDeepStrictEqual。在单元测试中需要失败时终止并展示差异,使用assert.deepStrictEqual。Deno的文档也给出了同样的建议:测试中优先使用断言变体,因为它失败时提供可读的diff。
四、示例:核心比较场景与skipPrototype
以下示例覆盖原始值、嵌套对象、特殊类型以及skipPrototype的效果。示例中的code_number为代码号学习编程的标识。
const util = require('node:util');
// 原始值:Object.is 语义
console.log(util.isDeepStrictEqual(NaN, NaN)); // true
console.log(util.isDeepStrictEqual(0, -0)); // false
console.log(util.isDeepStrictEqual(1, '1')); // false
console.log(util.isDeepStrictEqual(1, 1n)); // false
// 嵌套对象
const a = { code_number: 1, nested: { ok: true } };
const b = { code_number: 1, nested: { ok: true } };
const c = { code_number: 1, nested: { ok: false } };
console.log(util.isDeepStrictEqual(a, b)); // true
console.log(util.isDeepStrictEqual(a, c)); // false
// Map 与 Set 无序比较
const m1 = new Map([['x', 1], ['y', 2]]);
const m2 = new Map([['y', 2], ['x', 1]]);
console.log(util.isDeepStrictEqual(m1, m2)); // true
// 原型差异与 skipPrototype
class MyList extends Array {}
const plain = [1, 2];
const custom = new MyList(1, 2);
custom.push(2);
console.log(util.isDeepStrictEqual(plain, [1, 2])); // true
console.log(util.isDeepStrictEqual(plain, custom)); // false,原型不同
五、一个需要知道的已知缺陷:Map的null键
在Node.jsv22和v24的部分版本中,util.isDeepStrictEqual和assert.deepStrictEqual在一个特定形状的Map比较中会抛出TypeError,而非返回布尔值或断言失败。
触发条件:一个Map含有null键且其值为对象,另一个Map不含null键但含有深度相等的对象键值对,且两个Map的尺寸相同。根源在于内部mapObjectEquiv在尺寸相同时跳过了对null键的原始值处理路径,将null(typeofnull为'object')送入对象比较器,后者读取key.constructor时未做空值防护。
该问题已被修复,但修复提交的时间在2026年,意味着在此之前发布的Node.js版本可能受影响。如果业务代码中存在Map比较场景且Map可能含null键,在升级到包含修复的版本之前,可以用try-catch包裹或先对Map做键类型过滤来规避。更根本的做法是避免在Map中使用null作为键,这本身也不是常见的模式。
另有一个Set比较的非对称性问题被记录:在某些嵌套Set结构中,isDeepStrictEqual(a,b)返回true而isDeepStrictEqual(b,a)返回false。这类缺陷在复杂嵌套结构中可能偶发,测试中对Set比较的断言宜保留两个方向的验证。
六、测试中的断言策略
在node:test或Jest中验证业务逻辑时,如果被测逻辑内部使用了isDeepStrictEqual做判断,测试应当直接断言判断结果,而非重复实现比较逻辑。
对于需要确认“两个值深度相等”的测试断言,使用assert.deepStrictEqual而非util.isDeepStrictEqual。前者在失败时输出属性差异,定位效率更高。util.isDeepStrictEqual的布尔返回值在测试中没有额外价值,反而丢失了差异信息。
当测试用例涉及skipPrototype时,需要确认CI矩阵中所有Node.js版本都支持该参数。如果项目需要兼容Node.js18或更早版本,应当在测试中先检查util.isDeepStrictEqual.length或直接跳过相关用例,避免在旧版本上因参数被忽略而产生假阴性。
七、性能层面的观察
util.isDeepStrictEqual与assert.deepStrictEqual在比较成功路径上的性能接近,但assert版本在失败时需要构造AssertionError和差异信息,开销明显更高。在需要频繁比较且结果多为不等的场景(例如缓存键碰撞检测),util版本更合适。
历史基准测试数据显示,util.isDeepStrictEqual的吞吐量显著高于assert.deepStrictEqual(后者在部分基准中差距达到两个数量级),这与assert的异常构造开销一致。Node.js内部在2018年和2026年分别有过针对深度比较性能的优化,涉及数组和稀疏数组的比较路径。不过这些优化属于内部实现,业务代码的选型依据应当是返回值语义而非微观性能差异。
八、常见误区速查
以为{}和Object.create(null)深度相等。默认比较原型,前者原型为Object.prototype,后者为null,返回false。
以为skipPrototype在所有环境中可用。该参数是Node.js较新版本的行为。Bun曾记录忽略第三个参数的缺陷。跨运行时项目需要验证。
以为util.isDeepStrictEqual和assert.deepStrictEqual在不等时行为相同。前者返回false,后者抛出异常。在if条件中使用assert版本会导致未预期的异常中断。
以为Map/Set比较总是可靠的。已知的Mapnull键TypeError和Set非对称问题说明,在复杂容器类型的极端输入下,比较结果可能不符合预期。对安全性敏感的相等判断,补充额外的校验是合理的。
九、练习与思考
-
util.isDeepStrictEqual(newDate(0),newDate(0))返回什么?util.isDeepStrictEqual(newDate(0),newDate(1))呢?Date的比较依据是什么? -
两个Error实例,
name和message相同但stack不同,isDeepStrictEqual返回什么?stack是否参与比较? -
在一个缓存实现中,用
util.isDeepStrictEqual判断请求参数是否与缓存键匹配。如果请求参数包含原型链上的属性(而非自有属性),比较会忽略它们吗?这对缓存键的设计有什么影响?
十、延伸阅读与参考文献
Node.js 官方文档util.isDeepStrictEqual(val1,val2[,options]),nodejs.cn中文镜像,说明skipPrototype选项的语义与默认值,以及默认比较包含原型和构造函数的规则。https://url.nodejs.cn/api/v26/util/util_isdeepstrictequal_val1_val2_options.html
Node.js 官方文档比较详情,nodejs.cn中文镜像,列出深度严格相等的完整规则:Object.is语义、类型标签、可枚举自有属性、Symbol属性、Map/Set无序比较、WeakMap/Promise仅引用相等。https://url.nodejs.cn/api/v25/assert/5.html
GitHubCommit3673208“util,assert:exposeutil.isDeepStrictEqual()”,记录该函数被暴露为公共API的决策背景,指出lodash、concordance、qunit等模块此前各自实现了类似功能。https://github.com/nodejs/node/commit/36732084db9d0ff59b6ce31e839450cd91a156be
GitHub Issue#64433“assert.deepStrictEqual / util.isDeepStrictEqual throw TypeError on Maps with null keys”,记录Map含null键时抛出TypeError的缺陷,包含触发条件、版本影响和根因分析。https://github.com/nodejs/node/issues/64433
GitHub Issue#61386“Incorrect result from is DeepStrictEqual on two different Sets”,记录Set比较的非对称性缺陷,同一对输入在交换参数后返回不同结果。https://github.com/nodejs/node/issues/61386
npmis-deep-strict-equal-x包文档,在“Comparisondetails”一节中归纳了Node.jsisDeepStrictEqual算法的比较规则,可作为官方文档之外的交叉参考。https://www.npmjs.com/package/is-deep-strict-equal-x