在 javascript 中,拷贝对象或数组是常见的操作。然而,由于引用类型的存在,拷贝分为浅拷贝和深拷贝两种方式。理解它们的区别对于避免意外的数据共享和修改至关重要。
基本概念
在 javascript 中,数据类型分为基本类型(如 string、number、boolean、null、undefined、symbol、bigint)和引用类型(如 Object、Array、Function、Date、RegExp 等)。
- 基本类型:存储的是实际值,拷贝时直接复制值,互不影响。
- 引用类型:存储的是内存地址,拷贝时复制的是地址引用,多个变量可能指向同一个对象。
浅拷贝
创建一个新对象,但这个新对象会复制原对象的所有属性值。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址,因此新旧对象会共享同一块内存数据,修改其中一个会影响另一个。
深拷贝
创建一个全新的对象,递归地复制原对象的所有属性。新对象和原对象完全隔离,修改互不影响。
浅拷贝的实现方式
- 手动赋值(只适用于简单对象)
1 2 3 4
| const obj1 = { a: 1, b: { c: 2 } }; const obj2 = { a: obj1.a, b: obj1.b }; // 浅拷贝 obj2.b.c = 100; console.log(obj1.b.c); // 100 (相互影响)
|
- Object.assign()
Object.assign(target, …sources) 将源对象的可枚举属性复制到目标对象,返回目标对象。它是浅拷贝。
1 2 3 4
| const obj1 = { a: 1, b: { c: 2 } }; const obj2 = Object.assign({}, obj1); obj2.b.c = 100; console.log(obj1.b.c); // 100
|
- 展开运算符 …
1 2 3 4
| const obj1 = { a: 1, b: { c: 2 } }; const obj2 = { ...obj1 }; obj2.b.c = 100; console.log(obj1.b.c); // 100
|
- 数组的浅拷贝方法
数组也是对象,许多数组方法返回的是浅拷贝。
1 2 3 4
| const arr1 = [1, 2, { a: 3 }]; const arr2 = arr1.slice(); arr2[2].a = 100; console.log(arr1[2].a); // 100
|
1
| const arr2 = arr1.concat();
|
1
| const arr2 = Array.from(arr1);
|
深拷贝的实现方式
JSON.parse(JSON.stringify(obj))
这是最常用的简单深拷贝方法,但存在一些限制:
- 无法拷贝函数、undefined、Symbol、BigInt。
- 无法处理循环引用(会抛出错误)。
- 特殊对象如 Date、RegExp、Map、Set 等会被序列化为字符串或空对象。
- 会丢弃对象的 constructor,原型链不再保留。
1 2 3 4 5 6 7 8 9 10 11
| const obj1 = { a: 1, b: { c: 2 }, d: new Date(), e: undefined, f: function() {}, g: Symbol('g') }; const obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj2); // { a: 1, b: { c: 2 }, d: "2024-01-25T..." } // 函数、undefined、Symbol 丢失,Date 变成了字符串
|
递归实现深拷贝(基础版)
手动编写递归函数,处理基本类型和对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); // 处理数组和普通对象 const cloneObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key]); } } return cloneObj; }
const obj1 = { a: 1, b: { c: 2 }, d: [1, 2, { e: 3 }] }; const obj2 = deepClone(obj1); obj2.b.c = 100; console.log(obj1.b.c); // 2
|
该基础版未处理循环引用、Map、Set、Symbol 属性等,但已满足大部分场景。
处理循环引用
使用 WeakMap 记录已拷贝的对象,遇到循环引用直接返回记录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function deepClone(obj, hash = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (hash.has(obj)) return hash.get(obj); // 处理循环引用
const clone = Array.isArray(obj) ? [] : {}; hash.set(obj, clone);
// 处理 Symbol 属性 const symKeys = Object.getOwnPropertySymbols(obj); if (symKeys.length) { symKeys.forEach(symKey => { clone[symKey] = deepClone(obj[symKey], hash); }); }
// 处理普通字符串属性 for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key], hash); } } return clone; }
// 测试循环引用 const obj = { a: 1 }; obj.self = obj; const cloned = deepClone(obj); console.log(cloned.self === cloned); // true
|
使用第三方库
lodash 的 _.cloneDeep():功能完善,处理了各种边界情况。
1 2
| const _ = require('lodash'); const obj2 = _.cloneDeep(obj1);
|
jQuery 的 $.extend(true, {}, obj)。
结构化克隆(Structured Clone)
现代浏览器提供 structuredClone() 全局函数,用于深拷贝,支持大部分内置类型(如 Date、RegExp、Map、Set、ArrayBuffer 等),但仍不能拷贝函数、Symbol、DOM 节点等。
1 2 3
| const obj1 = { a: 1, b: { c: 2 }, d: new Date() }; const obj2 = structuredClone(obj1); console.log(obj2); // 正确深拷贝
|
浅拷贝与深拷贝对比示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| // 原始对象 const original = { name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: ['reading', 'gaming'] };
// 浅拷贝 const shallowCopy = { ...original };
// 深拷贝(JSON 方法) const deepCopy = JSON.parse(JSON.stringify(original));
// 修改浅拷贝的嵌套对象 shallowCopy.address.city = 'Los Angeles'; shallowCopy.hobbies.push('swimming'); console.log(original.address.city); // 'Los Angeles' (原对象被改变) console.log(original.hobbies); // ['reading', 'gaming', 'swimming'] (原对象被改变)
// 修改深拷贝的嵌套对象 deepCopy.address.city = 'Chicago'; deepCopy.hobbies.push('running'); console.log(original.address.city); // 仍为 'Los Angeles' (不受影响) console.log(original.hobbies); // 仍为 ['reading', 'gaming', 'swimming'] (不受影响)
|
特殊场景与注意事项
- 函数和 undefined 属性:JSON.stringify 会丢失函数和 undefined,递归实现可以保留函数(拷贝函数引用),但通常函数是无状态的,浅拷贝即可。
- Symbol 作为属性名:JSON.stringify 会忽略 Symbol 属性,递归拷贝时可借助 Object.getOwnPropertySymbols 处理。
- Date、RegExp、Map、Set 等:JSON.stringify 会将其转换为字符串或普通对象,导致信息丢失。递归拷贝时应针对这些类型特殊处理。
- 原型链:浅拷贝和大多数深拷贝方法都不会复制原型链上的属性,只拷贝对象自身的可枚举属性。如果需要保留原型,可使用 Object.create(Object.getPrototypeOf(obj)) 并结合 Reflect.ownKeys 等方式。
- 性能:深拷贝递归遍历对象,可能影响性能,对于大型对象应谨慎使用,或考虑 immutable 数据结构。
总结
| 特性 |
浅拷贝 |
深拷贝 |
| 是否创建新对象 |
是 |
是 |
| 复制基本类型值 |
复制值 |
复制值 |
| 复制引用类型 |
复制引用(共享内存) |
递归复制新对象 |
| 修改嵌套属性影响原对象 |
会 |
不会 |
| 实现复杂度 |
简单(内置方法) |
较复杂(需递归) |
| 常见实现 |
Object.assign、…、Array.slice() |
JSON.parse(JSON.stringify())、递归、structuredClone、lodash |
选择浅拷贝还是深拷贝取决于需求:
- 如果对象只有一层属性(所有属性都是基本类型),浅拷贝即可。
- 如果对象包含嵌套结构,且希望完全隔离,必须使用深拷贝。
- 注意深拷贝的性能和边界情况,必要时选择成熟库或原生 structuredClone。