文章

手写深拷贝与浅比较深度解析:从循环引用到特殊类型的全面覆盖

手写深拷贝与浅比较深度解析:从循环引用到特殊类型的全面覆盖

一句话概括

深拷贝和浅比较是 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 也被改了 ❌

这个问题在以下场景中尤为突出:

  1. 状态管理:React/Vue 中修改状态需返回新对象,不可变数据是原则
  2. 函数安全:函数内部不应修改传入的参数对象
  3. 数据持久化:存储前需要序列化,避免引用导致的意外修改
  4. 测试断言:比较预期值和实际值时不希望引用干扰

为什么 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
  • postMessageIndexedDBCache 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)) 的缺陷包括:

  1. undefinedSymbolFunction 会被忽略或转为 null
  2. Date 对象转为字符串,不是 Date 实例
  3. RegExpMapSet 转为 {}
  4. InfinityNaN 转为 null
  5. BigInt 会抛 TypeError
  6. 循环引用会抛错

替代方案:使用手写深拷贝函数(如前述的 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 的 PureComponentReact.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';
});

深拷贝不仅是面试题,更是工程化的基本功。理解它,才能写出安全的、无副作用的、可预测的代码。

本文由作者按照 CC BY 4.0 进行授权