JS深拷贝与浅拷贝

一文搞懂JS深浅拷贝:从原理到工业级实现,再也不怕面试问

深浅拷贝是前端面试的必考题 ,也是实际开发中最容易踩坑的知识点之一。很多人都遇到过"修改一个对象,另一个对象莫名其妙跟着变"的问题,这背后就是深浅拷贝的原理在起作用。

本文将从最基础的存储机制讲起,带你彻底搞懂深浅拷贝的本质区别,然后一步步实现一个覆盖所有边界情况的工业级深拷贝函数,最后总结所有常见误区和最佳实践。


一、为什么会有深浅拷贝之分?

一切的根源都在于JavaScript的数据类型存储机制。

JavaScript的数据类型分为两大类:

  • 基本类型stringnumberbooleannullundefinedBigIntSymbol
  • 引用类型objectarrayfunctionDateRegExpMapSet

它们在内存中的存储方式完全不同:

类型 存储位置 存储内容 拷贝行为
基本类型 栈内存 直接存储值本身 拷贝时创建一个完全独立的副本,修改互不影响
引用类型 堆内存 栈中只存储指向堆内存的地址 拷贝时默认只拷贝地址,新旧变量指向同一块堆内存,修改会互相影响

举个最简单的例子:

javascript 复制代码
// 基本类型拷贝
let a = 10;
let b = a;
b = 20;
console.log(a); // 10 ✅ 互不影响

// 引用类型拷贝
let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.name = '李四';
console.log(obj1.name); // '李四' ❌ 原对象被修改了!

这就是为什么我们需要深浅拷贝:当我们需要一个独立的引用类型副本时,默认的赋值操作无法满足需求


二、什么是浅拷贝?

定义

浅拷贝只拷贝对象的第一层属性

  • 如果属性是基本类型,拷贝的就是基本类型的值
  • 如果属性是引用类型,拷贝的就是引用地址

也就是说,浅拷贝后的对象,第一层是独立的,但深层的引用类型仍然和原对象共享同一块内存。

直观演示

javascript 复制代码
const original = {
  name: '张三', // 第一层基本类型
  info: { // 第一层引用类型
    address: '北京市'
  }
};

// 浅拷贝
const shallowClone = { ...original };

// 修改第一层基本类型:互不影响
shallowClone.name = '李四';
console.log(original.name); // '张三' ✅

// 修改深层引用类型:原对象被修改
shallowClone.info.address = '上海市';
console.log(original.info.address); // '上海市' ❌

常见的浅拷贝实现方式

所有只拷贝第一层属性的方法都是浅拷贝,前端最常用的有以下几种:

1. 对象展开运算符 ...(推荐)
javascript 复制代码
const clone = { ...original };
2. Object.assign()
javascript 复制代码
const clone = Object.assign({}, original);
3. 数组展开运算符 ...
javascript 复制代码
const arr = [1, 2, { a: 3 }];
const clone = [...arr];
4. 数组的 slice()concat()
javascript 复制代码
const arr = [1, 2, { a: 3 }];
const clone1 = arr.slice();
const clone2 = arr.concat();

⚠️ 面试重点:以上所有方法都是浅拷贝!很多新手误以为它们是深拷贝,这是最常见的面试坑。


三、什么是深拷贝?

定义

深拷贝会递归拷贝对象的所有层级属性

  • 无论属性是基本类型还是引用类型,都会创建一个全新的副本
  • 新旧对象完全独立,修改任何属性都不会互相影响

直观演示

javascript 复制代码
const original = {
  name: '张三',
  info: {
    address: '北京市'
  }
};

// 深拷贝
const deepClone = _.cloneDeep(original); // 使用Lodash的深拷贝

// 修改任何属性都不会影响原对象
deepClone.name = '李四';
deepClone.info.address = '上海市';
console.log(original.name); // '张三' ✅
console.log(original.info.address); // '北京市' ✅

常见的深拷贝实现方式

1. JSON.parse(JSON.stringify())(最简单但缺陷最多)

这是最容易想到的深拷贝方式,一行代码就能实现:

javascript 复制代码
const clone = JSON.parse(JSON.stringify(original));

优点 :简单方便,不需要自己写逻辑 缺点:有非常多的局限性,会丢失或错误处理以下类型的数据:

  • 丢失 undefinedfunctionSymbol 类型的属性
  • Date 对象会变成字符串
  • RegExpError 对象会变成空对象
  • BigInt 类型会直接报错
  • 无法处理循环引用(会直接抛出RangeError

反例演示

javascript 复制代码
const original = {
  a: undefined,
  b: () => console.log('hello'),
  c: Symbol('id'),
  d: new Date(),
  e: /abc/g
};

const clone = JSON.parse(JSON.stringify(original));
console.log(clone);
// { d: "2026-05-13T08:00:00.000Z", e: {} } ❌ 大部分属性都丢失了
2. 递归实现(最灵活,可定制)

这是最核心的实现方式,也是面试中最常考的。我们将在下一节一步步实现一个工业级的深拷贝函数。

3. 第三方库(生产环境首选)
  • Lodash _.cloneDeep():最成熟、最稳定的深拷贝实现,覆盖了所有边界情况
  • Immer:基于不可变数据的深拷贝,非常适合React等框架

四、从零实现一个工业级深拷贝函数

下面我们将一步步实现一个覆盖99%前端开发场景的深拷贝函数,解决所有常见的边界问题。

步骤1:基础版本(处理基本类型和普通对象/数组)

javascript 复制代码
function deepClone(obj) {
  // 处理基本类型和null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 创建新对象或数组
  const cloneObj = Array.isArray(obj) ? [] : {};

  // 遍历属性并递归拷贝
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key]);
    }
  }

  return cloneObj;
}

问题 :这个版本只能处理普通对象和数组,无法处理循环引用、DateRegExp等特殊对象。

步骤2:处理循环引用

循环引用是指对象的属性间接或直接指向自身,比如:

javascript 复制代码
const obj = {};
obj.self = obj; // 循环引用

基础版本遇到循环引用会无限递归,导致栈溢出。我们用WeakMap来缓存已拷贝的对象,解决这个问题:

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 如果已经拷贝过这个对象,直接返回缓存的副本
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  const cloneObj = Array.isArray(obj) ? [] : {};
  // 缓存当前对象
  hash.set(obj, cloneObj);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

步骤3:处理特殊内置对象

基础版本会把DateRegExp等特殊对象当作普通对象处理,导致拷贝错误。我们需要单独处理这些类型:

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理Date
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }

  // 处理RegExp
  if (obj instanceof RegExp) {
    return new RegExp(obj.source, obj.flags);
  }

  // 处理Error
  if (obj instanceof Error) {
    const cloneErr = new Error(obj.message);
    cloneErr.stack = obj.stack;
    return cloneErr;
  }

  if (hash.has(obj)) {
    return hash.get(obj);
  }

  const cloneObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, cloneObj);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

步骤4:处理ES6+集合类型(Map和Set)

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (obj instanceof Date) return new Date(obj.getTime());
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
  if (obj instanceof Error) {
    const cloneErr = new Error(obj.message);
    cloneErr.stack = obj.stack;
    return cloneErr;
  }

  // 处理Map
  if (obj instanceof Map) {
    const cloneMap = new Map();
    hash.set(obj, cloneMap);
    for (const [key, value] of obj) {
      cloneMap.set(deepClone(key, hash), deepClone(value, hash));
    }
    return cloneMap;
  }

  // 处理Set
  if (obj instanceof Set) {
    const cloneSet = new Set();
    hash.set(obj, cloneSet);
    for (const value of obj) {
      cloneSet.add(deepClone(value, hash));
    }
    return cloneSet;
  }

  if (hash.has(obj)) {
    return hash.get(obj);
  }

  const cloneObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, cloneObj);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

步骤5:处理Symbol类型的属性名

for...in循环不会遍历Symbol类型的键,我们需要单独获取并拷贝:

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (obj instanceof Date) return new Date(obj.getTime());
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
  if (obj instanceof Error) {
    const cloneErr = new Error(obj.message);
    cloneErr.stack = obj.stack;
    return cloneErr;
  }
  if (obj instanceof Map) {
    const cloneMap = new Map();
    hash.set(obj, cloneMap);
    for (const [key, value] of obj) {
      cloneMap.set(deepClone(key, hash), deepClone(value, hash));
    }
    return cloneMap;
  }
  if (obj instanceof Set) {
    const cloneSet = new Set();
    hash.set(obj, cloneSet);
    for (const value of obj) {
      cloneSet.add(deepClone(value, hash));
    }
    return cloneSet;
  }

  if (hash.has(obj)) {
    return hash.get(obj);
  }

  const cloneObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, cloneObj);

  // 拷贝字符串键
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  // 拷贝Symbol键
  const symbolKeys = Object.getOwnPropertySymbols(obj);
  for (const key of symbolKeys) {
    cloneObj[key] = deepClone(obj[key], hash);
  }

  return cloneObj;
}

最终工业级版本

加上参数校验和注释,就是一个可以直接在生产环境中使用的深拷贝函数:

javascript 复制代码
/**
 * 工业级深拷贝函数
 * @param {*} obj - 需要拷贝的对象
 * @param {WeakMap} hash - 用于处理循环引用的缓存
 * @returns {*} 拷贝后的新对象
 */
function deepClone(obj, hash = new WeakMap()) {
  // 处理基本类型、null、undefined、BigInt、函数和Promise
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理特殊内置对象
  if (obj instanceof Date) return new Date(obj.getTime());
  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
  if (obj instanceof Error) {
    const cloneErr = new Error(obj.message);
    cloneErr.stack = obj.stack;
    return cloneErr;
  }

  // 处理ES6集合类型
  if (obj instanceof Map) {
    const cloneMap = new Map();
    hash.set(obj, cloneMap);
    for (const [key, value] of obj) {
      cloneMap.set(deepClone(key, hash), deepClone(value, hash));
    }
    return cloneMap;
  }
  if (obj instanceof Set) {
    const cloneSet = new Set();
    hash.set(obj, cloneSet);
    for (const value of obj) {
      cloneSet.add(deepClone(value, hash));
    }
    return cloneSet;
  }

  // 处理TypedArray
  if (obj instanceof ArrayBuffer) return obj.slice();
  if (ArrayBuffer.isView(obj)) {
    return new obj.constructor(obj.buffer.slice(), obj.byteOffset, obj.length);
  }

  // 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 创建新对象(保留原对象的原型链)
  const cloneObj = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
  hash.set(obj, cloneObj);

  // 拷贝所有可枚举属性(包括字符串键和Symbol键)
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  const symbolKeys = Object.getOwnPropertySymbols(obj);
  for (const key of symbolKeys) {
    cloneObj[key] = deepClone(obj[key], hash);
  }

  return cloneObj;
}

五、深浅拷贝核心区别对比表

对比维度 浅拷贝 深拷贝
拷贝深度 只拷贝第一层属性 递归拷贝所有层级属性
引用类型处理 只拷贝引用地址,共享同一块内存 创建全新的副本,完全独立
修改影响 修改深层属性会影响原对象 修改任何属性都不会影响原对象
性能 快,资源消耗小 慢,资源消耗大,层级越深越明显
实现难度 简单,一行代码即可 复杂,需要处理大量边界情况
适用场景 对象只有一层基本类型属性 对象包含多层引用类型,需要完全独立的副本

六、99%的前端都踩过的坑

坑1:展开运算符是深拷贝

错! 展开运算符只拷贝第一层属性,深层引用类型仍然共享。这是最常见的误区。

坑2:JSON.parse(JSON.stringify())是万能的

错! 它会丢失undefinedfunctionSymbol,错误处理DateRegExp,无法处理循环引用。

坑3:所有场景都应该用深拷贝

错! 深拷贝性能很差,对于只有一层基本类型的对象,浅拷贝完全足够,而且性能更好。

坑4:深拷贝可以拷贝一切

错! 函数的闭包、作用域,Promise的状态都是无法被拷贝的。所有主流库的深拷贝都会直接返回原函数和原Promise。


七、最佳实践

  1. 简单场景用浅拷贝 :如果对象只有一层基本类型属性,优先使用展开运算符...
  2. 复杂场景用第三方库 :生产环境中优先使用Lodash的_.cloneDeep(),它经过了无数项目的验证
  3. 不要自己写深拷贝:除非是面试需要,否则不要在生产环境中使用自己写的深拷贝函数,很难覆盖所有边界情况
  4. React/Vue中更新状态:必须创建新的对象引用,这时候浅拷贝通常就足够了

八、总结

  1. 深浅拷贝的本质区别是对引用类型的拷贝深度不同
  2. 浅拷贝只拷贝第一层,深层引用共享内存;深拷贝递归拷贝所有层级,完全独立
  3. JSON.parse(JSON.stringify())有很多缺陷,只能用于简单场景
  4. 工业级深拷贝需要处理循环引用、特殊内置对象、ES6集合类型、Symbol属性等边界情况
  5. 生产环境优先使用Lodash的_.cloneDeep(),不要重复造轮子

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!

相关推荐
前端毕业班2 小时前
前端"枚举"管理指南
前端·javascript
Jx6573 小时前
初学者视角下的JavaScript作用域理解
javascript
廖松洋(Alina)3 小时前
07答案比对与反馈UI-鸿蒙PC端Electron开发
javascript·ui·华为·electron·开源·harmonyos·鸿蒙
nexustech4 小时前
JavaScript日期处理工具date-fns,累计36.5k Star
开发语言·javascript·其他·ecmascript
Lan.W5 小时前
vue3-element-admin里新增mock接口一直没有生成,不生效
前端·javascript·vue.js·mock
仙古.梦回~5 小时前
vue-skills
前端·javascript·vue.js
gCode Teacher 格码致知5 小时前
Javascript提高:canvas画布的网格背景-由Deepseek产生
javascript·css·css3
清灵xmf6 小时前
JS 原生深拷贝的终极方案——structuredClone
前端·javascript·vue.js·json.stringify·structuredclone
前端 贾公子6 小时前
响应式系统基础:依赖追踪的基础 —— 发布订阅模式(前端应用最广的设计模式)上
javascript·vue.js