JavaScript中的拷贝问题:深入理解深浅拷贝

129 阅读4分钟

JavaScript中的拷贝问题:深入理解深浅拷贝

在JavaScript开发中,拷贝对象是常见的操作,但深浅拷贝的区别常常让开发者困惑。本文将深入探讨拷贝的机制、方法和陷阱。

一、JavaScript数据类型与存储机制

数据类型分类

  • 基本类型:Number、String、Boolean、null、undefined、Symbol、BigInt
  • 引用类型:Object、Array、Function、Date等

内存存储方式

  • 栈内存:存储基本类型值和引用类型的地址指针
  • 堆内存:存储引用类型的实际数据
// 基本类型存储在栈中
let a = 10;
let b = a; // 创建值的副本
a = 20;
console.log(b); // 10 - b不受a影响

// 引用类型存储在堆中
let obj1 = { name: 'Alice' };
let obj2 = obj1; // 复制引用地址
obj1.name = 'Bob';
console.log(obj2.name); // 'Bob' - 两者共享同一内存

二、浅拷贝:只复制表层

浅拷贝只复制对象的第一层属性,当属性是引用类型时,复制的是地址引用。

常用浅拷贝方法

// 1. Object.assign()
let obj = { a: 1, b: { c: 2 } };
let shallow1 = Object.assign({}, obj);

// 2. 扩展运算符
let shallow2 = { ...obj };

// 3. Array.prototype.concat()
let arr = [1, 2, { d: 3 }];
let shallow3 = [].concat(arr);

// 4. Array.prototype.slice()
let shallow4 = arr.slice();

// 5. Array.from()
let shallow5 = Array.from(arr);

浅拷贝的陷阱

let original = {
  name: 'John',
  hobbies: ['reading', 'swimming']
};

let copy = { ...original };

// 修改原始对象的基本类型属性
original.name = 'Mike';
console.log(copy.name); // 'John' - 不受影响

// 修改原始对象的引用类型属性
original.hobbies.push('running');
console.log(copy.hobbies); // ['reading', 'swimming', 'running'] - 被影响!

三、深拷贝:完全克隆对象

深拷贝创建对象及其嵌套对象的完全独立副本,新旧对象互不影响。

常用深拷贝方法

// 1. JSON方法(最常用但有局限)
let deep1 = JSON.parse(JSON.stringify(obj));

// 2. structuredClone()(现代浏览器支持)
let deep2 = structuredClone(obj);

JSON方法的局限性

let obj = {
  a: undefined,    // 丢失
  b: Symbol('id'), // 丢失
  c: function() {}, // 丢失
  d: new Date(),    // 转为字符串
  e: BigInt(100),  // 报错
  f: new Map(),     // 转为空对象
  g: new Set(),     // 转为空对象
  h: obj            // 循环引用报错
};

let clone = JSON.parse(JSON.stringify(obj));
console.log(clone); 
// 输出: { d: "2023-08-17T12:34:56.789Z" }

四、手写深拷贝函数

基础版本:处理基本类型和普通对象

function deepClone(target) {
  // 基本类型直接返回
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  
  // 处理数组
  const clone = Array.isArray(target) ? [] : {};
  
  for (let key in target) {
    // 只复制自有属性(非原型链上的属性)
    if (target.hasOwnProperty(key)) {
      clone[key] = deepClone(target[key]);
    }
  }
  
  return clone;
}

进阶版本:处理特殊对象和循环引用

function deepClone(target, map = new WeakMap()) {
  // 基本类型直接返回
  if (typeof target !== 'object' || target === null) {
    return target;
  }
  
  // 处理循环引用
  if (map.has(target)) {
    return map.get(target);
  }
  
  // 初始化克隆对象
  let clone;
  
  // 处理特殊对象类型
  switch (true) {
    case Array.isArray(target):
      clone = [];
      break;
    case target instanceof Date:
      clone = new Date(target);
      break;
    case target instanceof Map:
      clone = new Map();
      target.forEach((value, key) => {
        clone.set(key, deepClone(value, map));
      });
      break;
    case target instanceof Set:
      clone = new Set();
      target.forEach(value => {
        clone.add(deepClone(value, map));
      });
      break;
    case target instanceof RegExp:
      clone = new RegExp(target.source, target.flags);
      break;
    default:
      clone = Object.create(Object.getPrototypeOf(target));
  }
  
  // 记录已拷贝对象
  map.set(target, clone);
  
  // 递归复制属性
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      clone[key] = deepClone(target[key], map);
    }
  }
  
  return clone;
}

测试深拷贝函数

const original = {
  name: 'Alice',
  age: 30,
  hobbies: ['reading', 'traveling'],
  meta: {
    created: new Date(),
    tags: new Set(['js', 'web']),
    settings: new Map([['theme', 'dark'], ['notifications', true]])
  },
  getInfo() {
    return `${this.name}, ${this.age}`;
  }
};

// 创建循环引用
original.self = original;

const clone = deepClone(original);

// 修改原始对象
original.hobbies.push('swimming');
original.meta.settings.set('theme', 'light');

console.log(clone.hobbies); // ['reading', 'traveling']
console.log(clone.meta.settings.get('theme')); // 'dark'
console.log(clone.self === clone); // true (循环引用正确处理)

五、拷贝方法对比指南

方法/特性浅/深拷贝处理函数处理Symbol处理循环引用性能
赋值(=)无拷贝最高
扩展运算符(...)浅拷贝
Object.assign()浅拷贝
Array.prototype.slice浅拷贝
JSON方法深拷贝
structuredClone()深拷贝
手写深拷贝函数深拷贝

六、最佳实践建议

  1. 优先使用浅拷贝:当对象没有嵌套引用类型时
  2. 使用JSON方法:处理简单数据结构时(无特殊类型和循环引用)
  3. 使用structuredClone():现代项目中处理较复杂对象
  4. 实现自定义深拷贝:需要处理函数、Symbol等特殊类型时
  5. 避免拷贝大对象:考虑性能影响,尤其递归深拷贝

总结

理解深浅拷贝的区别是JavaScript开发的基本功。浅拷贝适合简单对象,而深拷贝在需要完全隔离对象时必不可少。现代浏览器提供的structuredClone()是个不错的选择,但对于复杂场景,实现自定义深拷贝函数仍是必要的技能。根据具体需求选择合适的方法,才能在性能和功能间取得最佳平衡。