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

204 阅读6分钟

引言

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

40.jpg

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

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

赋值的本质

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

赋值的应用场景

赋值操作适用于:

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

示例

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = obj1; // 赋值

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

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

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

特点

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

浅拷贝的局限性

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

实现方法

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

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

obj2.b.c = 3;
console.log(obj1.b.c); // 3(原对象被修改)
(3) 数组的 slice 或 concat
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
    • 不能处理循环引用。
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) 递归实现
  • 手动实现深拷贝,支持所有数据类型。
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 方法。
npm install lodash
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 循环引用

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 特殊对象处理

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:

// 在浏览器环境中
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 实现按需深拷贝:

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;
    }
  });
}

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

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

八、开发者的终极拷问

17.jpg

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

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

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

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

十、总结:

复制三原则

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

总结对比表

特性赋值浅拷贝深拷贝
基本类型复制值同赋值同赋值
引用类型复制引用新对象,复制一级属性新对象,递归复制所有属性
嵌套对象影响共享共享嵌套对象完全独立
性能最优较好较差
实现方式=...Object.assign()JSON方法_.cloneDeep()
循环引用天然支持支持需要特殊处理
函数/Symbol保留保留JSON方法会丢失