《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方法会丢失
相关推荐
知否技术几秒前
面试官最爱问的Vue3响应式原理:我给你讲明白了!
前端·vue.js
ylfhpy24 分钟前
Java面试黄金宝典19
java·开发语言·数据结构·算法·面试·面经
Angelyb39 分钟前
前端Vue
开发语言·javascript·ecmascript
小周同学:1 小时前
vue将页面导出成word
前端·vue.js·word
阿杰在学习1 小时前
基于OpenGL ES实现的Android人体热力图可视化库
android·前端·opengl
xfq1 小时前
[ai] cline使用总结(包括mcp)
前端·后端·ai编程
weiran19991 小时前
手把手的建站思路和dev-ops方案
前端·后端·架构
自不量力的A同学1 小时前
Next.js 中间件曝高危漏洞 CVE-2025-29927,授权绕过风险波及全版本
开发语言·javascript·中间件
小刀飘逸1 小时前
子元素 margin-top 导致父元素下移问题的分析与解决方案
前端
Evrytos1 小时前
告别石器时代#2:ES6新数据类型
前端·javascript