如何最高效对比两个JS对象的属性值并生成对比结果对象
高性能对象属性对比实现方案
原有实现的问题
你给出的初始实现不仅性能不足,还存在核心逻辑错误,无法满足需求:
- 只遍历了第一个入参的属性,完全遗漏第二个对象独有的属性
- 代码中引用了未定义的
object变量,运行直接报错 - 没有校验第二个对象的对应属性是否存在,当某侧缺少嵌套对象时,递归传入
undefined会触发异常 - 用
constructor.name判断对象类型可靠性差,且会把数组误判为普通对象进入递归,不符合需求中数组直接复制原值的要求 - 用
Object.keys + forEach遍历会先生成键名数组,产生不必要的内存开销,遍历速度偏慢
最优实现(兼顾性能与正确性)
实现核心思路:
- 时间复杂度控制在O(N),N为两个对象所有自有属性总数,每个属性仅访问一次,无冗余遍历
- 空间复杂度控制在O(M),M为结果对象的属性总数,除必要的存储/递归栈外无额外开销
- 采用JS引擎高度优化的原生操作,避免高开销API调用
- 仅当两侧同键的值均为非null、非数组的普通对象时才递归对比,其余场景直接判断相等性/复制原值
- 分两次遍历:先处理第一个对象(schema)的所有属性,再遍历第二个对象的独有属性直接复制,避免重复判断
递归版(常规场景性能最优)
适合嵌套深度小于1000层的绝大多数业务场景,V8引擎对递归的优化程度很高,性能优于迭代实现:
function fastDeepCompare(schema, doc) { const result = {} let key, val1, val2 const hasOwn = Object.prototype.hasOwnProperty // 遍历schema侧属性 for (key in schema) { if (!hasOwn.call(schema, key)) continue val1 = schema[key] val2 = doc?.[key] // 仅当两侧值均为普通对象时递归 const isVal1Plain = typeof val1 === 'object' && val1 !== null && !Array.isArray(val1) const isVal2Plain = typeof val2 === 'object' && val2 !== null && !Array.isArray(val2) if (isVal1Plain && isVal2Plain) { result[key] = fastDeepCompare(val1, val2) } else if (doc != null && hasOwn.call(doc, key)) { // 两侧均有该属性,返回相等判断结果 result[key] = val1 === val2 } else { // 仅schema存在该属性,直接复制原值 result[key] = val1 } } // 遍历doc侧独有属性,直接复制 if (doc != null) { for (key in doc) { if (!hasOwn.call(doc, key) || hasOwn.call(schema, key)) continue result[key] = doc[key] } } return result }
迭代版(适配超深嵌套大对象)
如果需要处理嵌套深度超过JS递归栈上限(通常为1000~10000层)的超大对象,可以用显式栈替代递归,无栈溢出风险,性能和递归版几乎一致:
function fastDeepCompareIterative(schema, doc) { const root = {} const stack = [[schema, doc, root]] const hasOwn = Object.prototype.hasOwnProperty while (stack.length) { const [curSchema, curDoc, curRes] = stack.pop() let key, val1, val2 // 处理当前层schema属性 for (key in curSchema) { if (!hasOwn.call(curSchema, key)) continue val1 = curSchema[key] val2 = curDoc?.[key] const isVal1Plain = typeof val1 === 'object' && val1 !== null && !Array.isArray(val1) const isVal2Plain = typeof val2 === 'object' && val2 !== null && !Array.isArray(val2) if (isVal1Plain && isVal2Plain) { curRes[key] = {} stack.push([val1, val2, curRes[key]]) } else if (curDoc != null && hasOwn.call(curDoc, key)) { curRes[key] = val1 === val2 } else { curRes[key] = val1 } } // 处理当前层doc独有属性 if (curDoc != null) { for (key in curDoc) { if (!hasOwn.call(curDoc, key) || hasOwn.call(curSchema, key)) continue curRes[key] = curDoc[key] } } } return root }
循环选型说明
for...in循环是当前遍历对象属性性能最高的方式:
- 相比
Object.keys() + forEach/for of,不需要先生成键名的中间数组,减少了内存分配和遍历开销,在现代JS引擎中性能高20%~50% - 引擎对
for...in的遍历做了惰性优化,不会一次性加载所有键名到内存,大对象场景内存表现更好 - 仅需要通过
hasOwnProperty过滤原型链上的继承属性即可,开销极低
性能测试方法
要准确测试不同实现的性能差异,按照以下步骤操作,避免JIT优化干扰结果:
- 构造贴近业务场景的测试用例,建议覆盖三类场景:
- 浅层次大对象:万级平级属性,30%为双方共有属性,70%为单侧独有
- 深层次对象:嵌套深度≥100层,每层10个左右属性
- 混合类型对象:包含数组、数字、字符串、null、普通对象等多种类型值
- 预热阶段:先循环执行待测试函数10~20次,触发JS引擎的JIT编译优化
- 正式计时:用
performance.now()做高精度计时,循环执行测试函数100~1000次(保证总执行时间≥100ms,减少计时误差),计算单次平均执行时间 - 测试时关闭调试工具、其他占用CPU的程序,同一环境下多次测试取平均值
简单测试代码示例:
// 生成1w属性的测试对象 function genTestObj(prefix) { const obj = {} for (let i = 0; i < 10000; i++) { if (i % 3 === 0) obj[`common${i}`] = i % 2 ? `str${i}` : i else obj[`${prefix}${i}`] = i } obj.nested = { a:1, b: { c:2, d: 'test' } } return obj } const schema = genTestObj('schema_') const doc = genTestObj('doc_') // 预热 for (let i = 0; i < 20; i++) fastDeepCompare(schema, doc) // 计时 const loop = 100 const start = performance.now() for (let i = 0; i < loop; i++) fastDeepCompare(schema, doc) console.log(`单次平均耗时:${((performance.now() - start)/loop).toFixed(3)}ms`)
注:数组、Date、RegExp等非普通对象类型不会递归对比,共有属性直接判断引用相等,单侧存在则直接复制原值,完全匹配需求示例的逻辑。
内容的提问来源于stack exchange,提问作者ColdHands
相关产品推荐
相关产品推荐

