JavaScript 中的深拷贝与浅拷贝:原理、区别与实现

JavaScript 中的深拷贝与浅拷贝:原理、区别与实现

在 JavaScript 开发中,拷贝对象是一个常见操作,但很多人对浅拷贝(Shallow Copy)和深拷贝(Deep Copy)的区别并不清晰。

浅拷贝只是增加了一个指针指向已存在的内存地址,仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。

深拷贝是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。

本文将深入探讨这两种拷贝方式的原理、区别,并提供多种实现方法。

一、基本概念

1. 浅拷贝

浅拷贝只复制对象的第一层属性,如果属性是引用类型,则复制的是引用(内存地址),新旧对象会共享这些引用类型的属性。

2. 深拷贝

深拷贝会递归复制对象的所有层级,创建一个完全独立的新对象,新旧对象不共享任何引用。

二、为什么需要区分深浅拷贝?

考虑以下场景:

const original = { name: 'John', hobbies: ['reading', 'swimming'], address: { city: 'New York' } }; // 假设我们进行了某种拷贝操作得到 copied // 然后修改 copied 的属性 copied.hobbies.push('coding'); copied.address.city = 'Boston'; // 如果拷贝是浅拷贝,original 也会被修改 // 如果是深拷贝,original 保持不变

三、浅拷贝的实现方法

1. 使用展开运算符 (... )

const original = { a: 1, b: { c: 2 } }; const shallowCopy = { ...original }; original.a = 10; console.log(shallowCopy.a); // 1 (不受影响) original.b.c = 20; console.log(shallowCopy.b.c); // 20 (受影响)

2. 使用 Object.assign()

const original = { a: 1, b: { c: 2 } }; const shallowCopy = Object.assign({}, original); // 效果与展开运算符相同

3. 数组的浅拷贝方法

// 1. slice() const arr = [1, 2, { a: 3 }]; const arrCopy1 = arr.slice(); // 2. concat() const arrCopy2 = arr.concat(); // 3. 展开运算符 const arrCopy3 = [...arr]; // 修改嵌套对象会影响原数组 arrCopy1[2].a = 100; console.log(arr[2].a); // 100

四、深拷贝的实现方法

1. 使用 JSON 方法(最简单但有局限)

const original = { name: 'John', hobbies: ['reading', 'swimming'], address: { city: 'New York' }, date: new Date(), // 会丢失 regex: /test/gi, // 会丢失 undefinedProp: undefined // 会丢失 }; const deepCopy = JSON.parse(JSON.stringify(original)); // 修改深拷贝后的对象不会影响原对象 deepCopy.address.city = 'Boston'; console.log(original.address.city); // 'New York' // 缺点: // 1. 无法复制函数、Symbol、undefined // 2. 会丢失 Date 对象的日期值(转为字符串) // 3. 无法处理循环引用

2. 递归实现深拷贝(完整版)

function deepClone(obj, hash = new WeakMap()) { // 处理基本类型和 null/undefined 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); } // 处理 Map if (obj instanceof Map) { const cloneMap = new Map(); hash.set(obj, cloneMap); obj.forEach((value, key) => { cloneMap.set(key, deepClone(value, hash)); }); return cloneMap; } // 处理 Set if (obj instanceof Set) { const cloneSet = new Set(); hash.set(obj, cloneSet); obj.forEach(value => { cloneSet.add(deepClone(value, hash)); }); return cloneSet; } // 处理数组和对象 const cloneObj = Array.isArray(obj) ? [] : {}; hash.set(obj, cloneObj); for (let key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key], hash); } } // 处理 Symbol 属性 const symbolKeys = Object.getOwnPropertySymbols(obj); for (let symKey of symbolKeys) { cloneObj[symKey] = deepClone(obj[symKey], hash); } return cloneObj; } // 使用示例 const original = { a: 1, b: { c: 2 }, d: new Date(), e: /test/gi, f: new Set([1, 2, 3]), g: new Map([['key', 'value']]), [Symbol('sym')]: 'symbol value' }; const cloned = deepClone(original); console.log(cloned);

3. 使用第三方库

许多成熟的库提供了可靠的深拷贝实现:

  • Lodash 的 _.cloneDeep()
  • jQuery 的 $.extend(true, {}, original)
// 使用 Lodash const _ = require('lodash'); const original = { /* 复杂对象 */ }; const cloned = _.cloneDeep(original);

五、性能比较

对于小型对象,各种方法性能差异不大。但对于大型对象:

  1. JSON.parse(JSON.stringify()) 通常最快,但功能最有限
  2. 递归实现最灵活但性能较差
  3. 第三方库通常在性能和功能之间取得平衡

六、实际应用建议

  1. 简单对象:使用展开运算符或 Object.assign() 进行浅拷贝
  2. 需要完整深拷贝
    • 如果对象不包含函数、Symbol、undefined 等特殊值,且不担心循环引用,使用 JSON 方法
    • 否则使用递归实现或 Lodash 的 _.cloneDeep()
  3. 处理大型对象:考虑性能需求选择合适方法

七、总结

特性浅拷贝深拷贝
复制层级仅第一层所有层级
引用类型属性共享引用完全独立副本
实现难度简单复杂(需处理多种情况)
性能较低
适用场景简单对象,不需要独立引用类型属性需要完全独立的对象副本

理解深浅拷贝的区别并选择合适的拷贝方法,是避免 JavaScript 开发中许多难以调试问题的关键。