【JavaScript 对象比较指南:浅对比与深对比】
JavaScript 对象比较指南:浅对比与深对比
从基础概念到实战应用的技术解析
目录
- 一、核心概念
- 二、JavaScript 布尔转换机制
- 三、JavaScript 相等运算符详解
- 四、实现前的重要概念
- 五、浅对比实现
- 六、深对比实现
- 七、浅对比 vs 深对比
- 八、类型判断方法对比
- 九、现有库的实现
- 十、实际应用场景
- 十一、高级话题
- 十二、生活化记忆方法
- 十三、最佳实践
- 十四、总结
一、核心概念
1.1 什么是浅对比和深对比
生活化比喻
浅对比(Shallow Comparison)
- 就像只看房子的外观
- 检查房子的地址、颜色、层数
- 但不打开抽屉看里面的内容
- 快速,但不够深入
深对比(Deep Comparison)
- 就像验房师彻底检查整个房子
- 不仅看外观,还要打开每个抽屉
- 检查每件物品、每个细节
- 全面,但需要更多时间
技术定义
浅对比:只比较对象的第一层属性值,对于嵌套对象只比较引用(地址)。
const obj1 = {
name: '张三',
hobbies: ['读书', '游泳'] // 数组引用
}
const obj2 = {
name: '张三',
hobbies: ['读书', '游泳'] // 不同的数组引用
}
// 浅对比:name相等,但hobbies是不同的数组对象
// 结果:false
深对比:递归比较对象的所有层级,包括嵌套对象的所有属性值。
// 深对比:会递归进入hobbies数组,比较每个元素
// 结果:true
1.2 为什么需要对象比较
JavaScript 的对象引用特性
const person1 = { name: '张三', age: 25 }
const person2 = { name: '张三', age: 25 }
console.log(person1 === person2) // false!
为什么是 false?
- JavaScript 对象比较的是引用(内存地址),不是内容
- 就像问:“这两个长得一模一样的双胞胎是同一个人吗?”
- 答案取决于你怎么定义"相同":
- 引用相等:必须是同一个人(同一个对象)
- 结构相等:长得一样就行(内容相同)
实际需求
在实际开发中,我们经常需要判断两个对象的内容是否相同:
- React 组件优化:判断 props 是否变化,决定是否重新渲染
- 状态管理:判断状态是否真的改变了
- 单元测试:断言两个对象是否相等
- 数据比较:比较表单数据、配置数据等
1.3 JavaScript 的相等性判断
JavaScript 提供了多种判断相等的方式:
// 1. 严格相等 ===
5 === 5 // true
"5" === 5 // false(类型不同)
{} === {} // false(不同引用)
// 2. 松散相等 ==
5 == "5" // true(类型转换)
null == undefined // true(特殊规则)
// 3. Object.is()
Object.is(NaN, NaN) // true
Object.is(+0, -0) // false
Object.is({}, {}) // false(不同引用)
// 4. 我们需要的:结构化比较
deepEqual({ a: 1 }, { a: 1 }) // true
二、JavaScript 布尔转换机制
核心概念:所有值都可以转换为布尔值
JavaScript 的重要特性:
在 JavaScript 中,每个值都可以被转换成布尔值(true 或 false)。这是 布尔转换(Boolean Conversion) 机制。
// 任何值都可以用 Boolean() 转换
Boolean(42) // true
Boolean('hello') // true
Boolean(null) // false
Boolean({}) // true
Boolean([]) // true
Boolean(function () {}) // true
权威来源:
- ECMAScript® 2024 Language Specification
- 章节:7.1.2 ToBoolean ( argument )
- 原文:“The abstract operation ToBoolean takes argument argument (an ECMAScript language value) and returns a Boolean. It converts argument to a value of type Boolean.”
这不是某个人的说法,而是 JavaScript 语言的官方标准!
2.1 Falsy 和 Truthy 值
什么是 Falsy?
Falsy 就是在布尔转换中会被转换为 false 的值。
JavaScript 中只有 8 个 falsy 值:
false // 布尔 false
0 - // 数字 0
0 // 负零
0n // BigInt 的 0
;('') // 空字符串
null // 空值
undefined // 未定义
NaN // 不是数字
什么是 Truthy?
Truthy 就是除了上面 8 个 falsy 值以外的所有其他值!
// 这些都是 truthy 值
true // 布尔 true
1 // 任何非零数字
-1 // 负数也是 truthy
"hello" // 任何非空字符串
"0" // 字符串 "0" 是 truthy!
[] // 空数组是 truthy!
{} // 空对象是 truthy!
function() {} // 函数是 truthy
如何测试?
// 方法 1:使用 Boolean() 函数
Boolean(0) // false (falsy)
Boolean('') // false (falsy)
Boolean(null) // false (falsy)
Boolean(1) // true (truthy)
Boolean('hello') // true (truthy)
Boolean([]) // true (truthy)
// 方法 2:使用 !! 双重否定
!!0 // false
!!'hello' // true
!!null // false
!![] // true
权威来源
这是 ECMAScript 官方规范明确定义的:
- 规范章节:ECMAScript® 2024 - 7.1.2 ToBoolean
- 制定机构:TC39 委员会(由 Google、Mozilla、Microsoft、Apple 等组成)
- 历史背景:从 JavaScript 诞生(1995年)就存在
2.2 布尔转换的应用场景
1. if 语句中的隐式转换
let name = ''
if (name) {
// name 是空字符串,falsy
console.log('有名字')
} else {
console.log('没名字') // 会执行这里
}
let users = []
if (users) {
// 空数组是 truthy!
console.log('有用户数组') // 会执行这里
}
2. 逻辑运算符
// && 遇到 falsy 就停止
true && false && 'hello' // false
true && 'hello' && 1 // 1
// || 遇到 truthy 就停止
false || '' || 'hello' // "hello"
null || undefined || 0 // 0(所有都是falsy)
// 常见用法:设置默认值
let username = user.name || '游客'
3. 三元运算符
let message = user ? `欢迎 ${user.name}` : '请登录'
// ↑ user 是 truthy 还是 falsy?
4. 在对比函数中的应用
// !obj1 检查 obj1 是否为 falsy 值
if (!obj1 || !obj2) {
return false
}
// 相当于检查:obj1 是 null/undefined/0/""等吗?
2.3 常见陷阱
// 陷阱 1:空数组和空对象是 truthy!
if ([]) console.log("空数组是 truthy"); // 会执行
if ({}) console.log("空对象是 truthy"); // 会执行
// 陷阱 2:字符串 "0" 是 truthy
if ("0") console.log("字符串 0 是 truthy"); // 会执行
if (0) console.log("数字 0 是 falsy"); // 不会执行
// 陷阱 3:NaN 的比较
if (NaN) console.log("不会执行"); // NaN 是 falsy
console.log(NaN === NaN); // false!
// 陷阱 4:空数组在松散相等中的转换
[] == false // true(数组转成空字符串,再转成0)
[] == 0 // true
[] === false // false(类型不同)
2.4 记忆总结
核心原则
“在 JavaScript 中,所有值都可以转换为布尔值”
这是 ECMAScript 规范定义的语言特性,从 1995 年至今一直存在。
Falsy 值记忆口诀
“假零空难” + BigInt零
- 假:
false - 零:
0、-0 - 空:
""(空字符串)、null、undefined - 难:
NaN(难以定义的数) - BigInt零:
0n
判断方法
// 方法1:Boolean() 函数(显式转换)
Boolean(value)
// 方法2:!! 双重否定(常用简写)
!!value
// 方法3:在条件语句中(隐式转换)
if (value) {
/* ... */
}
实用技巧
// ✅ 检查变量是否有"有效值"
if (username) {
/* 用户名存在且非空 */
}
// ✅ 设置默认值
const name = inputName || '匿名用户'
// ❌ 注意:0 也是 falsy
const count = 0
if (count) {
/* 不会执行,即使count已定义 */
}
// ✅ 正确检查数字
if (count !== undefined && count !== null) {
/* 会执行 */
}
if (typeof count === 'number') {
/* 更严谨 */
}
在对比函数中的应用
// !obj1 || !obj2 检查的是:
// obj1 或 obj2 是否为 falsy 值(null、undefined、0、""等)
function shallowEqual(obj1, obj2) {
if (Object.is(obj1, obj2)) return true
// 排除 falsy 值(主要是 null/undefined)
if (!obj1 || !obj2) return false
// ... 继续处理
}
三、JavaScript 相等运算符详解
3.1 严格相等 ===
记忆口诀:“我很挑剔,必须完全一样!”
规则
- 类型必须相同
- 值必须相同
- 对象比较引用
- 不进行类型转换
// 基本类型
5 === 5 // true
"hello" === "hello" // true
true === true // true
// 类型不同
5 === "5" // false
0 === false // false
"" === 0 // false
// null 和 undefined
null === null // true
undefined === undefined // true
null === undefined // false(类型不同)
// 对象类型(比较引用)
{} === {} // false(不同对象)
[] === [] // false(不同数组)
const a = {};
const b = a;
a === b // true(同一个对象)
// 特殊值
NaN === NaN // false(NaN 不等于任何值,包括自己)
+0 === -0 // true
3.2 松散相等 ==
记忆口诀:“我很宽松,差不多就行!”
规则
- 会进行类型转换
- 转换后再比较
- 有特殊规则
类型转换规则
// 数字和字符串
"5" == 5 // true(字符串转数字)
"" == 0 // true(空字符串转 0)
"0" == 0 // true
// 布尔值
true == 1 // true(true 转 1)
false == 0 // true(false 转 0)
"" == false // true(都转成 0)
"0" == false // true(都转成 0)
// 特殊规则:null 和 undefined
null == null // true
undefined == undefined // true
null == undefined // true(特殊!)
null == 0 // false
undefined == 0 // false
// 对象转换
[] == false // true([]转""转0,false转0)
[] == 0 // true
[1] == 1 // true([1]转"1"转1)
// NaN 的规则
NaN == NaN // false(任何方式都不等于自己)
神奇的转换(容易出错)
// 这些都返回 true,但很容易搞错
true == 1 // true
false == 0 // true
'' == false // true
'0' == false // true
// 但是注意这个
'false' == false // false("false"转NaN,不等于0)
' ' == false // true(空格转0)
'1' == true // true
'2' == true // false(2不等于1)
3.3 Object.is()
现代化的比较方法,解决了 === 的两个特殊情况:
// Object.is vs ===
// 大多数情况相同
Object.is(5, 5) // true
Object.is('hello', 'hello') // true
Object.is({}, {}) // false(不同引用)
// NaN 的处理(区别)
NaN === NaN // false
Object.is(NaN, NaN) + // true ✅
// 正负零的处理(区别)
0 ===
-0 // true
Object.is(+0, -0) // false ✅
// 其他特殊值
Object.is(null, null) // true
Object.is(undefined, undefined) // true
Object.is(null, undefined) // false
Object.is() 不适合对象比较
const obj1 = { a: 1, b: 2 }
const obj2 = { a: 1, b: 2 }
Object.is(obj1, obj2) // false(只比较引用)
obj1 === obj2 // false(同样结果)
// Object.is() 只能比较引用是否相同,不能做结构化比较
// 所以不能用于浅对比或深对比
3.4 特殊值的比较规则
null 的规则
// 严格相等 === - "完全一样才行"
null === null // true ✅
null === undefined // false ❌
null === 0 // false ❌
null === false // false ❌
null === '' // false ❌
// 松散相等 == - "JavaScript 觉得它们差不多"
null == null // true ✅
null == undefined // true ✅(特殊规则)
null == 0 // false ❌
null == false // false ❌
// 为什么 null == undefined?
// JavaScript 设计者认为它们都表示"没有值"
// 这是语言规范的特殊规则,只对这两个值生效
实用技巧:
// 用 == null 同时检查 null 和 undefined
if (value == null) {
// value 是 null 或 undefined
}
// 等价于(但更简洁)
if (value === null || value === undefined) {
// 同上
}
undefined 的规则
// 严格相等
undefined === undefined // true ✅
undefined === null // false ❌
// 松散相等
undefined == undefined // true ✅
undefined == null // true ✅
// 其他值
undefined == 0 // false
undefined == false // false
undefined == '' // false
NaN 的奇葩规则
NaN 是唯一一个不等于自己的值!
// 任何方式都不等于自己
NaN === NaN // false ❌
NaN == NaN // false ❌
NaN !== NaN // true ✅(所以不等于自己)
// 为什么这样设计?
// 数学概念:"不是数字"怎么能等于"不是数字"?
// 0/0 的结果怎么能等于 1/0 的结果?
// 每个 NaN 可能来自不同的计算错误
// 判断 NaN 的正确方法
Number.isNaN(NaN) // true ✅
Object.is(NaN, NaN) // true ✅
isNaN(NaN) // true(但不推荐,会类型转换)
// isNaN 的陷阱
isNaN('hello') // true("hello"转NaN)
Number.isNaN('hello') // false(更严格,推荐)
NaN 规则记忆:“NaN 很孤独,连自己都不认识”
特殊值比较总结表
| 比较 | === |
== |
记忆方法 |
|---|---|---|---|
null === null |
✅ | ✅ | 自己当然等于自己 |
null === undefined |
❌ | ✅ | 严格派:类型不同;松散派:都是空 |
NaN === NaN |
❌ | ❌ | NaN 很孤独,谁都不认 |
+0 === -0 |
✅ | ✅ | 默认认为相等 |
Object.is(+0, -0) |
- | - | ❌ 区分正负零 |
"5" === 5 |
❌ | ✅ | 严格派:类型不同;松散派:转换后相等 |
[] === [] |
❌ | ❌ | 不同对象引用,都不认 |
3.5 实用建议
什么时候用严格 ===?
99% 的情况都用这个!
// 推荐:明确、可预测
if (count === 0) {
}
if (name === '') {
}
if (user === null) {
}
优势:
- 代码更可预测,不会有奇怪的转换
- 性能更好(不需要类型转换)
- 避免隐藏的 bug
什么时候用松散 ==?
只有一个场景:检查 null/undefined
// 推荐:同时检查 null 和 undefined
if (value == null) {
// value 是 null 或 undefined
}
// 不推荐其他场景
if (count == 0) {
} // 不好:"" 也会通过
if (name == '') {
} // 不好:0 也会通过
终极记忆法
严格相等 ===:像严格的老师,一个字母都不能错
松散相等 ==:像宽松的朋友,大概意思对就行
特殊记忆:
null == undefined→ “空兄弟,互相认识”NaN !== NaN→ “NaN 是独狼,谁都不认”- 对象比较 → “比的是身份证(引用),不是长相”
四、实现前的重要概念
在深入实现浅对比和深对比之前,我们需要理解几个关键概念和设计决策。这些概念将帮助你更好地理解实现细节和做出正确的技术选择。
4.1 NaN的特殊处理
NaN的哲学问题
NaN(Not a Number)是JavaScript中最令人困惑的值之一。它的比较行为来自IEEE 754浮点数标准:
// NaN的奇特性质
NaN === NaN // false ❌ (NaN不等于自己)
NaN == NaN // false ❌ (松散相等也不行)
Object.is(NaN, NaN) // true ✅ (只有这个方法认为NaN等于NaN)
原则: “不是数字"怎么能等于另一个"不是数字”?
为什么NaN !== NaN?
// 这些都产生NaN,但原因不同
0 / 0 // NaN (数学上未定义)
Math.sqrt(-1) // NaN (负数开方)
parseInt('hello') // NaN (字符串转数字失败)
undefined + 1 // NaN (undefined参与运算)
类比: 就像问:“不知道” 等于 “不知道” 吗?“错误A” 等于 “错误B” 吗?
IEEE标准认为:“不同原因产生的NaN,不应该相等”
实际项目中的处理策略
场景1:单元测试 - 通常认为相等
// Jest, Mocha等测试框架
expect(calculateSomething()).toEqual(NaN) // 会通过
// 因为测试关心的是"都是无效结果",而不是产生原因
场景2:数据比较库 - 通常认为相等
// Lodash
_.isEqual(NaN, NaN) // true ✅
// Ramda
R.equals(NaN, NaN) // true ✅
// 理由:用户期望"相同的异常值"应该被认为相等
场景3:数学计算 - 保持不等
// 科学计算库可能保持NaN !== NaN的行为
// 因为不同的计算错误应该区别对待
我们的对比函数如何处理?
考虑用户期望:
// 用户写测试时的期望
deepEqual({ result: NaN }, { result: NaN }) // 期望是true
// 用户比较计算结果时的期望
deepEqual([1, 2, NaN], [1, 2, NaN]) // 期望是true
用户的逻辑: “虽然计算出错了,但出错的方式一样,那结果应该算相等”
生活化比喻:NaN比较 = 看病人的体温计
两个病人都发烧到体温计爆表(显示"ERROR"):
- 医生A(严格派): “两个ERROR不一样!一个可能是43度,一个可能是45度!”
- 医生B(实用派): “都是高烧到爆表,对治疗来说,算是同一种情况”
大多数情况下,我们是医生B - 关心的是"都出错了"这个结果。
4.2 为什么用Object.is()
设计原则的统一
function deepEqual(obj1, obj2) {
if (Object.is(obj1, obj2)) return true // 一行解决多个问题!
// ...
}
一石二鸟的效果:
- 与主流库保持一致
// 现在我们的行为和它们一致了
deepEqual(NaN, NaN) // true ✅ (像Lodash)
deepEqual(+0, -0) // false ✅ (像Ramda)
- 与JavaScript新标准保持一致
// Object.is是ES6引入的"更好的相等比较"
Object.is(NaN, NaN) // true
Object.is(+0, -0) // false
Object.is() 的设计哲学
Object.is() 被设计为"SameValue"算法,它的理念是:
- “在JavaScript值的层面,它们是否为同一个值”
- 而不是IEEE 754的"数学相等"概念
生态系统的演进
// 历史演进
=== 运算符 (1995) -> 遵循IEEE 754,NaN !== NaN
Object.is() (2015) -> 语言层面的"同值",NaN === NaN
主流库的选择 -> 跟随Object.is的语义
使用Object.is()的理由
✅ 标准化 - ES6官方的"改进版相等比较"
✅ 生态一致性 - 与Lodash、Ramda等主流库一致
✅ 用户友好 - 符合用户对"深度相等"的直觉期望
✅ 简洁性 - 一行代码处理NaN和+0/-0的边界情况
✅ 未来性 - 跟随JavaScript语言的演进方向
生活比喻:
- === = 老派严格的数学老师:“按IEEE标准,NaN不等于NaN!”
- Object.is() = 现代实用的程序老师:“在编程世界里,相同的异常值就是相等的!”
4.3 函数的比较问题
函数的typeof结果
typeof function () {} // "function" (不是 "object")
函数比较的问题
const func1 = function () {
return 1
}
const func2 = function () {
return 1
}
// 深对比的处理
deepEqual(func1, func2) // false (不同引用)
// 更复杂的情况
const func1 = function () {}
func1.customProp = 'hello'
const func2 = function () {}
func2.customProp = 'hello'
deepEqual(func1, func2) // false (函数本身不同)
主流库如何处理?
// Lodash 认为函数只比较引用
_.isEqual(func1, func2) // false (除非是同一个引用)
生活化比喻
函数比较 = 比较两个"黑盒机器"
const 榨汁机A = function (fruit) {
return fruit + '汁'
}
const 榨汁机B = function (fruit) {
return fruit + '汁'
}
问题: 这两个榨汁机一样吗?
- 外观一样(toString相同)
- 功能一样(都能榨汁)
- 但是不是同一台机器(不同引用)
结论: “不是同一台机器,就认为不同”
4.4 浅对比vs深对比的设计差异
设计目标的本质区别
浅对比 = 专门的"对象属性比较器"
// 浅对比的使命:比较对象的第一层属性
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false
设计理念:
- “我只负责对象属性比较!”
- “基本类型?那不是我的工作范围”
- “你要比较数字字符串,用 === 就行了”
深对比 = 万能"值比较器"
// 深对比的使命:比较任何类型的值
if (typeof obj1 !== typeof obj2) return false
设计理念:
- “我要处理所有类型的比较”
- “数字、字符串、对象、数组…统统接受”
- “给我什么都能比较”
实际使用场景的差异
浅对比的典型场景:
// React组件props比较
const UserCard = React.memo(function UserCard(props) {
return <div>{props.user.name}</div>
})
// React关心的是:props对象是否发生变化
const oldProps = { user: { name: '张三' }, theme: 'dark' }
const newProps = { user: { name: '张三' }, theme: 'light' }
shallowEqual(oldProps, newProps) // false - props变了,需要重渲染
shallowEqual('hello', 'hello') // false - "这不是props对象!"
深对比的典型场景:
// 单元测试断言
expect(someFunction()).toEqual(expectedResult)
// 可能的返回值类型:
someFunction() // 可能返回数字: 42
someFunction() // 可能返回字符串: "success"
someFunction() // 可能返回对象: {status: "ok"}
// 深对比必须处理所有情况
deepEqual(42, 42) // true - "数字我也能比"
deepEqual('hello', 'hello') // true - "字符串我也能比"
deepEqual({ a: 1 }, { a: 1 }) // true - "对象当然能比"
生活化记忆
浅对比 = 房产中介
shallowEqual(房子A, 房子B) // ✅ "这是我的专业领域"
shallowEqual('张三', '李四') // ❌ "我不比较人名,我只看房子"
深对比 = 万能鉴定师
deepEqual(房子A, 房子B) // ✅ "房子我能鉴定"
deepEqual('张三', '李四') // ✅ "名字我也能鉴定"
deepEqual(42, 42) // ✅ "数字我还能鉴定"
类型检查的差异总结
| 特性 | 浅对比 | 深对比 |
|---|---|---|
| 角色定位 | 对象属性专家 | 万能比较专家 |
| 接受类型 | 只要对象 | 接受一切 |
| 比较原则 | 必须都是对象 | 类型必须相同 |
| 类型检查 | typeof obj !== 'object' |
typeof obj1 !== typeof obj2 |
| 拒绝理由 | “不是对象我不管” | “类型不同没法比” |
五、浅对比实现
5.1 设计思路
浅对比的目标:比较对象的第一层属性值是否相等。
关键特点:
- 只检查第一层属性
- 嵌套对象只比较引用
- 快速但不深入
适用场景:
- React 组件 props 比较
- Redux 状态变化检测
- 性能敏感的场景
4.2 完整实现
/**
* 浅对比:只比较对象的第一层属性
* 门卫大爷版 - 只看证件表面信息
* @param {*} obj1 - 第一个对象
* @param {*} obj2 - 第二个对象
* @returns {boolean} - 是否相等
*/
function shallowEqual(obj1, obj2) {
// 步骤 1:指纹识别(处理NaN、+0/-0等)
if (Object.is(obj1, obj2)) {
return true
}
// 步骤 2:证件检查(null/undefined)
if (obj1 == null || obj2 == null) {
return false
}
// 步骤 3:证件类型(必须都是对象)
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return false
}
// 步骤 4:证件格式(数组 vs 对象)
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
return false
}
// 步骤 5:数组的浅层比较
if (Array.isArray(obj1)) {
if (obj1.length !== obj2.length) {
return false
}
for (let i = 0; i < obj1.length; i++) {
if (!Object.is(obj1[i], obj2[i])) {
return false // 浅层比较
}
}
return true
}
// 步骤 6:对象的浅层比较
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 (!Object.is(obj1[key], obj2[key])) {
return false // 浅层比较
}
}
return true
}
4.3 实现细节分析
步骤 1:引用相等检查
if (obj1 === obj2) {
return true
}
考量:
- 最快的检查方式
- 处理基本类型:
5 === 5、"hello" === "hello" - 处理相同引用的对象:
const a = {}; const b = a; - 性能优化:避免不必要的后续检查
示例:
const user = { name: '张三' }
shallowEqual(user, user) // true(相同引用)
shallowEqual(5, 5) // true(基本类型)
shallowEqual('hello', 'hello') // true(基本类型)
步骤 2:null/undefined 检查
if (obj1 == null || obj2 == null) {
return false
}
为什么用 == null 而不是 !obj1?
// !obj1 会把所有 falsy 值都排除
!0 // true(0 被排除)
!'' // true(空字符串被排除)
!false // true(false 被排除)
!null // true(null 被排除)
// obj1 == null 只捕获 null 和 undefined
0 == null // false
'' == null // false
false == null // false
null == null // true
undefined == null // true
为什么这样设计?
- 防止 typeof null 陷阱:
typeof null === "object" - 避免 Object.keys(null) 报错:
TypeError - 精确处理空值:只排除 null/undefined,不影响其他 falsy 值
防御性编程示例:
// ❌ 错误:没有 null 检查
function badShallowEqual(obj1, obj2) {
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return false
}
// 危险!如果 obj1 是 null,这里会报错
const keys1 = Object.keys(obj1) // TypeError!
}
badShallowEqual(null, { a: 1 }) // 💥 崩溃
// ✅ 正确:有 null 检查
function goodShallowEqual(obj1, obj2) {
if (obj1 == null || obj2 == null) {
return false
}
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return false
}
const keys1 = Object.keys(obj1) // 安全
}
步骤 3:类型检查
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return false
}
考量:
- 确保都是对象类型
- 已经排除了 null(步骤 2)
- 非对象在步骤 1 已经比较过了
浅对比的哲学:只关心对象属性比较,非对象直接返回 false。
shallowEqual(5, 5) // true(步骤 1 返回)
shallowEqual(5, '5') // false(步骤 1 不等,这里返回)
shallowEqual('hello', 'hi') // false(步骤 1 不等,这里返回)
步骤 4-5:属性数量检查
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
考量:
- 快速排除:属性数量不同,肯定不相等
- 性能优化:避免进入循环比较
- O(1) 时间复杂度:只是长度比较
const obj1 = { a: 1, b: 2 }
const obj2 = { a: 1, b: 2, c: 3 }
// 属性数量不同,立即返回 false
shallowEqual(obj1, obj2) // false
步骤 6:属性值比较
for (let key of keys1) {
if (obj1[key] !== obj2[key]) {
return false
}
}
考量:
- 使用严格相等
!==,不做类型转换 - 只比较第一层:不递归进入嵌套对象
- 嵌套对象比较的是引用
关键特点:
const obj1 = {
name: '张三',
age: 25,
hobbies: ['读书', '游泳'] // 数组引用 A
}
const obj2 = {
name: '张三',
age: 25,
hobbies: ['读书', '游泳'] // 数组引用 B
}
// 浅对比
shallowEqual(obj1, obj2) // false
// 原因:
// obj1.name === obj2.name ✅ true
// obj1.age === obj2.age ✅ true
// obj1.hobbies === obj2.hobbies ❌ false(不同数组引用)
4.4 使用场景
1. React.memo 组件优化
import React from 'react'
// 使用浅对比优化组件
const UserCard = React.memo(function UserCard({ user, onEdit }) {
console.log('UserCard 渲染')
return (
<div>
<h3>{user.name}</h3>
<p>年龄:{user.age}</p>
<button onClick={onEdit}>编辑</button>
</div>
)
})
// 父组件
function App() {
const [user, setUser] = useState({ name: '张三', age: 25 })
const handleEdit = () => {
// ❌ 错误:创建新对象,但内容相同
setUser({ name: '张三', age: 25 }) // 会触发重新渲染
// ✅ 正确:引用相同,不触发渲染
setUser(user) // 不会重新渲染
}
return <UserCard user={user} onEdit={handleEdit} />
}
2. Redux 状态检测
// Redux 的 connect 使用浅对比
import { connect } from 'react-redux'
function mapStateToProps(state) {
return {
count: state.counter.value,
user: state.user.data
}
}
// 只有 count 或 user 的引用改变,组件才会重新渲染
export default connect(mapStateToProps)(MyComponent)
3. 自定义 Hook 依赖比较
import { useEffect, useRef } from 'react'
function useShallowEffect(callback, deps) {
const prevDepsRef = useRef()
useEffect(() => {
const prevDeps = prevDepsRef.current
// 使用浅对比判断依赖是否变化
if (!prevDeps || !shallowEqual(prevDeps, deps)) {
callback()
prevDepsRef.current = deps
}
})
}
// 使用
useShallowEffect(
() => {
console.log('User changed')
},
{ name: user.name, age: user.age }
)
五、深对比实现
5.1 设计思路
深对比的目标:递归比较对象的所有层级,包括嵌套对象的所有属性值。
关键特点:
- 递归比较所有层级
- 处理各种数据类型(数组、Date、RegExp 等)
- 全面但性能开销大
适用场景:
- 单元测试断言
- 配置文件比较
- 不频繁调用但需要准确性的场景
5.2 完整实现
/**
* 深对比:递归比较对象的所有层级
* @param {*} obj1 - 第一个值
* @param {*} obj2 - 第二个值
* @returns {boolean} - 是否相等
*/
function deepEqual(obj1, obj2) {
// 步骤 1:引用相等检查
if (obj1 === obj2) {
return true
}
// 步骤 2:null/undefined 特殊处理
if (obj1 == null || obj2 == null) {
return obj1 === obj2 // 必须都是 null 或都是 undefined
}
// 步骤 3:类型检查
if (typeof obj1 !== typeof obj2) {
return false
}
// 步骤 4:基本类型直接比较
if (typeof obj1 !== 'object') {
return obj1 === obj2
}
// 步骤 5:数组特殊处理
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
return false
}
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:Date 对象特殊处理
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
// 步骤 7:RegExp 对象特殊处理
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
// 步骤 8:普通对象处理
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
for (let key of keys1) {
if (!keys2.includes(key)) {
return false
}
if (!deepEqual(obj1[key], obj2[key])) {
// 递归!
return false
}
}
return true
}
5.3 实现细节分析
步骤 1:引用相等检查
if (obj1 === obj2) {
return true
}
考量:
- 最快的检查,包括基本类型和相同引用
- 处理
5 === 5、"hello" === "hello" - 性能优化:避免不必要的递归
特殊情况:
deepEqual(5, 5) // true(步骤 1 返回)
deepEqual('hello', 'hello') // true(步骤 1 返回)
// 相同引用的对象
const obj = { a: { b: { c: 1 } } }
deepEqual(obj, obj) // true(步骤 1 返回,避免深度递归)
步骤 2:null/undefined 特殊处理
if (obj1 == null || obj2 == null) {
return obj1 === obj2
}
为什么这样设计?
// 如果其中一个是 null/undefined,必须两个都相同
deepEqual(null, null) // true ✅
deepEqual(undefined, undefined) // true ✅
deepEqual(null, undefined) // false ❌(严格比较)
deepEqual(null, 0) // false ❌
deepEqual(undefined, '') // false ❌
与浅对比的区别:
// 浅对比:直接返回 false
if (obj1 == null || obj2 == null) {
return false // 任何一个是 null/undefined 就不等
}
// 深对比:允许两个都是 null/undefined 的情况
if (obj1 == null || obj2 == null) {
return obj1 === obj2 // 都是 null 或都是 undefined 才相等
}
步骤 3:类型检查
if (typeof obj1 !== typeof obj2) {
return false
}
考量:
- 不同类型不可能相等
- 性能优化:避免进入复杂的对象比较逻辑
- 已经排除了 null(步骤 2)
deepEqual('5', 5) // false(类型不同)
deepEqual(true, 1) // false(类型不同)
deepEqual({}, []) // false(typeof 都是 object,但在步骤 5 处理)
步骤 4:基本类型直接比较
if (typeof obj1 !== 'object') {
return obj1 === obj2
}
考量:
- 基本类型(string、number、boolean、symbol)不需要递归
- 已知类型相同(步骤 3 保证)
- 已知值不同(步骤 1 保证,否则早就返回了)
与浅对比的区别:
// 深对比:正确处理基本类型
deepEqual(42, 42) // true ✅
deepEqual('hello', 'hello') // true ✅
deepEqual(false, false) // true ✅
// 浅对比:非对象返回 false
shallowEqual(42, 42) // true(步骤 1 返回)
shallowEqual('x', 'y') // false(步骤 1 不等,步骤 3 返回)
步骤 5:数组特殊处理
if (Array.isArray(obj1) !== Array.isArray(obj2)) {
return false
}
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
}
为什么单独处理数组?
- 数组关心顺序,对象不关心
// 数组:顺序不同就不相等
deepEqual([1, 2], [2, 1]) // false ❌
// 对象:顺序无关
deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) // true ✅
- 数组用索引,对象用属性名
const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]
// 数组比较:arr1[0] vs arr2[0], arr1[1] vs arr2[1]...
// 对象比较:obj1.a vs obj2.a, obj1.b vs obj2.b...
- 性能优化
// 长度不同,立即返回
deepEqual([1, 2], [1, 2, 3]) // false(长度检查)
嵌套数组示例:
const arr1 = [1, [2, 3], [4, [5, 6]]]
const arr2 = [1, [2, 3], [4, [5, 6]]]
deepEqual(arr1, arr2) // true
// 递归过程:
// 1. deepEqual(1, 1) → true
// 2. deepEqual([2,3], [2,3]) → 递归比较 → true
// 3. deepEqual([4,[5,6]], [4,[5,6]]) → 递归比较 → true
步骤 6:Date 对象特殊处理
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
考量:
- Date 对象不能简单比较引用
- 需要比较时间戳值
- 处理无效日期:
new Date("invalid").getTime()是 NaN
const date1 = new Date('2024-01-01')
const date2 = new Date('2024-01-01')
const date3 = new Date('2024-01-02')
deepEqual(date1, date2) // true(时间戳相同)
deepEqual(date1, date3) // false(时间戳不同)
// 无效日期
const invalid1 = new Date('invalid')
const invalid2 = new Date('invalid')
deepEqual(invalid1, invalid2) // false(NaN !== NaN)
步骤 7:RegExp 对象特殊处理
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
考量:
- 正则表达式需要比较模式和标志
toString()包含完整信息:/pattern/flags
const reg1 = /hello/gi
const reg2 = /hello/gi
const reg3 = /hello/g
deepEqual(reg1, reg2) // true(模式和标志都相同)
deepEqual(reg1, reg3) // false(标志不同)
// toString() 的结果
reg1.toString() // "/hello/gi"
reg3.toString() // "/hello/g"
步骤 8:普通对象处理
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) {
return false
}
for (let key of keys1) {
if (!keys2.includes(key)) {
return false
}
if (!deepEqual(obj1[key], obj2[key])) {
// 递归!
return false
}
}
return true
考量:
- 属性数量检查:快速排除
- 属性存在性检查:确保相同的属性名
- 递归比较:每个属性值都可能是复杂对象
- 不关心顺序:对象属性顺序无关
嵌套对象示例:
const obj1 = {
user: {
name: '张三',
address: {
city: '北京',
district: '朝阳区'
}
},
scores: [90, 85, 88]
}
const obj2 = {
user: {
name: '张三',
address: {
city: '北京',
district: '朝阳区'
}
},
scores: [90, 85, 88]
}
deepEqual(obj1, obj2) // true
// 递归过程:
// 1. 比较 user 属性 → 进入递归
// a. 比较 name → "张三" === "张三" → true
// b. 比较 address → 进入递归
// i. 比较 city → "北京" === "北京" → true
// ii. 比较 district → "朝阳区" === "朝阳区" → true
// 2. 比较 scores 属性 → 进入数组递归
// a. 比较 scores[0] → 90 === 90 → true
// b. 比较 scores[1] → 85 === 85 → true
// c. 比较 scores[2] → 88 === 88 → true
5.4 使用场景
1. 单元测试断言
// Jest 测试
describe('User Service', () => {
it('should return user data', async () => {
const result = await getUserData(123)
// 使用深对比断言
expect(result).toEqual({
id: 123,
name: '张三',
profile: {
age: 25,
city: '北京'
},
hobbies: ['读书', '游泳']
})
})
})
2. 配置文件比较
function compareConfig(oldConfig, newConfig) {
const changes = []
if (!deepEqual(oldConfig, newConfig)) {
// 配置有变化,记录变更
for (let key in newConfig) {
if (!deepEqual(oldConfig[key], newConfig[key])) {
changes.push({
key,
oldValue: oldConfig[key],
newValue: newConfig[key]
})
}
}
}
return changes
}
3. 表单数据比较
function FormComponent() {
const [formData, setFormData] = useState(initialData)
const [savedData, setSavedData] = useState(initialData)
// 判断表单是否有未保存的更改
const hasUnsavedChanges = !deepEqual(formData, savedData)
return (
<form>
{/* 表单字段 */}
{hasUnsavedChanges && <div className="warning">您有未保存的更改</div>}
<button disabled={!hasUnsavedChanges}>保存</button>
</form>
)
}
六、浅对比 vs 深对比
6.1 设计哲学对比
| 特性 | 浅对比 | 深对比 |
|---|---|---|
| 设计目标 | 对象属性比较 | 通用值比较 |
| 比较深度 | 仅第一层 | 所有层级 |
| 嵌套对象 | 比较引用 | 递归比较内容 |
| 基本类型 | 步骤 1 处理 | 单独步骤处理 |
| null 处理 | 直接返回 false | 精确比较 |
| 性能 | O(n),n 是属性数 | O(m),m 是所有嵌套属性总数 |
| 使用场景 | 性能敏感 | 准确性优先 |
6.2 性能对比
时间复杂度
// 浅对比:O(n)
// n = 对象第一层属性数量
const obj = { a: 1, b: 2, c: 3, d: 4, e: 5 } // n = 5
// 深对比:O(m)
// m = 所有嵌套属性总数
const obj = {
a: { b: { c: 1 } }, // 3 个属性
d: { e: { f: 2 } }, // 3 个属性
g: 3 // 1 个属性
} // m = 7
空间复杂度
// 浅对比:O(1)
// 不需要递归栈
// 深对比:O(d)
// d = 对象嵌套深度(递归栈深度)
const obj = {
a: {
b: {
c: {
d: 1 // 深度 = 4
}
}
}
}
实际性能测试
// 测试数据
const shallowObj = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
f: 6,
g: 7,
h: 8,
i: 9,
j: 10
}
const deepObj = {
level1: {
level2: {
level3: {
level4: {
value: 1
}
}
}
}
}
// 性能测试(10000 次迭代)
console.time('浅对比')
for (let i = 0; i < 10000; i++) {
shallowEqual(shallowObj, { ...shallowObj })
}
console.timeEnd('浅对比') // ~5ms
console.time('深对比')
for (let i = 0; i < 10000; i++) {
deepEqual(deepObj, JSON.parse(JSON.stringify(deepObj)))
}
console.timeEnd('深对比') // ~15ms
6.3 使用场景对比
浅对比适用场景
// 1. React 组件优化
const Component = React.memo(MyComponent) // 使用浅对比
// 2. Redux 状态检测
function reducer(state, action) {
return { ...state, count: state.count + 1 } // 新对象,浅对比检测到
}
// 3. 频繁调用的性能敏感场景
function onScroll() {
const currentState = getScrollState()
if (!shallowEqual(currentState, prevState)) {
updateUI()
}
}
深对比适用场景
// 1. 单元测试
expect(result).toEqual(expectedResult) // Jest 的 toEqual 使用深对比
// 2. 配置比较
const configChanged = !deepEqual(oldConfig, newConfig)
// 3. 表单数据验证
const hasChanges = !deepEqual(formData, originalData)
七、类型判断方法对比
7.1 typeof
基本用法:
typeof 42 // "number"
typeof 'hello' // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof null // "object" ❗️(历史 bug)
typeof {} // "object"
typeof [] // "object"
typeof function () {} // "function"
typeof Symbol() // "symbol"
typeof BigInt(123) // "bigint"
优势:
- 简单快速
- 适合基本类型判断
- 所有环境都支持
劣势:
typeof null === "object"(著名 bug)- 无法区分数组和对象
- 无法区分 Date、RegExp 等特殊对象
7.2 instanceof
基本用法:
[] instanceof Array // true
{} instanceof Object // true
new Date() instanceof Date // true
/regex/ instanceof RegExp // true
// 继承链
class MyArray extends Array {}
const myArr = new MyArray();
myArr instanceof Array // true
myArr instanceof MyArray // true
myArr instanceof Object // true
优势:
- 可以判断具体的对象类型
- 支持原型链判断
- 适合类的实例判断
劣势:
- 跨 iframe/window 问题
// 来自不同 iframe 的数组
const iframe = document.createElement('iframe')
document.body.appendChild(iframe)
const iframeArray = iframe.contentWindow.Array
const arr1 = []
const arr2 = new iframeArray()
arr1 instanceof Array // true
arr2 instanceof Array // false ❗️(不同的 Array 构造函数)
// 解决方案
Array.isArray(arr1) // true ✅
Array.isArray(arr2) // true ✅
- 基本类型无法判断
'hello' instanceof String // false(基本类型)
new String('hello') instanceof String // true(对象包装)
- null 和 undefined 报错
null instanceof Object // false
undefined instanceof Object // false
7.3 Array.isArray()
专门用于数组判断:
Array.isArray([]) // true
Array.isArray([1, 2, 3]) // true
Array.isArray(new Array()) // true
Array.isArray(Array.prototype) // true
Array.isArray({}) // false
Array.isArray(null) // false
Array.isArray(undefined) // false
Array.isArray('hello') // false
// 解决 instanceof 的跨 frame 问题
Array.isArray(iframeArray) // true ✅
7.4 最佳实践
综合判断方案
function getType(value) {
// null 特殊处理
if (value === null) return 'null'
// undefined
if (value === undefined) return 'undefined'
// 基本类型
const type = typeof value
if (type !== 'object') return type
// 数组
if (Array.isArray(value)) return 'array'
// Date
if (value instanceof Date) return 'date'
// RegExp
if (value instanceof RegExp) return 'regexp'
// 普通对象
return 'object'
}
// 测试
console.log(getType(42)) // "number"
console.log(getType('hello')) // "string"
console.log(getType(null)) // "null"
console.log(getType([])) // "array"
console.log(getType({})) // "object"
console.log(getType(new Date())) // "date"
console.log(getType(/regex/)) // "regexp"
在对比函数中的应用
function deepEqual(obj1, obj2) {
// 1. 使用 typeof 进行基本类型检查
if (typeof obj1 !== typeof obj2) return false
// 2. 使用 Array.isArray 判断数组
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
// 3. 使用 instanceof 判断特殊对象
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
// 4. 普通对象处理
// ...
}
类型判断对比表
| 方法 | 优势 | 劣势 | 推荐场景 |
|---|---|---|---|
typeof |
简单快速 | null 是 “object”,无法区分数组 | 基本类型判断 |
instanceof |
判断具体类型,支持继承 | 跨 frame 问题 | 类的实例判断 |
Array.isArray() |
可靠,跨 frame 有效 | 只能判断数组 | 数组判断 |
Object.prototype.toString.call() |
最准确 | 语法繁琐 | 需要精确类型 |
八、现有库的实现
8.1 Lodash
_.isEqual()
const _ = require('lodash')
// 基本用法
_.isEqual({ a: 1 }, { a: 1 }) // true
_.isEqual([1, 2], [1, 2]) // true
// 嵌套对象
const obj1 = {
user: { name: '张三', age: 30 },
items: [1, 2, { id: 3, name: '商品' }]
}
const obj2 = {
user: { name: '张三', age: 30 },
items: [1, 2, { id: 3, name: '商品' }]
}
_.isEqual(obj1, obj2) // true
// 特殊对象
_.isEqual(new Date('2024-01-01'), new Date('2024-01-01')) // true
_.isEqual(/regex/gi, /regex/gi) // true
// 循环引用
const circular1 = { a: 1 }
circular1.self = circular1
const circular2 = { a: 1 }
circular2.self = circular2
_.isEqual(circular1, circular2) // true ✅
_.isEqualWith() - 自定义比较
// 自定义比较逻辑
const customizer = (val1, val2) => {
// 忽略大小写比较字符串
if (typeof val1 === 'string' && typeof val2 === 'string') {
return val1.toLowerCase() === val2.toLowerCase()
}
}
_.isEqualWith({ name: 'JOHN' }, { name: 'john' }, customizer) // true
Lodash 的浅对比实现
Lodash 没有直接的 shallowEqual,但可以这样实现:
function shallowEqualLodash(obj1, obj2) {
return _.isEqualWith(obj1, obj2, (val1, val2, key, obj1, obj2, stack) => {
// 如果有调用栈,说明在递归,直接用 === 比较
if (stack) return val1 === val2
// 否则让 lodash 继续默认比较
return undefined
})
}
8.2 Ramda
const R = require('ramda')
// R.equals - 深度比较
R.equals({ a: 1, b: 2 }, { a: 1, b: 2 }) // true
R.equals([1, 2, 3], [1, 2, 3]) // true
// 函数式编程风格
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
]
// 查找用户
const findUser = R.find(R.propEq('name', '张三'))
console.log(findUser(users)) // { id: 1, name: '张三' }
// 过滤用户
const activeUsers = users.filter(R.propEq('active', true))
8.3 React 的浅对比
React.memo
import React from 'react'
// 默认使用浅对比
const MyComponent = React.memo(function MyComponent(props) {
return <div>{props.name}</div>
})
// 自定义比较函数
const MyComponent = React.memo(
function MyComponent(props) {
return <div>{props.name}</div>
},
(prevProps, nextProps) => {
// 返回 true 表示不重新渲染
// 返回 false 表示重新渲染
return prevProps.name === nextProps.name
}
)
shallowEqual 源码(简化版)
// React 内部的 shallowEqual 实现
function shallowEqual(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++) {
const currentKey = keysA[i]
if (
!Object.prototype.hasOwnProperty.call(objB, currentKey) ||
!Object.is(objA[currentKey], objB[currentKey])
) {
return false
}
}
return true
}
注意:React 使用 Object.is() 而不是 ===,主要是为了正确处理 NaN。
九、实际应用场景
9.1 React 组件优化
使用 React.memo
import React, { useState } from 'react'
// 子组件:用户卡片
const UserCard = React.memo(function UserCard({ user, onEdit }) {
console.log('UserCard 渲染')
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>年龄:{user.age}</p>
<p>城市:{user.city}</p>
<button onClick={onEdit}>编辑</button>
</div>
)
})
// 父组件
function UserList() {
const [users, setUsers] = useState([
{ id: 1, name: '张三', age: 25, city: '北京' },
{ id: 2, name: '李四', age: 30, city: '上海' }
])
const [count, setCount] = useState(0)
const handleEdit = (userId) => {
console.log('编辑用户', userId)
}
return (
<div>
<button onClick={() => setCount(count + 1)}>点击次数:{count}</button>
{users.map((user) => (
<UserCard
key={user.id}
user={user}
onEdit={() => handleEdit(user.id)} // ❌ 每次都是新函数
/>
))}
</div>
)
}
问题:每次点击按钮,所有 UserCard 都会重新渲染,因为 onEdit 每次都是新函数。
解决方案:
import React, { useState, useCallback } from 'react';
function UserList() {
const [users, setUsers] = useState([...]);
const [count, setCount] = useState(0);
// 使用 useCallback 缓存函数
const handleEdit = useCallback((userId) => {
console.log('编辑用户', userId);
}, []); // 空依赖,函数永不变化
return (
<div>
<button onClick={() => setCount(count + 1)}>
点击次数:{count}
</button>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onEdit={() => handleEdit(user.id)} // ✅ 但还是新函数...
/>
))}
</div>
);
}
更好的解决方案:
// 修改 UserCard,让 onEdit 接收 user 对象
const UserCard = React.memo(function UserCard({ user, onEdit }) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>年龄:{user.age}</p>
<button onClick={() => onEdit(user)}>编辑</button>
</div>
)
})
function UserList() {
const handleEdit = useCallback((user) => {
console.log('编辑用户', user.id)
}, [])
return (
<div>
{users.map((user) => (
<UserCard
key={user.id}
user={user}
onEdit={handleEdit} // ✅ 相同的函数引用
/>
))}
</div>
)
}
9.2 Redux 状态管理
Redux 的不可变更新
// ❌ 错误:直接修改 state
function todoReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
state.push(action.payload) // 直接修改,浅对比检测不到
return state
default:
return state
}
}
// ✅ 正确:返回新对象
function todoReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload] // 新数组,浅对比检测到
case 'UPDATE_TODO':
return state.map((todo) =>
todo.id === action.id
? { ...todo, ...action.updates } // 新对象
: todo
)
case 'DELETE_TODO':
return state.filter((todo) => todo.id !== action.id) // 新数组
default:
return state
}
}
使用 Immer 简化不可变更新
import { produce } from 'immer'
function todoReducer(state = [], action) {
return produce(state, (draft) => {
switch (action.type) {
case 'ADD_TODO':
draft.push(action.payload) // 看起来是可变的,但 Immer 会返回新对象
break
case 'UPDATE_TODO':
const todo = draft.find((t) => t.id === action.id)
if (todo) {
Object.assign(todo, action.updates)
}
break
case 'DELETE_TODO':
return draft.filter((todo) => todo.id !== action.id)
}
})
}
9.3 单元测试断言
Jest 测试
// 使用深对比断言
describe('User API', () => {
it('should return user data', async () => {
const result = await fetchUser(123)
// toEqual 使用深对比
expect(result).toEqual({
id: 123,
name: '张三',
profile: {
age: 25,
city: '北京',
hobbies: ['读书', '游泳']
}
})
})
it('should create new user', async () => {
const newUser = {
name: '李四',
age: 30
}
const result = await createUser(newUser)
// 部分匹配
expect(result).toMatchObject({
name: '李四',
age: 30,
id: expect.any(Number) // id 是任意数字
})
})
})
自定义匹配器
// 深对比但忽略某些字段
expect.extend({
toEqualIgnoringFields(received, expected, ignoreFields) {
const filterObj = (obj) => {
const filtered = { ...obj }
ignoreFields.forEach((field) => delete filtered[field])
return filtered
}
const pass = deepEqual(filterObj(received), filterObj(expected))
return {
pass,
message: () => `Objects are ${pass ? '' : 'not '}equal (ignoring ${ignoreFields.join(', ')})`
}
}
})
// 使用
expect(result).toEqualIgnoringFields(expected, ['createdAt', 'updatedAt'])
十、高级话题
10.1 循环引用处理
问题
const obj1 = { a: 1 }
obj1.self = obj1 // 循环引用
const obj2 = { a: 1 }
obj2.self = obj2
// 基础实现会无限递归
deepEqual(obj1, obj2) // 💥 Maximum call stack size exceeded
解决方案:使用 WeakMap 跟踪
function deepEqual(obj1, obj2, visited = new WeakMap()) {
// 引用相等
if (obj1 === obj2) return true
// null/undefined
if (obj1 == null || obj2 == null) {
return obj1 === obj2
}
// 类型检查
if (typeof obj1 !== typeof obj2) return false
if (typeof obj1 !== 'object') return obj1 === obj2
// 🔑 检查循环引用
if (visited.has(obj1)) {
return visited.get(obj1) === obj2
}
// 记录已访问的对象
visited.set(obj1, obj2)
// 数组处理
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
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], visited)) return false
}
return true
}
// Date、RegExp 处理
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
// 普通对象
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) return false
for (let key of keys1) {
if (!keys2.includes(key)) return false
if (!deepEqual(obj1[key], obj2[key], visited)) return false
}
return true
}
// 测试循环引用
const circular1 = { a: 1 }
circular1.self = circular1
const circular2 = { a: 1 }
circular2.self = circular2
console.log(deepEqual(circular1, circular2)) // true ✅
10.2 性能优化策略
1. 早期退出
function deepEqual(obj1, obj2) {
// 策略 1:引用相等 -> 立即返回(最快)
if (obj1 === obj2) return true
// 策略 2:类型不同 -> 立即返回(很快)
if (typeof obj1 !== typeof obj2) return false
// 策略 3:长度不同 -> 立即返回(快)
if (Array.isArray(obj1) && obj1.length !== obj2.length) {
return false
}
// 策略 4:属性数量不同 -> 立即返回(快)
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) return false
// 策略 5:逐个比较(最慢)
for (let key of keys1) {
if (!deepEqual(obj1[key], obj2[key])) return false
}
return true
}
2. 递归深度限制
function deepEqual(obj1, obj2, depth = 0, maxDepth = 100) {
if (depth > maxDepth) {
throw new Error(`Maximum depth ${maxDepth} exceeded`)
}
// ... 其他逻辑
// 递归时增加深度
return deepEqual(obj1[key], obj2[key], depth + 1, maxDepth)
}
3. 智能选择对比方式
function getObjectDepth(obj, depth = 1) {
if (!obj || typeof obj !== 'object') return depth
const values = Object.values(obj)
if (values.length === 0) return depth
return Math.max(...values.map((val) => getObjectDepth(val, depth + 1)))
}
function smartEqual(obj1, obj2, maxDepth = 3) {
// 先尝试浅对比(快)
if (shallowEqual(obj1, obj2)) return true
// 如果对象层级不深,使用深对比
const depth1 = getObjectDepth(obj1)
const depth2 = getObjectDepth(obj2)
if (depth1 <= maxDepth && depth2 <= maxDepth) {
return deepEqual(obj1, obj2)
}
// 层级太深,返回 false 避免性能问题
return false
}
10.3 边界情况处理
1. Symbol 类型
function deepEqual(obj1, obj2) {
// ... 其他逻辑
// 处理 Symbol 属性
const symbols1 = Object.getOwnPropertySymbols(obj1)
const symbols2 = Object.getOwnPropertySymbols(obj2)
if (symbols1.length !== symbols2.length) return false
for (let sym of symbols1) {
if (!symbols2.includes(sym)) return false
if (!deepEqual(obj1[sym], obj2[sym])) return false
}
return true
}
2. 不可枚举属性
function deepEqualWithNonEnum(obj1, obj2) {
// 获取所有属性(包括不可枚举)
const keys1 = Object.getOwnPropertyNames(obj1)
const keys2 = Object.getOwnPropertyNames(obj2)
if (keys1.length !== keys2.length) return false
for (let key of keys1) {
if (!keys2.includes(key)) return false
if (!deepEqual(obj1[key], obj2[key])) return false
}
return true
}
3. Getter/Setter
const obj1 = {
_value: 1,
get value() {
return this._value
},
set value(v) {
this._value = v
}
}
const obj2 = {
_value: 1,
get value() {
return this._value
},
set value(v) {
this._value = v
}
}
// 标准深对比会比较 getter 的返回值
deepEqual(obj1, obj2) // 比较 obj1.value 和 obj2.value
// 如果要比较属性描述符
function deepEqualWithDescriptors(obj1, obj2) {
const keys = Object.keys(obj1)
for (let key of keys) {
const desc1 = Object.getOwnPropertyDescriptor(obj1, key)
const desc2 = Object.getOwnPropertyDescriptor(obj2, key)
if (!deepEqual(desc1, desc2)) return false
}
return true
}
4. 原型链
function deepEqualWithProto(obj1, obj2) {
// 比较原型
if (Object.getPrototypeOf(obj1) !== Object.getPrototypeOf(obj2)) {
return false
}
// 比较自有属性
return deepEqual(obj1, obj2)
}
// 示例
class Person {
constructor(name) {
this.name = name
}
}
class Animal {
constructor(name) {
this.name = name
}
}
const person = new Person('张三')
const animal = new Animal('张三')
deepEqual(person, animal) // true(只比较属性)
deepEqualWithProto(person, animal) // false(原型不同)
十一、最佳实践
1. 选择合适的对比方式
// ✅ 组件 props - 使用浅对比
const Component = React.memo(MyComponent)
// ✅ 测试断言 - 使用深对比
expect(result).toEqual(expected)
// ✅ 性能敏感场景 - 优先浅对比
if (shallowEqual(newState, oldState)) {
return // 避免不必要的更新
}
// ✅ 需要准确性 - 使用深对比
const configChanged = !deepEqual(oldConfig, newConfig)
2. 避免常见陷阱
// ❌ 错误:直接比较数组
const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]
console.log(arr1 === arr2) // false
// ✅ 正确:使用深对比
console.log(deepEqual(arr1, arr2)) // true
// ❌ 错误:忽略嵌套对象
if (obj1.user === obj2.user) {
// 只比较引用,不比较内容
}
// ✅ 正确:使用深对比
if (deepEqual(obj1.user, obj2.user)) {
// 比较内容
}
3. 优化 React 组件
// ❌ 不好:每次渲染都创建新对象
function Component() {
return <Child config={{ theme: 'dark' }} />
}
// ✅ 好:使用 useMemo
function Component() {
const config = useMemo(() => ({ theme: 'dark' }), [])
return <Child config={config} />
}
// ❌ 不好:每次渲染都创建新函数
function Component() {
return <Child onClick={() => console.log('click')} />
}
// ✅ 好:使用 useCallback
function Component() {
const handleClick = useCallback(() => {
console.log('click')
}, [])
return <Child onClick={handleClick} />
}
4. 使用现有库
// ✅ 推荐:使用成熟的库
import { isEqual } from 'lodash'
// 而不是自己实现(除非有特殊需求)
function myDeepEqual(obj1, obj2) {
// 可能有 bug,性能可能不佳...
}
5. 编写可维护的代码
// ✅ 好:清晰的注释和边界情况处理
function deepEqual(obj1, obj2) {
// 1. 引用相等 - 最快的路径
if (obj1 === obj2) return true
// 2. null/undefined - 避免 typeof null === 'object' 陷阱
if (obj1 == null || obj2 == null) {
return obj1 === obj2
}
// 3. 类型检查 - 早期退出
if (typeof obj1 !== typeof obj2) return false
// ... 其他逻辑
}
6. 性能监控
function performanceTest() {
const testData = generateLargeObject()
console.time('shallowEqual')
for (let i = 0; i < 1000; i++) {
shallowEqual(testData, testData)
}
console.timeEnd('shallowEqual')
console.time('deepEqual')
for (let i = 0; i < 1000; i++) {
deepEqual(testData, testData)
}
console.timeEnd('deepEqual')
}
十二、生活化记忆方法
在了解了所有技术细节之后,如何牢记这些概念?让我们用生活化的比喻和记忆口诀来帮助记忆。
12.1 对比函数的角色定位
浅对比 = 门卫大爷
只看身份证封面信息
function shallowEqual(obj1, obj2) {
// 1. 是同一个人吗?(指纹识别)
if (Object.is(obj1, obj2)) return true // "诶,你就是刚才那个人!"
// 2. 有人没带身份证吗?
if (obj1 == null || obj2 == null) return false // "没证件不让进!"
// 3. 都带了证件吗?
if (typeof obj1 !== 'object') return false // "这不是身份证,是驾驶证!"
// 4. 都是同类证件吗?
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
// "一个是身份证,一个是护照,不能比!"
// 5. 门卫只看证件表面信息
for (let key of Object.keys(obj1)) {
if (!Object.is(obj1[key], obj2[key])) return false
// "姓名不一样!" "照片不一样!"(但不会拆开看里面的夹层)
}
}
深对比 = 专业验证员
连身份证里夹的小纸条都要检查
function deepEqual(obj1, obj2) {
// 1. 指纹识别(最快)
if (Object.is(obj1, obj2)) return true // "指纹一样,肯定是同一人!"
// 2. 基本检查
if (obj1 == null || obj2 == null) return obj1 === obj2
// "如果有人没带证件,另一个人也必须没带才算一样"
// 3. 证件类型检查
if (typeof obj1 !== typeof obj2) return false
// "身份证和驾驶证不能比较"
// 4. 简单证件直接看
if (typeof obj1 !== 'object') return false
// "如果是名片这种简单的,直接看就行"
// 5-8. 专业检验不同类型证件...
}
12.2 执行流程记忆口诀
总体流程:“指证类基特递”
- 指纹识别(Object.is - 引用相等)
- 证件检查(null/undefined 检查)
- 类型核对(typeof检查)
- 基本放行(基本类型直接比较)
- 特殊处理(Date、RegExp等)
- 递归检查(普通对象/数组)
####性能记忆:“能偷懒就偷懒”
- 指纹识别最快,先试
- 证件都没带,直接拒绝
- 类型不对,不用细看
- 简单证件,看一眼就够
浅vs深记忆:“门卫vs海关”
- 门卫:看个大概,速度快
- 海关:翻箱倒柜,准确全面
12.3 完整实现与记忆技巧
修正后的完整浅对比
/**
* 浅对比 - 门卫大爷版
* 只看第一层,不深入检查
*/
function shallowEqual(obj1, obj2) {
// 1. 指纹识别 (最快检查)
if (Object.is(obj1, obj2)) return true
// 2. 证件检查 (null/undefined)
if (obj1 == null || obj2 == null) return false
// 3. 证件类型 (必须都是对象)
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false
// 4. 证件格式 (数组 vs 对象)
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
// 5. 数组的浅层比较
if (Array.isArray(obj1)) {
if (obj1.length !== obj2.length) return false
for (let i = 0; i < obj1.length; i++) {
if (!Object.is(obj1[i], obj2[i])) return false // 浅层比较
}
return true
}
// 6. 对象的浅层比较
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 (!Object.is(obj1[key], obj2[key])) return false // 浅层比较
}
return true
}
修正后的完整深对比
/**
* 深对比 - 海关检查员版
* 递归检查所有层级
*/
function deepEqual(obj1, obj2) {
// 1. 指纹识别 (处理NaN、+0/-0等特殊情况)
if (Object.is(obj1, obj2)) return true
// 2. 证件检查 (null/undefined)
if (obj1 == null || obj2 == null) return false
// 3. 证件类型一致性
if (typeof obj1 !== typeof obj2) return false
// 4. 基本类型和函数 (名片直接看)
if (typeof obj1 !== 'object') {
return false // 已经在第1步处理了,到这里说明不相等
}
// 5. 数组类型检查
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
// 6. 数组的深层比较
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
}
// 7. Date对象 (时间戳印章)
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
// 8. RegExp对象 (印章图案)
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
// 9. 普通对象的深层比较
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
}
特殊情况记忆:“指纹印章时间戳”
- Object.is = 指纹识别(处理NaN和+0/-0)
- RegExp = 印章图案(toString比较)
- Date = 时间戳印章(getTime比较)
递归vs非递归:“挖坑vs看表面”
- 浅对比:
Object.is(obj1[key], obj2[key])- “看表面” - 深对比:
deepEqual(obj1[key], obj2[key])- “继续挖坑”
关键差异总结表
| 特性 | 浅对比 | 深对比 |
|---|---|---|
| 比喻 | 门卫大爷 | 海关检查员 |
| 基本类型 | Object.is() |
Object.is() |
| 对象属性 | Object.is() (不递归) |
deepEqual() (递归) |
| 数组元素 | Object.is() (不递归) |
deepEqual() (递归) |
| Date对象 | 引用比较 | getTime() 比较 |
| RegExp对象 | 引用比较 | toString() 比较 |
| 性能 | 快⚡ | 慢🐌 |
| 准确性 | 有限 | 全面 |
终极记忆法
核心原则: “能偷懒就偷懒,该认真就认真”
- Object.is() 是万能钥匙,处理所有边界情况
- 浅对比 是快递小哥,只看包装不拆箱
- 深对比 是海关大叔,连夹层都要翻
记住这些,你就掌握了JavaScript对比的精髓!🎯
12.4 浅对比与深对比的设计哲学差异
浅对比 = 房产中介(对象属性专家)
“我只负责看房子的第一层信息”
// 房产中介的工作范围
shallowEqual(房子A, 房子B) // ✅ "这是我的专业领域"
shallowEqual('张三', '李四') // ❌ "我不比较人名,我只看房子"
shallowEqual(42, 42) // ❌ "我不比较数字,我只看房子"
// 类型检查逻辑
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return false // "不是房子?那不是我的业务"
}
处理流程:
客户来了
↓
是房子吗? → 不是 → 拒绝 ❌
↓ 是
看第一层信息
↓
快速给结论 ✅
深对比 = 万能鉴定师(全能比较专家)
“什么东西都能鉴定,但必须是同类”
// 万能鉴定师的工作范围
deepEqual(房子A, 房子B) // ✅ "房子我能鉴定"
deepEqual('张三', '李四') // ✅ "名字我也能鉴定"
deepEqual(42, 42) // ✅ "数字我还能鉴定"
// 类型检查逻辑
if (typeof obj1 !== typeof obj2) {
return false // "类型不同,没法比较"
}
// 然后按类型分流处理
处理流程:
客户来了任何东西
↓
类型相同吗? → 不同 → 拒绝 ❌
↓ 相同
分类处理:
├── 数字 → 数字专家
├── 字符串 → 文本专家
├── 数组 → 数组专家(递归)
├── Date → 时间专家
├── RegExp → 正则专家
└── 对象 → 对象专家(递归)
↓
详细鉴定结论 ✅
类型检查的本质差异
| 特性 | 浅对比 | 深对比 |
|---|---|---|
| 设计目标 | 对象属性快速比较器 | JavaScript 通用值比较器 |
| 接受类型 | 只要对象(必须都是 object) | 接受一切(但类型必须相同) |
| 类型检查 | typeof obj !== 'object' |
typeof obj1 !== typeof obj2 |
| 拒绝理由 | “不是对象我不管” | “类型不同没法比” |
| 处理分支 | 简单(只有数组和对象两种) | 复杂(多种类型分流) |
| 应用场景 | React props、Redux state | 测试断言、配置比较 |
分支处理的对比
浅对比的分支树:
Object.is() → 相同?返回 true
↓ 不同
null 检查 → 有 null?返回 false
↓ 都不是 null
对象检查 → 不是对象?返回 false
↓ 都是对象
├── 数组 → 浅层遍历
└── 普通对象 → 浅层遍历
深对比的分支树:
Object.is() → 相同?返回 true
↓ 不同
null 检查 → 处理空值
↓
类型检查 → 类型不同?返回 false
↓ 类型相同
按类型分流:
├── 基本类型 → 已在 Object.is 处理,返回 false
├── 函数 → 引用比较
└── 对象类型 → 进一步分流:
├── 数组 → 递归遍历
├── Date → getTime()比较
├── RegExp → toString()比较
└── 普通对象 → 递归遍历
12.5 完整实现代码(带生活化注释)
浅对比完整实现
/**
* 浅对比 - 门卫大爷版
* 只看身份证封面信息,不拆开看里面
*/
function shallowEqual(obj1, obj2) {
// 1. 指纹识别 - "诶,你就是刚才那个人!"
if (Object.is(obj1, obj2)) return true
// 2. 证件检查 - "没带身份证的不让进!"
if (obj1 == null || obj2 == null) return false
// 3. 证件类型 - "必须是身份证,驾驶证不行!"
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false
// 4. 证件格式 - "一个竖版一个横版,不能比!"
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
// 5. 数组的浅层比较 - "只看数组表面的每一项"
if (Array.isArray(obj1)) {
if (obj1.length !== obj2.length) return false
for (let i = 0; i < obj1.length; i++) {
if (!Object.is(obj1[i], obj2[i])) return false // 不深入
}
return true
}
// 6. 对象的浅层比较 - "只看对象第一层属性"
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 (!Object.is(obj1[key], obj2[key])) return false // 不深入
}
return true
}
深对比完整实现
/**
* 深对比 - 海关检查员版
* 连身份证里夹的小纸条都要检查
*/
function deepEqual(obj1, obj2) {
// 1. 指纹识别 - "指纹一样,肯定是同一人!"(处理 NaN、+0/-0)
if (Object.is(obj1, obj2)) return true
// 2. 证件检查 - "如果有人没带证件,另一个人也必须没带"
if (obj1 == null || obj2 == null) return false
// 3. 证件类型 - "身份证和驾驶证不能比较"
if (typeof obj1 !== typeof obj2) return false
// 4. 简单证件直接看 - "名片这种简单的,直接看就行"
if (typeof obj1 !== 'object') {
return false // Object.is 已处理,到这说明不相等
}
// 5. 数组类型检查 - "都是数组还是都是对象?"
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false
// 6. 数组的深层比较 - "打开每个格子,递归检查"
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
}
// 7. Date 对象 - "时间戳印章,看印出来的时间"
if (obj1 instanceof Date && obj2 instanceof Date) {
return obj1.getTime() === obj2.getTime()
}
// 8. RegExp 对象 - "印章图案,印在纸上看效果"
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
return obj1.toString() === obj2.toString()
}
// 9. 普通对象的深层比较 - "打开每个抽屉,递归检查"
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
}
12.6 终极记忆法汇总
口诀:“指证类基特数对”
- 指纹识别(Object.is)
- 证件检查(null/undefined)
- 类型核对(typeof)
- 基本放行(基本类型)
- 特殊处理(Date/RegExp)
- 数组递归(数组处理)
- 对象递归(对象处理)
核心比喻记忆
| 概念 | 浅对比 | 深对比 |
|---|---|---|
| 角色 | 门卫大爷 | 海关检查员 |
| 工作 | 看证件表面 | 翻箱倒柜 |
| 速度 | 快如闪电 ⚡ | 慢慢悠悠 🐌 |
| 原则 | 能偷懒就偷懒 | 该认真就认真 |
| NaN 处理 | 失忆症识别 | 失忆症识别 |
| Date 处理 | 不拆开看 | 看时间戳 |
| RegExp 处理 | 不拆开看 | 印出来看 |
十三、最佳实践
1. 选择合适的对比方式
// ✅ 组件 props - 使用浅对比
const Component = React.memo(MyComponent)
// ✅ 测试断言 - 使用深对比
expect(actualResult).toEqual(expectedResult)
// ✅ 性能敏感 - 优先浅对比
if (shallowEqual(newState, oldState)) {
return // 避免不必要的更新
}
2. 避免常见陷阱
// ❌ 错误:直接比较数组
const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3]
console.log(arr1 === arr2) // false
// ✅ 正确:使用深对比
console.log(deepEqual(arr1, arr2)) // true
3. 优化 React 组件
// ❌ 不好:每次渲染都创建新对象
function Component() {
return <Child config={{ theme: 'dark' }} />
}
// ✅ 好:使用 useMemo
function Component() {
const config = useMemo(() => ({ theme: 'dark' }), [])
return <Child config={config} />
}
4. 使用现有库
// ✅ 推荐:使用成熟的库
import { isEqual } from 'lodash'
// 而不是自己实现(除非有特殊需求)
5. 性能监控
function performanceTest() {
const testData = generateLargeObject()
console.time('shallowEqual')
for (let i = 0; i < 1000; i++) {
shallowEqual(testData, testData)
}
console.timeEnd('shallowEqual')
console.time('deepEqual')
for (let i = 0; i < 1000; i++) {
deepEqual(testData, testData)
}
console.timeEnd('deepEqual')
}
十四、总结
核心概念回顾
-
Falsy 和 Truthy
- JavaScript 中只有 8 个 falsy 值
- 其他所有值都是 truthy
- 这是 ECMAScript 规范明确定义的
-
相等运算符
===:严格相等,不进行类型转换==:松散相等,会进行类型转换Object.is():解决 NaN 和 ±0 的特殊情况null == undefined是唯一推荐使用==的场景
-
浅对比 vs 深对比
- 浅对比:只比较第一层属性,快速但有限
- 深对比:递归比较所有层级,全面但慢
- 选择取决于具体场景和性能需求
-
类型判断
typeof:适合基本类型,但有typeof null === "object"陷阱instanceof:适合对象类型,但有跨 frame 问题Array.isArray():专门用于数组判断,最可靠
实践建议
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| React 组件优化 | 浅对比 | 性能好,符合不可变数据模式 |
| Redux 状态检测 | 浅对比 | 检测第一层变化即可 |
| 单元测试断言 | 深对比 | 需要准确性 |
| 配置文件比较 | 深对比 | 需要完整比较 |
| 频繁调用 | 浅对比 | 性能敏感 |
| 复杂嵌套对象 | 成熟库(Lodash) | 处理边界情况完善 |
记忆口诀
浅对比:“看房子外观,只检查表面”
深对比:“验房师检查,彻底查所有细节”
严格相等 ===:“严格老师,一个字母都不能错”
松散相等 ==:“宽松朋友,大概意思对就行”
特殊值:
null == undefined:“空兄弟,互相认识”NaN !== NaN:“NaN 是独狼,谁都不认”- 对象比较:“比的是身份证(引用),不是长相”
最终建议
- 默认使用严格相等
===,只在检查 null/undefined 时用== - React 开发优先使用浅对比,配合不可变数据
- 测试断言使用深对比,确保准确性
- 优先使用成熟库(Lodash、Ramda),除非有特殊需求
- 注意性能,避免在循环中进行深对比
- 处理边界情况,如循环引用、特殊对象等
附录
相关资源
文档信息
- 创建日期:2025-10-11
- 最后更新:2025-10-11
- 版本:3.1
- 适用于:JavaScript/React 开发者
版本更新记录:
-
v3.1 (2025-10-11)
- 补充第二章核心概念:“JavaScript 中所有值都可以转换为布尔值”
- 添加 ECMAScript 规范权威引用(7.1.2 ToBoolean)
- 新增 2.4:记忆总结(Falsy 口诀"假零空难"、实用技巧)
- 强化布尔转换机制在对比函数中的应用说明
-
v3.0 (2025-10-11)
- 新增 12.4:浅对比与深对比的设计哲学差异详解
- 新增 12.5:带生活化注释的完整实现代码
- 新增 12.6:终极记忆法汇总(口诀+比喻表格)
- 补充了类型检查本质差异、分支处理对比
- 完善了房产中介 vs 万能鉴定师的角色定位比喻
-
v2.0 (2025-10-11)
- 新增第四章:实现前的重要概念(NaN、Object.is、函数比较、设计差异)
- 新增第十二章:生活化记忆方法(角色定位、执行流程口诀)
- 所有代码实现统一使用
Object.is()替代===
-
v1.0 (2025-10-11)
- 初始版本,完整技术解析
核心亮点:
✅ 完整性 - 从基础到高级,覆盖所有细节
✅ 生活化 - 门卫大爷、海关检查员、房产中介等生动比喻
✅ 记忆法 - "指证类基特数对"口诀,轻松记忆
✅ 实战性 - 包含 Lodash、React 等主流库的实际应用
✅ 正确性 - 与 ECMAScript 规范和主流库保持一致
更多推荐
所有评论(0)