手写深拷贝与浅比较深度解析:从循环引用到特殊类型的全面覆盖
一句话概括
深拷贝和浅比较是 JavaScript 引用类型操作的”双刃剑”——深拷贝需要遍历对象图、处理循环引用、兼容所有内置类型(Symbol/Map/Set/Date/RegExp/BigInt等),而浅比较则需要精确判断值类型和引用类型的差异,是 React 性能优化的核心理论基础。
背景与意义
引用类型的困境
JavaScript 的对象赋值是按引用传递的,这意味着:
1
2
3
const state = { user: { name: 'Alice', settings: { theme: 'dark' } } };
const newState = state; // 这不是拷贝,这是起了个别名
newState.user.name = 'Bob'; // state.user.name 也被改了 ❌
这个问题在以下场景中尤为突出:
- 状态管理:React/Vue 中修改状态需返回新对象,不可变数据是原则
- 函数安全:函数内部不应修改传入的参数对象
- 数据持久化:存储前需要序列化,避免引用导致的意外修改
- 测试断言:比较预期值和实际值时不希望引用干扰
为什么 JSON.parse(JSON.stringify()) 不够用
这是很多开发者”偷懒”用的深拷贝方法,但它有多个致命弱点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const problematic = {
date: new Date(),
regex: /hello/gi,
fn: () => 'hello',
undefined: undefined,
symbol: Symbol('id'),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
infinity: Infinity,
nan: NaN,
cyclic: null,
};
problematic.cyclic = problematic; // 循环引用
JSON.parse(JSON.stringify(problematic));
// ❌ Date 变成字符串
// ❌ RegExp 变成 {}
// ❌ Function 被丢弃
// ❌ undefined 被丢弃
// ❌ Symbol 被丢弃
// ❌ Map/Set 变成 {}
// ❌ Infinity 变成 null
// ❌ NaN 变成 null
// ❌ 循环引用抛错:Converting circular structure to JSON
概念与定义
三种拷贝的层次
| 层级 | 行为 | 示例 |
|---|---|---|
| 引用赋值 | 两个变量指向同一个对象 | const b = a; b.x=1 → a.x===1 |
| 浅拷贝 | 只复制最外层,嵌套对象共享引用 | {...a} 或 Object.assign({}, a) |
| 深拷贝 | 递归复制所有层级,完全独立 | structuredClone(a) 或手写实现 |
浅比较(Shallow Equality)
比较两个值是否”看起来相等”:对于基本类型比较值,对于引用类型比较引用地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// React 函数组件的浅比较
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;
return keysA.every(key =>
Object.prototype.hasOwnProperty.call(objB, key) &&
Object.is(objA[key], objB[key])
);
}
最小示例
基础深拷贝(递归版)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function deepClone(obj, hash = new WeakMap()) {
// 基本类型直接返回
if (obj === null || typeof obj !== 'object') return obj;
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
// 处理 Date
if (obj instanceof Date) return new Date(obj.getTime());
// 处理 RegExp
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
// 处理 Map
if (obj instanceof Map) {
const map = new Map();
hash.set(obj, map);
obj.forEach((value, key) => {
map.set(deepClone(key, hash), deepClone(value, hash));
});
return map;
}
// 处理 Set
if (obj instanceof Set) {
const set = new Set();
hash.set(obj, set);
obj.forEach(value => {
set.add(deepClone(value, hash));
});
return set;
}
// 处理 Array 和普通 Object
const result = Array.isArray(obj) ? [] : {};
hash.set(obj, result);
// 复制普通属性
for (const key of Object.keys(obj)) {
result[key] = deepClone(obj[key], hash);
}
// 复制 Symbol 属性
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const symKey of symbolKeys) {
result[symKey] = deepClone(obj[symKey], hash);
}
return result;
}
浅比较最小实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function shallowEqual(a, b) {
// 先处理 NaN 和 +0/-0
if (Object.is(a, b)) return true;
if (typeof a !== typeof b) return false;
if (typeof a !== 'object' || a === null || b === null) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (!Object.is(a[key], b[key])) return false;
}
return true;
}
核心知识点拆解
1. 深拷贝:从递归到栈安全的优化
简单递归深拷贝的最大问题是 栈溢出(Stack Overflow):当对象深度超过调用栈深度时(浏览器通常 ~10000 层),递归会崩溃。
迭代版深拷贝(使用显式栈):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function deepCloneIterative(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const root = Array.isArray(obj) ? [] : {};
const stack = [{ source: obj, target: root }];
const visited = new WeakMap();
visited.set(obj, root);
while (stack.length > 0) {
const { source, target } = stack.pop();
// 处理普通属性
for (const key of Object.keys(source)) {
const value = source[key];
if (value === null || typeof value !== 'object') {
target[key] = value;
} else if (value instanceof Date) {
target[key] = new Date(value.getTime());
} else if (value instanceof RegExp) {
target[key] = new RegExp(value.source, value.flags);
} else if (visited.has(value)) {
target[key] = visited.get(value);
} else {
const clone = Array.isArray(value) ? [] : {};
visited.set(value, clone);
target[key] = clone;
stack.push({ source: value, target: clone });
}
}
// 处理 Symbol 属性
const symKeys = Object.getOwnPropertySymbols(source);
for (const symKey of symKeys) {
const value = source[symKey];
if (value === null || typeof value !== 'object') {
target[symKey] = value;
} else if (visited.has(value)) {
target[symKey] = visited.get(value);
} else {
const clone = Array.isArray(value) ? [] : {};
visited.set(value, clone);
target[symKey] = clone;
stack.push({ source: value, target: clone });
}
}
}
return root;
}
2. 深比较(Deep Equality)
深比较需要递归比较对象的所有属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
function deepEqual(a, b, visited = new Set()) {
// 同一引用
if (Object.is(a, b)) return true;
// 类型不同
if (typeof a !== typeof b) return false;
// 非对象 / null
if (a === null || b === null || typeof a !== 'object') return false;
// 处理循环引用
if (visited.has(a) || visited.has(b)) {
return visited.has(a) && visited.has(b);
}
visited.add(a);
visited.add(b);
// 处理 Date
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// 处理 RegExp
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// 处理 Map
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size) return false;
for (const [key, value] of a) {
if (!b.has(key)) return false;
if (!deepEqual(value, b.get(key), visited)) return false;
}
return true;
}
// 处理 Set
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size) return false;
for (const value of a) {
if (!b.has(value)) return false;
}
return true;
}
// 处理 Array
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i], visited)) return false;
}
return true;
}
// 普通对象
const keysA = Reflect.ownKeys(a); // 包含 Symbol
const keysB = Reflect.ownKeys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
if (!deepEqual(a[key], b[key], visited)) return false;
}
return true;
}
3. 特殊类型的处理
深拷贝中最容易被忽略的类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. ArrayBuffer / TypedArray
if (ArrayBuffer.isView(obj)) {
// TypedArray: Int32Array, Float64Array 等
return new obj.constructor(obj); // 直接复制缓冲区
}
// 2. BigInt 值
// 如果 BigInt 是值,直接返回(基本类型处理)
// 如果作为对象属性,赋值即可
// 3. Error 对象
if (obj instanceof Error) {
const error = new obj.constructor(obj.message);
error.stack = obj.stack;
return error;
}
// 4. Promise
// Promise 不可克隆!需要特殊处理
// 简单方案:返回一个 resolved 的相同值
if (obj instanceof Promise) {
return Promise.resolve(obj).then(v => deepClone(v));
}
// 5. WeakMap / WeakSet
// 不可遍历,只能直接引用
// 如果要拷贝:必须知道内部引用,WeakMap 不支持迭代
if (obj instanceof WeakMap || obj instanceof WeakSet) {
return new obj.constructor(); // 返回空实例
}
4. 不可枚举属性和原型链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 获取所有属性(包括不可枚举的)
function getAllProperties(obj) {
const props = [];
for (let current = obj; current !== null; current = Object.getPrototypeOf(current)) {
props.push(...Object.getOwnPropertyNames(current));
props.push(...Object.getOwnPropertySymbols(current));
}
return [...new Set(props)];
}
// 带属性描述符的深拷贝(保留 getter/setter 等)
function deepCloneWithDescriptors(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj);
const result = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
hash.set(obj, result);
const descriptors = Object.getOwnPropertyDescriptors(obj);
for (const key of Reflect.ownKeys(descriptors)) {
const descriptor = descriptors[key];
if (descriptor.get || descriptor.set) {
// getter/setter: 直接定义描述符
Object.defineProperty(result, key, descriptor);
} else if (descriptor.value && typeof descriptor.value === 'object') {
// 值属性:递归拷贝
Object.defineProperty(result, key, {
...descriptor,
value: deepCloneWithDescriptors(descriptor.value, hash),
});
} else {
Object.defineProperty(result, key, descriptor);
}
}
return result;
}
实战案例:Redux 状态管理中的深比较
在 Redux 的 connect 高阶组件(或 React-Redux 的 useSelector)中,浅比较决定了组件是否重新渲染。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// React-Redux 中的浅比较实现
export function is(x, y) {
// 修复了 Object.is 中 +0 !== -0 的特性
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}
export function shallowEqual(objA, objB) {
if (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++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
自定义 Hooks 中的深比较优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// useDeepEffect — 基于深比较的 useEffect
import { useEffect, useRef } from 'react';
function useDeepEffect(fn, deps) {
const prevDepsRef = useRef();
if (!prevDepsRef.current || !deepEqual(prevDepsRef.current, deps)) {
prevDepsRef.current = deps;
}
useEffect(fn, [prevDepsRef.current]);
}
// 使用
function MyComponent({ config }) {
// 即使每次 render 都传入新的对象字面量,也只会在内容变化时才重新执行
useDeepEffect(() => {
fetchData(config);
}, [config]);
return <div>...</div>;
}
1
2
3
4
5
6
7
8
9
10
11
// useDeepMemo — 基于深比较的 useMemo
function useDeepMemo(factory, deps) {
const ref = useRef();
const prevDeps = ref.current?.deps;
if (!prevDeps || !deepEqual(prevDeps, deps)) {
ref.current = { deps, value: factory() };
}
return ref.current.value;
}
不可变数据工具库的核心
一个轻量级的不可变数据更新工具,内嵌深拷贝和浅比较:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const ImmutableUtils = {
// 更新对象属性(浅合并 + 深拷贝受影响分支)
update(obj, path, updater) {
const keys = Array.isArray(path) ? path : path.split('.');
const clone = deepClone(obj);
let current = clone;
for (let i = 0; i < keys.length - 1; i++) {
current = current[keys[i]];
}
const lastKey = keys[keys.length - 1];
current[lastKey] = updater(current[lastKey]);
return clone;
},
// 深度冻结(递归 Object.freeze)
deepFreeze(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const props = Object.getOwnPropertyNames(obj);
for (const prop of props) {
const value = obj[prop];
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
ImmutableUtils.deepFreeze(value);
}
}
return Object.freeze(obj);
},
// 差量更新(只返回变化的部分,用于状态同步)
diffChanges(prev, next) {
if (deepEqual(prev, next)) return null;
const changes = {};
const allKeys = new Set([...Object.keys(prev), ...Object.keys(next)]);
for (const key of allKeys) {
if (!deepEqual(prev[key], next[key])) {
changes[key] = deepClone(next[key]);
}
}
return changes;
},
};
底层原理
1. V8 中对象的内部表示
V8 使用两种对象表示方式:
- 快属性(in-object / fast properties):存储在对象内部固定位置,通过数字索引直接访问
- 慢属性(dictionary / slow properties):使用哈希表存储,适用于频繁增删属性的场景
深拷贝时遍历属性,实际上是在遍历 V8 的”属性字典”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// V8 中对象属性的存储结构
// 快属性模式:
{
map: Map, // 描述对象结构和类型
properties: [v1, v2, v3], // 属性值存储在固定位置
elements: [e1, e2, e3], // 数组元素
}
// 慢属性模式:
{
map: Map (dict_mode),
property_dictionary: {
"key1": value1,
"key2": value2,
...
},
}
理解这一层对拷贝性能的优化很重要:
- 快属性对象的属性遍历速度比慢属性快约 10 倍
Object.keys()对快属性对象更快- 在深拷贝前使用
Object.assign()可以保持快属性模式
2. structuredClone() 的浏览器实现
structuredClone(v) 是原生 API(浏览器 + Deno),基于结构化克隆算法(Structured Serialize/Deserialize)。实现位于:
- Chromium:
content/browser/structured_clone_helper.cc - 与
postMessage、IndexedDB、Cache Storage使用相同的算法
1
2
3
4
5
6
7
8
9
10
11
12
// 使用 structuredClone
const cloned = structuredClone(original);
// 支持的类型比手写实现更完整:
// ✓ Array / Object / Primitive
// ✓ Date / RegExp / Map / Set
// ✓ ArrayBuffer / TypedArray / DataView
// ✓ Blob / File / ImageData
// ✓ Error 子类(部分)
// ✗ Function / Symbol(作为值时丢抛错)
// ✗ WeakMap / WeakSet
// ✗ DOM 节点
structuredClone 的局限:
- 不支持包含 Symbol 值的对象(会抛出
DataCloneError) - 不支持
Function类型 - 对象原型丢失(还原为
Object.prototype) - 某些类型(如
Promise)会抛出DataCloneError
3. Object.is vs === 运算符
Object.is() 与 === 有两点不同:
1
2
3
4
5
6
7
// NaN 的比较
NaN === NaN; // false
Object.is(NaN, NaN); // true
// +0 与 -0 的比较
+0 === -0; // true
Object.is(+0, -0); // false
这对深比较的影响:
1
2
3
4
5
6
7
8
9
function deepEqualWithIs(a, b) {
// 使用 Object.is 确保 NaN 等于 NaN,但 +0 不等于 -0
if (Object.is(a, b)) return true;
// ...
}
// React-Redux 使用 Object.is 的考量:
// - a !== a 且 b !== b 时,两者都是 NaN,返回 true ✓
// - a === 0 且 b === 0 不判断 +0/-0 差异,但极少发生所以可接受
高频面试题解析
面试题 1:为什么 JSON.parse(JSON.stringify(obj)) 不能用于深拷贝?写一个完整的替代方案。
解答:
JSON.parse(JSON.stringify(obj)) 的缺陷包括:
undefined、Symbol、Function会被忽略或转为 nullDate对象转为字符串,不是 Date 实例RegExp、Map、Set转为{}Infinity、NaN转为null- BigInt 会抛 TypeError
- 循环引用会抛错
替代方案:使用手写深拷贝函数(如前述的 deepClone),或使用浏览器原生 structuredClone(ES2023+)。
面试题 2:实现一个函数,可以比较两个包含循环引用的对象是否”内容相等”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function deepEqualWithCircular(a, b, seen = new WeakMap()) {
// 处理循环引用
if (typeof a === 'object' && a !== null &&
typeof b === 'object' && b !== null) {
// 检查 a 是否已经访问过
if (seen.has(a)) {
// a 之前访问过 → 检查 b 是否被"指向相同"的对象
return seen.get(a) === b; // 如果 b 也是之前记录的相同引用,返回 true
}
// 记录 a → b 的映射
seen.set(a, b);
}
// 基本类型比较
if (Object.is(a, b)) return true;
if (typeof a !== typeof b) return false;
// ... 进一步的对象比较(同 deepEqual)
return true;
}
// 测试
const circle1 = { name: 'circle' };
circle1.self = circle1;
const circle2 = { name: 'circle' };
circle2.self = circle2;
console.log(deepEqualWithCircular(circle1, circle2)); // true
// 注意:circle1.self 是整个 circle1,circle2.self 是整个 circle2
// 逻辑应该是:比较 a.self 和 b.self 时,seen 中 a.self (即 circle1) 映射到
// seen.get(circle1) 应该是 circle2,而 b.self 也是 circle2,返回 true
面试题 3:React 的 PureComponent 和 React.memo 默认使用浅比较。如果传给子组件的数据结构很深,如何优化?
解答:
浅比较的问题在于:如果父组件每次 render 都创建新的深层对象,即使内容没变,浅比较也会认为”变了”:
1
2
3
4
5
6
7
8
9
10
11
// 问题代码
function Parent() {
const data = { items: [{ id: 1, name: 'Alice' }] };
// 每次 render 都创建新的 data 对象 → Child 每次都会重新渲染
return <Child data={data} />;
}
const Child = React.memo(({ data }) => {
return <div>{data.items[0].name}</div>;
}); // React.memo 的浅比较认为 data 变了,子组件重渲染
优化方案:
(1) 使用 useMemo / useCallback
1
2
3
4
5
6
7
8
9
10
11
function Parent() {
const data = useMemo(() => ({
items: [{ id: 1, name: 'Alice' }],
}), []); // 依赖为空 → 永远不重新创建
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <Child data={data} onClick={handleClick} />;
}
(2) 传递自定义比较函数
1
2
3
4
5
6
7
const Child = React.memo(
({ data }) => <div>{data.items[0].name}</div>,
(prevProps, nextProps) => {
// 使用深比较(但要注意性能成本)
return deepEqual(prevProps, nextProps);
}
);
(3) 对复杂对象做 Immer 或 Immutable.js 处理
使用 Immer 时,修改 produce 返回新的引用,但与旧版本比较时可以用 === 比较包含嵌套路径:
1
2
3
4
5
6
7
8
9
10
import { produce } from 'immer';
const baseState = { items: [{ id: 1, name: 'Alice' }] };
const nextState = produce(baseState, draft => {
draft.items[0].name = 'Bob';
});
console.log(baseState.items === nextState.items); // false
console.log(baseState.items[0] === nextState.items[0]); // false
// Immer 保证:未修改的路径,引用不变;修改的路径,引用改变
(4) 更极端的:标准化数据结构(Normalizr)
1
2
3
4
5
6
7
8
9
10
// 将嵌套结构展平为 { entities: { ... }, result: [...] }
// 这样浅比较可以直接判断每个实体是否变化
const normalizedData = {
entities: {
users: {
1: { id: 1, name: 'Alice' },
},
},
result: [1],
};
总结与扩展
深拷贝和浅比较是 JavaScript 面试中”看起来简单,写到完美很难”的题目。一个生产级的深拷贝需要考虑:
- 所有基本类型与包装类型
- 所有标准内置对象
- 原型链与属性描述符
- 循环引用与栈安全
- 性能优化(快属性探测、提前退出、递归深度)
值得进一步探索的方向:
- structuredClone 的浏览器兼容性:从 Chrome 98 / Firefox 94 开始支持,Safari 15.4 起支持
- lodash.cloneDeep 源码:约 200 行的完善实现,比手写完整版本少了但覆盖了绝大多数场景
- Immer 的 Proxy 架构:通过 Proxy 实现”写时复制”(Copy-on-Write),避免了深拷贝全部属性的开销
- 前端状态管理中的不可变数据最佳实践:
1
2
3
4
5
6
7
// 用 Object.assign 或展开运算符做浅层不可变更新
const newState = { ...state, user: { ...state.user, name: 'Bob' } };
// 用 immer produce 做深层不可变更新(语法更像"改")
const newState = produce(state, draft => {
draft.user.name = 'Bob';
});
深拷贝不仅是面试题,更是工程化的基本功。理解它,才能写出安全的、无副作用的、可预测的代码。