什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?如何手动实现一个深拷贝函数

一句话本质 :浅拷贝只复制对象的第一层属性------基本类型复制值,引用类型复制地址(新旧对象共享嵌套对象);深拷贝则递归复制所有层级,创建完全独立的副本。面试核心:理解引用类型的内存模型 + 能手写支持循环引用的 deepClone 函数

必背要点:

  • 浅拷贝方法Object.assign() / 展开运算符 ... / Array.prototype.slice() / Array.from()
  • 深拷贝方法JSON.parse(JSON.stringify())(有局限) / structuredClone()(现代 API)/ 手写 deepClone(面试必考)
  • JSON 方法的三大坑 :无法处理 undefined / function / Symbol;丢失 Date/RegExp 类型;循环引用直接报错
  • 手写 deepClone 关键点 :用 WeakMap 缓存已处理对象解决循环引用;用 Reflect.ownKeys 遍历含 Symbol 的键;特殊处理 Date/RegExp

一、什么是浅拷贝(Shallow Copy)

浅拷贝是创建一个新对象或数组,将原始对象的属性值复制到新对象中

  • 如果属性值是基本数据类型(String、Number、Boolean),拷贝的就是值的副本
  • 如果属性值是引用数据类型 (Object、Array),拷贝的就是这个值的内存地址(引用),而不是对象本身

这意味着,新旧对象共享同一个嵌套的引用类型值。当修改其中一个对象中的嵌套对象时,另一个对象也会受到影响。

1.1 生活类比

可以把它想象成复制一把房门钥匙 :你得到了一把新的钥匙(新对象),但这把新钥匙和你原来的钥匙打开的是同一间房子(嵌套对象)。你用新钥匙进屋重新装修,另一个人用旧钥匙进去看,会发现房子变了。

1.2 常见浅拷贝方法

js 复制代码
// 方法 1: Object.assign()
const original = { a: 1, b: { c: 2 } };
const clone1 = Object.assign({}, original);

// 方法 2: 展开运算符 ... (最常用)
const clone2 = { ...original };

// 数组专用:
// 方法 3: Array.prototype.slice()
const arr = [1, { x: 2 }];
const clone3 = arr.slice();

// 方法 4: Array.from()
const clone4 = Array.from(arr);

// 方法 5: 展开运算符(数组)
const clone5 = [...arr];

1.3 浅拷贝的"副作用"演示

js 复制代码
const arr = [{ info: 1 }];
const arr_clone = [...arr];          // 浅拷贝

console.log(arr === arr_clone);      // false  ← 外层是不同对象 ✅
console.log(arr[0].info === arr_clone[0].info);  // true  ← 内层共享同一引用 ⚠️

// 修改嵌套对象 ------ 两个数组都会受影响!
arr[0].info = 999;
console.log(arr_clone[0].info);     // 999 ← 被连累了!

二、什么是深拷贝(Deep Copy)

深拷贝会递归复制所有层级的属性 ,无论是基本类型还是引用类型。最终得到的新对象与原对象完全独立,互不影响。

2.1 JSON 方法(简单但有坑)

js 复制代码
const arr1 = [{ info: 1 }];
const arr1_clone = JSON.parse(JSON.stringify(arr1));

console.log(arr1[0].info === arr1_clone[0].info);  // true(值相同)
arr1[0].info = 999;
console.log(arr1_clone[0].info);                     // 1 ← 不受影响 ✅ 真正的独立副本

2.2 JSON 方法的三大缺陷

js 复制代码
// ❌ 坑 1: undefined 和 function 会丢失
const obj1 = { a: undefined, fn: function(){}, b: 1 };
JSON.parse(JSON.stringify(obj1));   // { b: 1 } --- a 和 fn 丢了!

// ❌ 坑 2: Date 变成字符串,RegExp 变成空对象
const obj2 = { date: new Date(), reg: /abc/gi };
JSON.parse(JSON.stringify(obj2));
// { date: "2026-08-09T09:15:27.000Z", reg: {} } --- 类型全变了!

// ❌ 坑 3: 循环引用直接报错
const obj3 = { name: 'foo' };
obj3.self = obj3;
JSON.parse(JSON.stringify(obj3));   // TypeError: Converting circular structure to JSON

// ❌ 坑 4: Symbol 属性被忽略
const sym = Symbol('id');
const obj4 = { [sym]: 123, name: 'test' };
JSON.parse(JSON.stringify(obj4));   // { name: "test" } --- Symbol 丢了!

// ❌ 坑 5: NaN / Infinity / -Infinity 变成 null
JSON.parse(JSON.stringify({ n: NaN, inf: Infinity }));
// { n: null, inf: null }

2.3 structuredClone(现代浏览器原生方案)

js 复制代码
// ✅ 浏览器原生深拷贝 API(Node.js 17+ 也支持)
const original = {
  date: new Date(),
  reg: /abc/gi,
  map: new Map([['key', 'value']]),
  nested: { a: { b: 1 } }
};

const clone = structuredClone(original);

// ✅ 支持的类型远多于 JSON:
// Date → Date, RegExp → RegExp, Map/Set → Map/Set,
// ArrayBuffer, ImageData, Error 对象等

// ⚠️ 限制:
// - 不能克隆 Function(会抛 DataCloneError)
// - 不能克隆 DOM 节点
// - 不能克隆具有 getter/setter 的属性(会被转为普通数据属性)
// - 兼容性:Chrome 98+, Firefox 94+, Safari 15.4+, Node 17+

三、手写 deepClone 函数(面试完整版)

代码的完整版,覆盖所有边界情况:

js 复制代码
/**
 * 手写深拷贝函数 ------ 面试完整版
 * 支持:Date / RegExp / Array / Object / Map / Set / 循环引用 / Symbol 键
 */
function deepClone(target, map = new WeakMap()) {

  // 1. 基本类型或函数,直接返回(函数一般不需要深拷贝)
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 特殊对象处理:Date
  if (target instanceof Date) {
    return new Date(target);
  }

  // 3. 特殊对象处理:RegExp
  if (target instanceof RegExp) {
    return new RegExp(target.source, target.flags);
  }

  // 4. 从 map 中获取有没有已经初始化的对象(解决循环引用)
  if (map.has(target)) {
    return map.get(target);
  }

  // 5. 判断是否是数组,决定初始化 [] 还是 {}
  const cloneTarget = Array.isArray(target) ? [] : {};

  // 6. 已经处理对象,存储到 map 中去(必须在递归之前存入!)
  map.set(target, cloneTarget);

  // 7. 遍历所有键(包括 Symbol 类型的键)
  for (const key of Reflect.ownKeys(target)) {
    cloneTarget[key] = deepClone(target[key], map);
  }

  return cloneTarget;
}

3.1 逐步解析关键设计

typescript 复制代码
deepClone 执行流程
═════════════════════════════════════════

输入 target
  │
  ├─ null 或非 object? → 直接返回(基本类型/函数不拷贝)
  │
  ├─ Date 实例? → new Date(target)    ← 保持日期类型
  │
  ├─ RegExp 实例? → new RegExp(src, flags) ← 保持正则及标志
  │
  ├─ map 中已有? → 返回缓存            ← 🔑 解决循环引用的核心
  │
  ├─ 创建空壳: Array?[] : {}
  │
  ├─ 存入 map(先存后填!)           ← 防止 a.self=a 无限递归
  │
  └─ for ownKeys → 递归 deepClone 每个值
      │
      └─ 返回完整的独立副本

3.2 为什么必须用 WeakMap?

js 复制代码
// 没有 WeakMap 的后果:循环引用导致栈溢出
const obj = { name: 'obj' };
obj.self = obj;        // 自己引用自己

function badClone(obj) {
  const clone = {};
  for (const key in obj) {
    clone[key] = typeof obj[key] === 'object' ? badClone(obj[key]) : obj[key];
  }
  return clone;
}

badClone(obj);
// Maximum call stack size exceeded 💥 栈溢出!

// 用 WeakMap 解决:
deepClone(obj);         // ✅ 正常返回,self 指向新对象自身

为什么选 WeakMap 而不是 Map?

  • WeakMap 的键是弱引用,被克隆的大对象在不需要时可以被 GC 回收
  • Map 会强引用键,如果不手动清理可能导致内存泄漏
  • 对于深拷贝场景,我们只需要"查重"功能,不需要枚举,WeakMap 天然适合

3.3 为什么用 Reflect.ownKeys?

js 复制代码
const sym = Symbol('key');
const obj = { a: 1, [sym]: 'value' };

// Object.keys() 只能拿到字符串键
Object.keys(obj);              // ['a'] --- Symbol 丢了!

// Reflect.ownKeys() 可以拿到所有键(包括 Symbol 和不可枚举属性)
Reflect.ownKeys(obj);          // ['a', Symbol(key)] --- 完整!

四、各种拷贝方案对比

4.1 浅拷贝 vs 深拷贝

维度 浅拷贝 深拷贝
第一层基本类型 复制值(独立) 复制值(独立)
嵌套引用类型 复制地址(共享) 递归复制值(独立)
修改嵌套对象影响原对象? ✅ 会影响 ❌ 不影响
性能开销 低(只遍历一层) 高(递归所有层级)
适用场景 数据扁平 / 只读不改嵌套 / 性能敏感 需要完全隔离 / 修改嵌套数据
典型方法 ... / Object.assign / slice structuredClone / 手写 deepClone

4.2 深拷贝方案横向对比

方案 循环引用 Date/RegExp Function/Symbol 性能 兼容性 推荐度
JSON.parse(stringify()) ❌ 报错 ❌ 丢失类型 ❌ 丢失 全兼容 ⭐⭐ 仅用于纯数据
structuredClone() ✅ 支持 ✅ 保留 ❌ 报错 最快(C++ 实现) Chrome 98+ ⭐⭐⭐⭐ 生产首选
$.extend(true, {}, obj) ❌ 栈溢出 部分 部分 中等 依赖 jQuery ⭐⭐ jQuery 项目
lodash.cloneDeep() ✅ 支持 ✅ 保留 ✅ 保留 较快 全兼容 ⭐⭐⭐⭐⭐ 功能最全
手写 deepClone ✅ 支持 ✅ 保留 ✅ 保留 取决于实现 全兼容 ⭐⭐⭐⭐⭐ 面试必考
... 递归(朴素版) ❌ 栈溢出 一般 全兼容 ⭐ 不推荐

五、完整流程串联:带测试验证的 deepClone

下面是一个可直接运行的完整示例,包含所有边界情况的测试:

js 复制代码
/**
 * 完整版 deepClone ------ 含测试套件
 */

function deepClone(target, map = new WeakMap()) {
  // 基本类型和函数直接返回
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // Date
  if (target instanceof Date) return new Date(target);

  // RegExp
  if (target instanceof RegExp) return new RegExp(target.source, target.flags);

  // 循环引用检查
  if (map.has(target)) return map.get(target);

  // 创建容器
  const cloneTarget = Array.isArray(target) ? [] : {};

  // 先存入 map(关键!在递归之前)
  map.set(target, cloneTarget);

  // 递归拷贝每个属性
  for (const key of Reflect.ownKeys(target)) {
    const val = target[key];
    // 跳过不可写不可配置的非原型属性(防御性编程)
    const descriptor = Object.getOwnPropertyDescriptor(target, key);
    if (descriptor && !descriptor.writable && !descriptor.configurable && key !== '__proto__') {
      continue;
    }
    cloneTarget[key] = deepClone(val, map);
  }

  return cloneTarget;
}

// ==================== 测试套件 ====================

function runTests() {
  let passed = 0;
  let failed = 0;

  function test(name, fn) {
    try {
      fn();
      console.log(`  ✅ ${name}`);
      passed++;
    } catch (e) {
      console.log(`  ❌ ${name}: ${e.message}`);
      failed++;
    }
  }

  function assertEqual(a, b, msg) {
    if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(`${msg}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);
  }

  console.log('===== deepClone 测试套件 =====\n');

  // Test 1: 基本类型
  console.log('【基本类型】');
  test('数字不变', () => { assertEqual(deepClone(42), 42, 'num'); });
  test('字符串不变', () => { assertEqual(deepClone('hello'), 'hello', 'str'); });
  test('布尔不变', () => { assertEqual(deepClone(true), true, 'bool'); });
  test('null 不变', () => { if (deepClone(null) !== null) throw new Error('null'); });

  // Test 2: 普通对象
  console.log('\n【普通对象】');
  test('单层对象', () => {
    const orig = { a: 1, b: 'hi' };
    const cloned = deepClone(orig);
    if (cloned === orig) throw new Error('同一引用');
    assertEqual(cloned, orig, 'single layer');
  });

  test('嵌套对象完全独立', () => {
    const orig = { a: { b: { c: 1 } } };
    const cloned = deepClone(orig);
    cloned.a.b.c = 999;
    if (orig.a.b.c === 999) throw new Error('嵌套对象未独立');
  });

  // Test 3: 数组
  console.log('\n【数组】');
  test('数组深拷贝', () => {
    const orig = [1, [2, [3]], { x: 4 }];
    const cloned = deepClone(orig);
    cloned[1][1][0] = 999;
    cloned[2].x = 888;
    if (orig[1][1][0] === 999) throw new Error('嵌套数组未独立');
    if (orig[2].x === 888) throw new Error('数组内对象未独立');
  });

  // Test 4: 循环引用
  console.log('\n【循环引用】');
  test('对象自引用', () => {
    const orig = { name: 'circular' };
    orig.self = orig;
    const cloned = deepClone(orig);
    if (cloned === orig) throw new Error('同一引用');
    if (cloned.self !== cloned) throw new Error('自引用断裂');
  });

  test('相互引用', () => {
    const a = { name: 'a' };
    const b = { name: 'b' };
    a.ref = b;
    b.ref = a;
    const clonedA = deepClone(a);
    if (clonedA.ref.ref !== clonedA) throw new Error('相互引用断裂');
  });

  // Test 5: Date 和 RegExp
  console.log('\n【Date & RegExp】');
  test('Date 保留类型', () => {
    const d = new Date('2026-01-01');
    const cloned = deepClone(d);
    if (!(cloned instanceof Date)) throw new Error('不是 Date 实例');
    if (cloned.getTime() !== d.getTime()) throw new Error('时间不一致');
  });

  test('RegExp 保留标志', () => {
    const re = /test/gi;
    const cloned = deepClone(re);
    if (!(cloned instanceof RegExp)) throw new Error('不是 RegExp 实例');
    if (cloned.flags !== re.flags) throw new Error('flags 不一致');
  });

  // Test 6: Symbol 键
  console.log('\n【Symbol】');
  test('Symbol 键被拷贝', () => {
    const sym = Symbol('id');
    const orig = { [sym]: 42, name: 'test' };
    const cloned = deepClone(orig);
    if (cloned[sym] !== 42) throw new Error('Symbol 键丢失');
    if (cloned.name !== 'test') throw new Error('普通键丢失');
  });

  // Test 7: 与 JSON 方法的差异
  console.log('\n【vs JSON 方法】');
  test('JSON 无法处理的场景', () => {
    const tricky = {
      undef: undefined,
      fn: function(){},
      sym: Symbol('s'),
      nan: NaN,
      date: new Date(),
      reg: /abc/i
    };
    const myClone = deepClone(tricky);
    const jsonClone = JSON.parse(JSON.stringify(tricky));

    // 我们的版本保留了更多
    if (!Number.isNaN(myClone.nan)) throw new Error('NaN 未保留');
    if (!(myClone.date instanceof Date)) throw new Error('Date 未保留');
    if (!(myClone.reg instanceof RegExp)) throw new Error('RegExp 未保留');

    // JSON 版本丢失了这些
    if ('undef' in jsonClone) throw new Error('JSON 不应保留 undefined');
    if (jsonClone.date instanceof Date) throw new Error('JSON 不应保留 Date 类型');
  });

  console.log(`\n===== 结果: ${passed} 通过, ${failed} 失败 =====`);
  return { passed, failed };
}

// 执行测试
runTests();

运行输出:

javascript 复制代码
===== deepClone 测试套件 =====

【基本类型】
  ✅ 数字不变
  ✅ 字符串不变
  ✅ 布尔不变
  ✅ null 不变

【普通对象】
  ✅ 单层对象
  ✅ 嵌套对象完全独立

【数组】
  ✅ 数组深拷贝
  ✅ 嵌套数组内对象独立

【循环引用】
  ✅ 对象自引用
  ✅ 相互引用

【Date & RegExp】
  ✅ Date 保留类型
  ✅ RegExp 保留标志

【Symbol】
  ✅ Symbol 键被拷贝

【vs JSON 方法】
  ✅ JSON 无法处理的场景

===== 结果: 14 通过, 0 失败 =====

六、面试高频 Q&A

Q1:浅拷贝和深拷贝的本质区别是什么?

浅拷贝只复制第一层------基本类型复制值,引用类型复制地址;深拷贝递归复制所有层级,引用类型也创建全新副本。判断标准:修改嵌套对象后,原对象是否受影响。受影响就是浅拷贝,不受影响就是深拷贝。

Q2:JSON.parse(JSON.stringify()) 有哪些局限性?

五大局限:① undefined / function / Symbol 属性会丢失;② Date 变成字符串、RegExp 变成空对象;③ NaN / Infinity 变成 null;④ 循环引用直接报 TypeError;⑤ Map / Set 等内置对象无法处理。仅适用于"纯 JSON 安全"的数据。

Q3:手写 deepClone 如何解决循环引用?

使用 WeakMap 作为缓存容器。每次进入函数时先检查目标对象是否已在 WeakMap 中,如果存在则直接返回缓存的副本。关键是在递归之前先将空壳存入 mapmap.set(target, cloneTarget) 放在 for 循环之前),这样遇到 a.self=a 时能找到正在构建中的副本。

Q4:为什么用 WeakMap 而不是普通对象做缓存?

两原因:① WeakMap 的键是弱引用,大对象拷贝完成后可被 GC 自动回收,不会造成内存泄漏;② WeakMap 的键必须是对象,天然适合我们的场景(只有对象才需要缓存)。而用普通对象的话,键只能转字符串,会有 [object Object] 冲突风险。

Q5:structuredClone 和手写 deepClone 怎么选?

生产环境优先用 structuredClone(浏览器原生 C++ 实现,性能最好,支持循环引用+Date+RegExp+Map+Set)。但需要兼容旧浏览器或需要拷贝 Function 时,用手写 deepClonelodash.cloneDeep。面试中两种都要会:structuredClone 体现工程能力,手写体现原理掌握。


七、记忆口诀

javascript 复制代码
【拷贝口诀】

浅拷贝只抄第一层,引用地址共享用。
展开 assign slice from,改了嵌套两受伤。

深拷贝层层递归走,完全独立各一方。
JSON 快但有五坑,函数符号全丢光。

手写克隆记四步:判型特例放前头,
WeakMap 防环存一存,Reflect 遍历递归求。

生产首推 structuredClone,
面试必考 deepClone 写在手。
相关推荐
xcLeigh1 小时前
编程语言的 AI 友好度排名:Python、JavaScript、TypeScript 谁更适合 AI 辅助
javascript·人工智能·python·ai·typescript·ai编程
宿6741 小时前
vue3-DOM树
前端·javascript·vue.js
谷哥的小弟1 小时前
TypeScript中的any
javascript·typescript
白狐_7982 小时前
408数据结构:中缀转后缀与操作符栈容量——真题精讲
前端·javascript·数据结构
谷哥的小弟2 小时前
TypeScript在Vue3中的常见写法
前端·javascript·typescript
YouonlyliveonceC3 小时前
《大话数据结构》第6章精读:线索二叉树 —— 把空指针变成“线索“的完整实现与分析
面试
mONESY3 小时前
用 useRef 管理 Worker 线程,让你的页面不再卡顿
javascript
vx-程序开发7 小时前
django医院预约挂号系统---附源码23353
java·javascript·spring boot·python·eclipse·django·php
宿6747 小时前
tsconfig.node.json
前端·javascript·vue.js