别再踩坑!一文搞懂 JS 深浅拷贝:为什么修改拷贝对象会影响原始数据?

129 阅读6分钟

今天在开发过程中编写代码时,没想到在深浅拷贝的问题上栽了跟头,因此特意重新梳理了相关知识。下面跟我复习一下关于深浅拷贝吧!

一、为什么需要深浅拷贝?

要理解深浅拷贝,首先要明白JavaScript的数据类型存储机制:

  • 基本类型:Number、String、Boolean等,直接存储在栈内存中

  • 引用类型:Object、Array等,地址存储在栈内存,真实数据在堆内存

举个简单的例子:

const arr = [1, 2, 3];
const newArr = arr;
newArr.push(4);
console.log(arr); // 输出 [1, 2, 3, 4]

这里newArrarr指向同一块内存地址,修改newArr会直接影响arr。这就是典型的引用传递带来的副作用。要避免这种问题,就必须掌握正确的拷贝姿势。深浅拷贝的出现就是为了解决这个问题:浅拷贝复制一层引用,深拷贝则递归复制所有层级。

二、浅拷贝,浅拷贝复制一层属性

浅拷贝的本质

浅拷贝会创建新对象,复制原始对象属性值。当属性是基本类型时直接复制值,引用类型时复制内存地址。

常见实现方法

1. Object.assign()

const obj = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, obj);
copy.b.c = 3;
console.log(obj.b.c); // 输出 3

优缺点分析

  • 优点
    • 简单易用,无需额外代码
    • 明确语义(ES6标准方法)
  • 缺点
    • 无法处理嵌套引用类型
    • 仅拷贝对象自身可枚举属性
    • 不保留原型链属性
// 示例:原型链属性丢失
const parent = { a: 1 };
const obj = Object.create(parent, {
  b: { value: 2, enumerable: true } 
});
const copy = Object.assign({}, obj);
console.log(copy.a); // undefined(原型链属性未拷贝)

2. 展开运算符(...)

const arr = [1, [2, 3]];
const copy = [...arr];
copy[1][0] = 4;
console.log(arr[1][0]); // 输出 4

优缺点分析

  • 优点
    • 语法简洁直观
    • 支持对象和数组
  • 缺点
    • 无法处理嵌套引用
    • 不保留构造函数信息

3. 数组方法(concat、slice)

const arr = [1, [2, 3]];
const copy = arr.concat();
copy[1][0] = 4;
console.log(arr[1][0]); // 输出 4

优缺点分析

  • 优点
    • ES5兼容性好
    • 明确数组合并语义
  • 缺点
    • 仅限数组使用
    • 语义不直观

浅拷贝有一个致命缺陷,遇到嵌套对象时就会暴露问题。例如:

const obj = {
  name: '张三',
  family: {
    child: '小张三'
  }
};
const copy = { ...obj };
copy.family.child = '大张三';
console.log(obj.family.child); // 输出 '大张三'

此时修改拷贝对象的family属性,原始对象也会被修改,这就是典型的浅拷贝陷阱。

三、深拷贝:递归复制所有层级

深拷贝的本质

深拷贝会递归遍历对象的所有层级,为每个引用类型创建新的内存空间。这样新对象和原对象完全独立,互不影响。

常见实现方法

1. JSON大法

const obj = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(obj));
copy.b.c = 3;
console.log(obj.b.c); // 输出 2

优缺点分析

  • 优点:简单易用,无需额外代码

  • 缺点

    • 无法处理循环引用
    • 忽略特殊类型
    • Date对象转为字符串
    • 性能低下(大数据量时明显卡顿)
//无法处理循环引用
const obj = { a: null };
obj.a = obj;
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
//忽略特殊类型
const data = { 
  fn: () => {}, 
  sym: Symbol(), 
  undef: undefined 
};
const copy = JSON.parse(JSON.stringify(data));
console.log(copy); // {}(函数/Symbol/undefined丢失)
//Date对象转为字符串
const obj = { date: new Date() };
const copy = JSON.parse(JSON.stringify(obj));
console.log(typeof obj.date); // object
console.log(typeof copy.date); // string

2. 递归实现

function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;
  const isArray = Array.isArray(obj);
  const copy = isArray ? [] : {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key]);
    }
  }
  return copy;
}

增强递归实现

优化点

  1. 处理数组和对象的区别
  2. 处理循环引用(使用 WeakMap)
  3. 支持特殊对象(Date、RegExp 等)
function deepClone(obj, map = new WeakMap()) {
  if (typeof obj !== 'object' || obj === null) return obj;
  if (map.has(obj)) return map.get(obj);
  
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  
  const isArray = Array.isArray(obj);
  const copy = isArray ? [] : {};
  map.set(obj, copy);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepClone(obj[key], map);
    }
  }
  return copy;
}

3. 第三方库实现

Lodash的_.cloneDeep是最佳实践:

import _ from 'lodash';
const obj = { a: 1, b: { c: 2 } };
const copy = _.cloneDeep(obj);
copy.b.c = 3;
console.log(obj.b.c); // 输出 2

优缺点分析

  • 优点
    • 开箱即用,处理所有边缘情况
    • 支持复杂类型(Map/Set等)
  • 缺点
    • 需引入第三方库,增加项目体积

4.现代浏览器原生API

const obj = { a: 1, b: { c: 2 } };
const copy = structuredClone(obj);
copy.b.c = 3;
console.log(obj.b.c); // 输出 2

支持处理更多数据类型,但是要注意兼容性的问题

四、该用哪种拷贝方式?

浅拷贝方法对比

方法适用场景代码示例优点局限性
Object.assign()对象拷贝Object.assign({}, obj)明确语义,ES6标准方法只能处理对象,需空对象占位
展开运算符对象/数组拷贝{...obj} / [...arr]语法简洁,直观易读无法处理深层嵌套
Array.prototype.concat数组拷贝[].concat(arr)兼容性好(ES5)仅限数组,语义不直观
Array.prototype.slice数组拷贝arr.slice()零参数快速拷贝方法名易与截取操作混淆

深拷贝方法对比

方法适用场景代码示例优点局限性
JSON方法纯数据对象临时拷贝JSON.parse(JSON.stringify(obj))实现简单、无需第三方库丢失函数/Symbol/undefined;Date转为字符串;无法处理循环引用
基础递归学习/简单对象拷贝function clone(o){...递归逻辑}理解原理、自定义灵活无法处理循环引用;不处理特殊对象(Date/RegExp等);Symbol键名丢失
增强递归需要保留特殊类型的自研场景递归+WeakMap+类型判断支持循环引用和特殊类型开发成本高;性能较差;需手动维护类型判断逻辑
lodash.cloneDeep生产环境复杂业务数据import _ from 'lodash'; _.cloneDeep(obj)开箱即用;处理所有边缘情况增加项目体积(约17KB);需引入第三方库
structuredClone现代浏览器环境const copy = structuredClone(obj)原生API性能最佳

这么多的方法,在具体使用时,该怎么选择? 我们需要根据具体场景选择合适的方法:

  1. 简单对象 ➡️ 扩展运算符
  2. 需要保留方法 ➡️ 递归+类型判断
  3. 复杂业务场景 ➡️ lodash.cloneDeep
  4. 现代浏览器环境 ➡️ structuredClone

五、总结

深浅拷贝是 JavaScript 中处理数据复制的核心概念。浅拷贝适用于单层对象,深拷贝则用于复杂数据结构。

对比维度浅拷贝深拷贝
拷贝层级仅第一层递归所有层级
内存引用嵌套对象仍共享内存地址完全独立的内存空间
性能高(仅复制一层)低(递归遍历消耗资源)
适用场景简单数据隔离复杂数据完全隔离
典型问题修改嵌套属性会影响原对象处理循环引用和特殊对象较复杂

结尾互动
你在开发中遇到过哪些因深浅拷贝导致的 “玄学问题”?欢迎在评论区分享你的踩坑经历,或提出关于拷贝性能优化的疑问,一起讨论交流!如果觉得本文对你有帮助,别忘了点赞收藏,让更多开发者看到这份深浅拷贝避坑指南~ 😊