Skip to content

生产级深拷贝实现详解

实现思路概述

这个深拷贝实现采用递归 + 缓存的核心思路,通过分层处理不同数据类型,确保完整性和正确性。整体架构遵循"先特殊后通用"的原则,优先处理特殊类型,最后处理普通对象。

核心要素分析

1. 基本类型处理

javascript
if (source === null || typeof source !== "object") return source;

设计考虑:

  • nulltypeof'object',需要特殊判断
  • 基本类型(string、number、boolean、undefined、symbol、bigint)直接返回
  • 这是递归的终止条件之一

2. 特殊内置对象处理

javascript
const specialConstructors = [
  [Date,     src => new Date(src.getTime())],
  [RegExp,   src => new RegExp(src.source, src.flags)],
  [ArrayBuffer, src => src.slice()],
  [DataView,    src => new DataView(src.buffer.slice(...))]
];

设计亮点:

  • 使用配置数组模式,便于扩展和维护
  • 每种类型都有专门的克隆策略
  • Date 使用 getTime() 而非 valueOf(),更明确
  • RegExp 保持 sourceflags
  • ArrayBuffer 使用 slice() 创建新的内存区域

3. TypedArray 处理

javascript
if (ArrayBuffer.isView(source)) {
  const Ctor = source.constructor;
  return new Ctor(source.buffer.slice(0));
}

技术细节:

  • ArrayBuffer.isView() 可以识别所有 TypedArray
  • 但会包含 DataView,所以 DataView 需要提前处理
  • 使用 buffer.slice(0) 创建新的底层缓冲区

4. 集合类型处理

javascript
if (source instanceof Map) {
  if (cache.has(source)) return cache.get(source);
  const m = new Map();
  cache.set(source, m);
  source.forEach((v, k) => m.set(deepClone(k, cache), deepClone(v, cache)));
  return m;
}

关键设计:

  • 先检查缓存,防止循环引用
  • 先创建空容器,再设置缓存
  • 键和值都需要深拷贝(Map 的键也可能是对象)
  • WeakMap/WeakSet 无法枚举,只能返回引用

5. 循环引用处理

javascript
if (cache.has(source)) return cache.get(source);
const cloned = Object.create(Object.getPrototypeOf(source));
cache.set(source, cloned);

核心机制:

  • 使用 WeakMap 作为缓存,避免内存泄漏
  • 先创建空壳,立即缓存,再填充内容
  • 这样可以处理自引用和相互引用

6. 原型链保持

javascript
const cloned = Object.create(Object.getPrototypeOf(source));

设计考虑:

  • 不使用 new source.constructor(),避免构造函数副作用
  • 保持原始对象的原型链关系
  • 支持自定义类的实例拷贝

7. 属性描述符完整拷贝

javascript
Reflect.ownKeys(source).forEach(key => {
  const desc = Object.getOwnPropertyDescriptor(source, key);
  if (desc.get || desc.set) {
    Object.defineProperty(cloned, key, desc);
  } else {
    desc.value = deepClone(source[key], cache);
    Object.defineProperty(cloned, key, desc);
  }
});

完整代码

js
/**
 * deepClone 生产级深拷贝
 * @param {*} source        待拷贝数据
 * @param {WeakMap} cache  循环引用缓存(内部用,可省略)
 * @return {*}             深拷贝结果
 */
function deepClone(source, cache = new WeakMap()) {
  // 0. 先处理循环引用检查
  if (cache.has(source)) return cache.get(source);

  // 1. 基本类型直接返回
  if (source === null || typeof source !== "object") return source;

  // 2. 不可遍历的内置对象
  const specialConstructors = [
    [Date, src => new Date(src.getTime())],
    [RegExp, src => new RegExp(src.source, src.flags)],
    [ArrayBuffer, src => src.slice()],
    [DataView, src => new DataView(src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength))],
  ];
  for (const [Ctor, handler] of specialConstructors) {
    if (source instanceof Ctor) return handler(source);
  }

  // 3. TypedArray(ArrayBuffer.isView 会过滤掉 DataView,已在上一步处理)
  if (ArrayBuffer.isView(source)) {
    const Ctor = source.constructor;
    return new Ctor(source.buffer.slice(0));
  }

  // 4. Map / Set / WeakMap / WeakSet
  if (source instanceof Map) {
    const m = new Map();
    cache.set(source, m);
    source.forEach((v, k) => m.set(deepClone(k, cache), deepClone(v, cache)));
    return m;
  }
  if (source instanceof Set) {
    const s = new Set();
    cache.set(source, s);
    source.forEach(v => s.add(deepClone(v, cache)));
    return s;
  }
  // WeakMap / WeakSet 不能枚举,只能浅拷贝引用
  if (source instanceof WeakMap || source instanceof WeakSet) return source;

  // 5. 函数 —— 无法深拷贝,直接返回引用(或抛错/返回 null,看需求)
  if (typeof source === "function") return source;

  // 6. 防止循环引用
  if (cache.has(source)) return cache.get(source);

  // 7. 处理对象、数组
  //  对数组应该单独处理 数组用Object.create(Object.getPrototypeOf(source))会创建类数组
  const cloned = Array.isArray(source) ? [] : Object.create(Object.getPrototypeOf(source));
  cache.set(source, cloned);

  // 8. 拷贝所有自有属性(含 Symbol、不可枚举)
  Reflect.ownKeys(source).forEach(key => {
    const desc = Object.getOwnPropertyDescriptor(source, key);
    if (desc.get || desc.set) {
      // 访问器属性
      Object.defineProperty(cloned, key, desc);
    } else {
      desc.value = deepClone(source[key], cache);
      Object.defineProperty(cloned, key, desc);
    }
  });

  return cloned;
}

// 测试用例
const test = {
  a: 1,
  b: { c: 2 },
  d: [1, 2, 3],
  e: new Date(),
  f: /test/g,
  g: new Map([["key", "value"]]),
  h: new Set([1, 2, 3]),
  i: function () {
    return "hello";
  },
};

// 添加循环引用测试
test.self = test;

const cloned = deepClone(test);
console.log("原始对象:", test);
console.log("克隆对象:", cloned);
console.log("循环引用测试:", cloned.self === cloned); // 应该是 true

/**
 * 原始对象: <ref *1> {
  a: 1,
  b: { c: 2 },
  d: [ 1, 2, 3 ],
  e: 2025-08-04T01:55:31.595Z,
  f: /test/g,
  g: Map(1) { 'key' => 'value' },
  h: Set(3) { 1, 2, 3 },
  i: [Function: i],
  self: [Circular *1]
}
克隆对象: <ref *1> {
  a: 1,
  b: { c: 2 },
  d: Array { '0': 1, '1': 2, '2': 3 },
  e: 2025-08-04T01:55:31.595Z,
  f: /test/g,
  g: Map(1) { 'key' => 'value' },
  h: Set(3) { 1, 2, 3 },
  i: [Function: i],
  self: [Circular *1]
}
 */

循环引用测试: true;

技术亮点:

  • Reflect.ownKeys() 获取所有自有属性(包括 Symbol 和不可枚举)
  • 区分数据属性访问器属性
  • 保持 writableenumerableconfigurable 特性
  • 访问器属性直接复制描述符,避免触发 getter

需要考虑的细节

1. 类型判断的顺序

  • 先特殊后通用:特殊类型优先处理
  • 先简单后复杂:基本类型最先处理
  • 避免重复判断DataViewTypedArray 之前处理

2. 内存管理

  • 使用 WeakMap 而非 Map,避免内存泄漏
  • 及时设置缓存,防止深度递归时的重复处理

3. 边界情况处理

  • null 的特殊判断
  • 函数的处理策略(返回引用 vs 抛错)
  • WeakMap/WeakSet 的不可枚举特性

4. 性能优化

  • 配置数组避免多个 instanceof 判断
  • 提前返回,减少不必要的处理
  • 缓存机制避免重复拷贝

5. 兼容性考虑

  • 使用现代 API(Reflect.ownKeysObject.getOwnPropertyDescriptor
  • 支持 ES6+ 的数据结构(Map、Set、Symbol)

实现优势

  1. 完整性:覆盖了 JavaScript 中几乎所有的数据类型
  2. 正确性:正确处理循环引用、原型链、属性描述符
  3. 可扩展性:配置数组模式便于添加新的特殊类型
  4. 性能:合理的缓存机制和类型判断顺序
  5. 安全性:使用 WeakMap 避免内存泄漏

潜在改进点

  1. 深度限制:可以添加最大递归深度保护
  2. 自定义处理器:支持用户自定义特定类型的拷贝逻辑
  3. 错误处理:对异常情况的更详细处理
  4. 性能监控:大对象拷贝的性能优化

使用场景

  • 状态管理:Redux、Vuex 等状态库的深拷贝需求
  • 数据处理:API 响应数据的安全拷贝
  • 组件开发:React/Vue 组件的 props 深拷贝
  • 工具库:作为基础工具函数使用

这个实现在功能完整性、代码质量和工程实践方面都达到了生产级别的标准,可以作为深拷贝的标准实现参考。

深拷贝方法对比

1. JSON 序列化方法

javascript
function jsonDeepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

优点:

  • 代码极简,一行搞定
  • 性能较好,原生实现
  • 适合简单的数据结构

缺点:

  • 函数会丢失function 类型被忽略
  • undefined 会丢失undefined 值被忽略
  • Symbol 会丢失:Symbol 属性被忽略
  • Date 变字符串new Date() 变成字符串
  • RegExp 变空对象:正则表达式变成 {}
  • 循环引用报错TypeError: Converting circular structure to JSON
  • NaN/Infinity 变 null:特殊数值处理不当
  • Map/Set 变空对象:新数据结构不支持

适用场景: 纯数据对象,无函数、无特殊类型、无循环引用

2. 简单递归深拷贝

javascript
function simpleDeepClone(obj) {
  if (obj === null || typeof obj !== "object") return obj;

  const result = Array.isArray(obj) ? [] : {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = simpleDeepClone(obj[key]);
    }
  }

  return result;
}

优点:

  • 逻辑清晰,易于理解
  • 代码量少,实现简单
  • 支持基本的对象和数组

缺点:

  • 循环引用死循环:无缓存机制
  • 特殊对象处理不当:Date、RegExp 等变成普通对象
  • 原型链丢失:使用字面量创建对象
  • 属性描述符丢失:只复制可枚举属性
  • Symbol 属性丢失for...in 不遍历 Symbol
  • 不可枚举属性丢失hasOwnProperty 只检查可枚举

适用场景: 简单对象结构,无特殊类型,无循环引用

3. Lodash cloneDeep

javascript
import { cloneDeep } from "lodash";
const cloned = cloneDeep(original);

优点:

  • 久经考验:大量项目使用,稳定可靠
  • 功能完整:支持几乎所有 JavaScript 类型
  • 性能优化:经过大量优化
  • 自定义支持:支持 customizer 函数
  • 生态成熟:文档完善,社区支持好

缺点:

  • 包体积大:整个 lodash 库较大
  • 外部依赖:需要引入第三方库
  • 黑盒实现:内部逻辑复杂,不易定制
  • 版本依赖:需要管理库版本更新

适用场景: 项目已使用 Lodash,或对稳定性要求极高的场景

4. structuredClone(原生 API)

javascript
const cloned = structuredClone(original);

优点:

  • 原生支持:浏览器和 Node.js 原生实现
  • 性能优异:底层 C++ 实现,速度快
  • 功能强大:支持大部分 JavaScript 类型
  • 循环引用安全:原生处理循环引用
  • 代码简洁:一行代码搞定
  • 标准化:Web API 标准,未来趋势

缺点:

  • 兼容性限制:需要较新的浏览器版本(Chrome 98+, Firefox 94+)
  • Node.js 版本要求:需要 Node.js 17.0+
  • 函数不支持:函数类型会抛出 DataCloneError
  • Symbol 不支持:Symbol 属性会被忽略
  • 原型链丢失:不保持原型链关系
  • 部分内置对象不支持:如 Error、Function 等

支持的类型:

  • 基本类型、Date、RegExp
  • Array、Object、Map、Set
  • ArrayBuffer、TypedArray、DataView
  • ImageData、Blob、File 等 Web API 对象

不支持的类型:

  • Function、Error、DOM 节点
  • Symbol 属性、原型链
  • 不可克隆的对象(如 WeakMap、WeakSet)

适用场景: 现代浏览器环境,处理标准数据结构,性能要求高

5. 本实现(生产级深拷贝)

优点:

  • 功能完整:支持所有主要 JavaScript 类型
  • 循环引用安全:WeakMap 缓存机制
  • 内存安全:避免内存泄漏
  • 属性完整性:保持描述符和原型链
  • 现代语法:使用 ES6+ 特性
  • 可定制:代码透明,易于修改
  • 无依赖:纯原生实现
  • 兼容性好:支持较老的 JavaScript 环境

缺点:

  • 代码量较大:相比简单方法更复杂
  • 学习成本:需要理解各种边界情况
  • 维护成本:需要跟进 JavaScript 新特性
  • 性能略逊:相比原生 structuredClone

适用场景: 对功能完整性和可控性要求高的生产环境,需要兼容性支持

性能对比

方法简单对象复杂对象循环引用特殊类型兼容性内存占用
JSON 序列化⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
简单递归⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
structuredClone⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Lodash⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
本实现⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

选择建议

使用 JSON 序列化,当:

  • 数据结构简单(纯对象/数组)
  • 无函数、无特殊类型
  • 性能要求高
  • 代码简洁性优先

使用简单递归,当:

  • 学习目的或面试场景
  • 数据结构可控
  • 需要理解深拷贝原理

使用 structuredClone,当:

  • 现代浏览器环境(Chrome 98+, Firefox 94+)
  • Node.js 17.0+ 环境
  • 性能要求极高
  • 处理标准数据结构
  • 不需要函数和 Symbol 属性

使用 Lodash,当:

  • 项目已依赖 Lodash
  • 稳定性要求极高
  • 不想维护自定义代码
  • 需要额外的工具函数

使用本实现,当:

  • 需要完整的深拷贝功能
  • 需要兼容性支持
  • 不想引入外部依赖
  • 需要定制化修改
  • 对代码质量有高要求

面试中的回答策略

  1. 先说简单方法:展示基础理解
    • "最简单的是 JSON.parse(JSON.stringify()),但有很多限制..."
  2. 提及现代方案:展示技术敏感度
    • "现在有了原生的 structuredClone API,性能很好,但兼容性有限制..."
  3. 指出问题:体现深度思考
    • "然后是简单递归,但处理不了循环引用..."
  4. 对比成熟方案:体现工程经验
    • "Lodash 很成熟,但增加了依赖..."
  5. 提出完整方案:展示技术能力
    • "所以我实现了这个生产级版本,考虑了..."

2025 年的最佳实践建议

  • 现代项目:优先考虑 structuredClone,fallback 到自定义实现
  • 兼容性要求高:使用本实现或 Lodash
  • 简单场景:JSON 序列化足够
  • 学习目的:从简单递归开始理解

这样的回答既展示了技术广度,又体现了实际项目经验和对新技术的关注。

Released under the MIT License.