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);
五、性能比较
对于小型对象,各种方法性能差异不大。但对于大型对象:
JSON.parse(JSON.stringify())通常最快,但功能最有限- 递归实现最灵活但性能较差
- 第三方库通常在性能和功能之间取得平衡
六、实际应用建议
- 简单对象:使用展开运算符或
Object.assign()进行浅拷贝 - 需要完整深拷贝:
- 如果对象不包含函数、Symbol、undefined 等特殊值,且不担心循环引用,使用 JSON 方法
- 否则使用递归实现或 Lodash 的
_.cloneDeep()
- 处理大型对象:考虑性能需求选择合适方法
七、总结
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 复制层级 | 仅第一层 | 所有层级 |
| 引用类型属性 | 共享引用 | 完全独立副本 |
| 实现难度 | 简单 | 复杂(需处理多种情况) |
| 性能 | 高 | 较低 |
| 适用场景 | 简单对象,不需要独立引用类型属性 | 需要完全独立的对象副本 |
理解深浅拷贝的区别并选择合适的拷贝方法,是避免 JavaScript 开发中许多难以调试问题的关键。