JavaScript 对象比较指南:浅对比与深对比

从基础概念到实战应用的技术解析

目录


一、核心概念

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 对象比较的是引用(内存地址),不是内容
  • 就像问:“这两个长得一模一样的双胞胎是同一个人吗?”
  • 答案取决于你怎么定义"相同":
    • 引用相等:必须是同一个人(同一个对象)
    • 结构相等:长得一样就行(内容相同)
实际需求

在实际开发中,我们经常需要判断两个对象的内容是否相同:

  1. React 组件优化:判断 props 是否变化,决定是否重新渲染
  2. 状态管理:判断状态是否真的改变了
  3. 单元测试:断言两个对象是否相等
  4. 数据比较:比较表单数据、配置数据等

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
  • ""(空字符串)、nullundefined
  • 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 严格相等 ===

记忆口诀:“我很挑剔,必须完全一样!”

规则
  1. 类型必须相同
  2. 值必须相同
  3. 对象比较引用
  4. 不进行类型转换
// 基本类型
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 松散相等 ==

记忆口诀:“我很宽松,差不多就行!”

规则
  1. 会进行类型转换
  2. 转换后再比较
  3. 有特殊规则
类型转换规则
// 数字和字符串
"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 // 一行解决多个问题!
  // ...
}

一石二鸟的效果:

  1. 与主流库保持一致
// 现在我们的行为和它们一致了
deepEqual(NaN, NaN) // true ✅ (像Lodash)
deepEqual(+0, -0) // false ✅ (像Ramda)
  1. 与JavaScript新标准保持一致
// Object.is是ES6引入的"更好的相等比较"
Object.is(NaN, NaN) // true
Object.is(+0, -0) // false
Object.is() 的设计哲学

Object.is() 被设计为"SameValue"算法,它的理念是:

  • “在JavaScript值的层面,它们是否为同一个值”
  • 而不是IEEE 754的"数学相等"概念
生态系统的演进
// 历史演进
=== 运算符 (1995)  -> 遵循IEEE 754NaN !== 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
}

为什么单独处理数组?

  1. 数组关心顺序,对象不关心
// 数组:顺序不同就不相等
deepEqual([1, 2], [2, 1]) // false ❌

// 对象:顺序无关
deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }) // true ✅
  1. 数组用索引,对象用属性名
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...
  1. 性能优化
// 长度不同,立即返回
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

优势

  • 可以判断具体的对象类型
  • 支持原型链判断
  • 适合类的实例判断

劣势

  1. 跨 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 ✅
  1. 基本类型无法判断
'hello' instanceof String // false(基本类型)
new String('hello') instanceof String // true(对象包装)
  1. 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 执行流程记忆口诀

总体流程:“指证类基特递”
  1. 纹识别(Object.is - 引用相等)
  2. 件检查(null/undefined 检查)
  3. 型核对(typeof检查)
  4. 本放行(基本类型直接比较)
  5. 殊处理(Date、RegExp等)
  6. 归检查(普通对象/数组)

####性能记忆:“能偷懒就偷懒”

  • 指纹识别最快,先试
  • 证件都没带,直接拒绝
  • 类型不对,不用细看
  • 简单证件,看一眼就够
浅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')
}

十四、总结

核心概念回顾

  1. Falsy 和 Truthy

    • JavaScript 中只有 8 个 falsy 值
    • 其他所有值都是 truthy
    • 这是 ECMAScript 规范明确定义的
  2. 相等运算符

    • ===:严格相等,不进行类型转换
    • ==:松散相等,会进行类型转换
    • Object.is():解决 NaN 和 ±0 的特殊情况
    • null == undefined 是唯一推荐使用 == 的场景
  3. 浅对比 vs 深对比

    • 浅对比:只比较第一层属性,快速但有限
    • 深对比:递归比较所有层级,全面但慢
    • 选择取决于具体场景和性能需求
  4. 类型判断

    • typeof:适合基本类型,但有 typeof null === "object" 陷阱
    • instanceof:适合对象类型,但有跨 frame 问题
    • Array.isArray():专门用于数组判断,最可靠

实践建议

场景 推荐方案 理由
React 组件优化 浅对比 性能好,符合不可变数据模式
Redux 状态检测 浅对比 检测第一层变化即可
单元测试断言 深对比 需要准确性
配置文件比较 深对比 需要完整比较
频繁调用 浅对比 性能敏感
复杂嵌套对象 成熟库(Lodash) 处理边界情况完善

记忆口诀

浅对比:“看房子外观,只检查表面”
深对比:“验房师检查,彻底查所有细节”

严格相等 ===:“严格老师,一个字母都不能错”
松散相等 ==:“宽松朋友,大概意思对就行”

特殊值

  • null == undefined:“空兄弟,互相认识”
  • NaN !== NaN:“NaN 是独狼,谁都不认”
  • 对象比较:“比的是身份证(引用),不是长相”

最终建议

  1. 默认使用严格相等 ===,只在检查 null/undefined 时用 ==
  2. React 开发优先使用浅对比,配合不可变数据
  3. 测试断言使用深对比,确保准确性
  4. 优先使用成熟库(Lodash、Ramda),除非有特殊需求
  5. 注意性能,避免在循环中进行深对比
  6. 处理边界情况,如循环引用、特殊对象等

附录

相关资源

文档信息

  • 创建日期: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 规范和主流库保持一致

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐