《JavaScript的“套娃陷阱”:90%程序员栽过的三种复制大坑》

引言

在 JavaScript 中,赋值浅拷贝深拷贝 是处理对象和数组时的三种常见操作。如果把 JavaScript 的数据操作比作文件管理,赋值就像创建快捷方式,浅拷贝类似复制文件夹结构,而深拷贝才是真正的文件克隆。三者各司其职,用错场景就会导致数据混乱!

一. 赋值:共享内存的引用

赋值是 JavaScript 中最基础的操作之一,但对于引用类型(对象、数组等)来说,赋值操作实际上只是复制了引用(内存地址),而不是创建新的独立数据。

赋值的本质

  • 基本类型:赋值是值的拷贝(字符串、数字、布尔值等)
  • 引用类型:赋值是引用的拷贝(对象、数组、函数等)

赋值的应用场景

赋值操作适用于:

  • 需要多个变量指向同一对象时
  • 函数参数传递(JavaScript 中参数是按值传递,但对于对象是传递引用的值)

示例

javascript 复制代码
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = obj1; // 赋值

obj2.a = 10;
console.log(obj1.a); // 10(原对象被修改)

二. 浅拷贝(Shallow Copy):一级属性的独立副本

浅拷贝创建一个新对象,并复制原始对象的一级属性。如果属性是基本类型,则复制值;如果是引用类型,则复制引用。

特点

  • 修改嵌套对象的属性会影响原对象。
  • 适用于简单对象(没有嵌套对象或数组)。

浅拷贝的局限性

浅拷贝只能保证第一层属性的独立性,嵌套对象仍然是共享的。这在某些场景下会导致意外的数据污染。

实现方法

(1) 扩展运算符(...
javascript 复制代码
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { ...obj1 }; // 浅拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 3(原对象被修改)
(2) Object.assign
javascript 复制代码
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1); // 浅拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 3(原对象被修改)
(3) 数组的 sliceconcat
javascript 复制代码
const arr1 = [1, 2, { a: 3 }];
const arr2 = arr1.slice(); // 浅拷贝

arr2[2].a = 4;
console.log(arr1[2].a); // 4(原数组被修改)

三. 深拷贝(Deep Copy):完全的独立副本

深拷贝创建一个全新的对象,并递归复制原始对象的所有属性,包括嵌套对象,使得新旧对象完全独立,互不影响。

特点

  • 修改嵌套对象的属性不会影响原对象。
  • 适用于复杂对象(包含嵌套对象或数组)。

实现方法

(1) JSON.parse(JSON.stringify(obj))
  • 简单易用,但有以下限制:
    • 不能复制函数、undefinedSymbol
    • 不能处理循环引用。
javascript 复制代码
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1)); // 深拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
(2) 递归实现
  • 手动实现深拷贝,支持所有数据类型。
javascript 复制代码
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  const clone = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }
  return clone;
}

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1); // 深拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
(3) 使用第三方库
  • 使用 Lodash 的 cloneDeep 方法。
bash 复制代码
npm install lodash
javascript 复制代码
import _ from 'lodash';

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = _.cloneDeep(obj1); // 深拷贝

obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)

四、对比分析

特性 赋值 浅拷贝 深拷贝
基本类型 值复制 值复制 值复制
引用类型 引用复制 一级属性值复制,嵌套属性引用复制 完全独立复制
内存占用 最小 中等 最大
性能 最快 较快 较慢
修改原对象影响 互相影响 一级属性不影响,嵌套属性影响 完全不影响

黄金定律

  1. 超过 3 层嵌套考虑深拷贝
  2. 数据量 > 1MB 时慎用 JSON 法
  3. 循环结构必须用 WeakMap 方案

五、特殊情况的处理

5.1 循环引用

javascript 复制代码
let obj = { a: 1 };
obj.self = obj;

// 简单的深拷贝会栈溢出
function cloneDeep(obj) {
  const cloned = {};
  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      cloned[key] = cloneDeep(obj[key]);
    } else {
      cloned[key] = obj[key];
    }
  }
  return cloned;
}

// 使用WeakMap解决循环引用
function cloneDeepWithCircular(obj, hash = new WeakMap()) {
  if (hash.has(obj)) return hash.get(obj);
  
  let cloned = Array.isArray(obj) ? [] : {};
  hash.set(obj, cloned);
  
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloned[key] = typeof obj[key] === 'object' ? 
        cloneDeepWithCircular(obj[key], hash) : obj[key];
    }
  }
  
  return cloned;
}

5.2 特殊对象处理

javascript 复制代码
function cloneDeep(obj, hash = new WeakMap()) {
  // 处理基本类型和null
  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);
  
  // 处理RegExp对象
  if (obj instanceof RegExp) return new RegExp(obj);
  
  // 处理Set
  if (obj instanceof Set) {
    let clonedSet = new Set();
    hash.set(obj, clonedSet);
    obj.forEach(value => {
      clonedSet.add(cloneDeep(value, hash));
    });
    return clonedSet;
  }
  
  // 处理Map
  if (obj instanceof Map) {
    let clonedMap = new Map();
    hash.set(obj, clonedMap);
    obj.forEach((value, key) => {
      clonedMap.set(cloneDeep(key, hash), cloneDeep(value, hash));
    });
    return clonedMap;
  }
  
  // 处理普通对象和数组
  let clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);
  
  // 处理Symbol属性
  let symKeys = Object.getOwnPropertySymbols(obj);
  if (symKeys.length) {
    symKeys.forEach(symKey => {
      clone[symKey] = cloneDeep(obj[symKey], hash);
    });
  }
  
  // 处理普通属性
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = cloneDeep(obj[key], hash);
    }
  }
  
  return clone;
}

六、现代JavaScript中的新特性

6.1 结构化克隆(Structured Clone)

HTML5 引入了结构化克隆算法,可用于 Worker 间通信或存储 API:

javascript 复制代码
// 在浏览器环境中
let original = { a: 1, b: { c: 2 } };
let deepCopy = structuredClone(original);

// Node.js 中的类似功能
const v8 = require('v8');
let deepCopy = v8.deserialize(v8.serialize(original));

6.2 使用Proxy实现惰性拷贝

对于大型对象,可以结合 Proxy 实现按需深拷贝:

javascript 复制代码
function createLazyDeepCopy(obj) {
  const cache = new Map();
  
  return new Proxy(obj, {
    get(target, prop) {
      const value = Reflect.get(target, prop);
      
      if (typeof value === 'object' && value !== null) {
        if (!cache.has(prop)) {
          cache.set(prop, createLazyDeepCopy(value));
        }
        return cache.get(prop);
      }
      
      return value;
    }
  });
}

七、决策流程图:如何选择复制方式?

复制代码
开始
↓
需要完全独立副本? → 是 → 深拷贝
↓否
需要共享数据变化? → 是 → 赋值
↓否
对象只有一层结构? → 是 → 浅拷贝
↓否
返回第一步重新思考需求

八、开发者的终极拷问

  1. 为什么 structuredClone 不能克隆函数?
    → 答:函数可能包含闭包等运行环境信息,如同不能复制一个人的记忆
  2. 深拷贝会复制原型链吗?
    → 答:大多数方案不会,如同复印文件不会复制打印机型号
  3. 如何判断该用哪种拷贝方式?
    → 记住口诀:"一变则变用赋值,浅层独立用浅拷,完全独立深拷贝"

九、课后彩蛋:console.log 的隐藏特性

javascript 复制代码
const obj = { a: 1 };
console.log(obj); // 输出时可能显示修改后的值!

原理:控制台输出的是对象的实时引用,如同查看监控摄像头而非拍摄照片

十、总结:

复制三原则

  1. 经济原则:能赋值不拷贝,能浅拷不深拷
  2. 安全原则:处理循环引用和特殊对象就像拆炸弹
  3. 性能原则:深拷贝是最后的底牌,不是首选方案

总结对比表

特性 赋值 浅拷贝 深拷贝
基本类型 复制值 同赋值 同赋值
引用类型 复制引用 新对象,复制一级属性 新对象,递归复制所有属性
嵌套对象影响 共享 共享嵌套对象 完全独立
性能 最优 较好 较差
实现方式 = ..., Object.assign() JSON方法, _.cloneDeep()
循环引用 天然支持 支持 需要特殊处理
函数/Symbol 保留 保留 JSON方法会丢失
相关推荐
编程猪猪侠25 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞29 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js
@大迁世界1 小时前
第7章 React性能优化核心
前端·javascript·react.js·性能优化·前端框架