JavaScript 浅拷贝与深拷贝详解
2024/8/22大约 6 分钟
JavaScript 浅拷贝与深拷贝详解
什么是拷贝
在 JavaScript 中,当我们需要复制一个变量时,根据复制的深度不同,可以分为浅拷贝和深拷贝。理解这两种拷贝方式的区别对于避免程序中的意外行为至关重要。
数据类型回顾
基本数据类型(值类型)
numberstringbooleannullundefinedsymbolbigint
引用数据类型
ObjectArrayFunctionDateRegExp- 等等
基本数据类型的复制
// 基本数据类型的复制是值复制
let a = 10;
let b = a; // 复制 a 的值给 b
a = 20;
console.log(a); // 20
console.log(b); // 10 - b 不受 a 的改变影响引用数据类型的复制问题
// 引用类型的直接赋值是引用复制
let obj1 = { name: 'Alice', age: 25 };
let obj2 = obj1; // obj2 和 obj1 指向同一个对象
obj1.name = 'Bob';
console.log(obj1.name); // 'Bob'
console.log(obj2.name); // 'Bob' - obj2 也被影响了!浅拷贝(Shallow Copy)
定义
浅拷贝创建一个新对象,但只复制对象的第一层属性。如果属性值是引用类型,则只复制引用地址。
浅拷贝的实现方法
1. Object.assign()
const original = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'swimming']
};
const copy = Object.assign({}, original);
// 修改第一层属性
copy.name = 'Bob';
console.log(original.name); // 'Alice' - 原对象不受影响
// 修改嵌套属性
copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'swimming', 'coding'] - 原对象被影响!2. 扩展运算符(...)
const original = {
name: 'Alice',
age: 25,
address: {
city: 'Shanghai',
street: 'Nanjing Road'
}
};
const copy = { ...original };
copy.name = 'Bob';
console.log(original.name); // 'Alice' - 不受影响
copy.address.city = 'Beijing';
console.log(original.address.city); // 'Beijing' - 被影响了!3. Array.from()(数组)
const originalArray = [1, 2, [3, 4]];
const copyArray = Array.from(originalArray);
copyArray[0] = 10;
console.log(originalArray[0]); // 1 - 不受影响
copyArray[2][0] = 30;
console.log(originalArray[2][0]); // 30 - 被影响了!4. slice()(数组)
const originalArray = [
{ name: 'Alice' },
{ name: 'Bob' }
];
const copyArray = originalArray.slice();
copyArray[0].name = 'Charlie';
console.log(originalArray[0].name); // 'Charlie' - 被影响了!5. concat()(数组)
const originalArray = [1, 2, { value: 3 }];
const copyArray = [].concat(originalArray);
copyArray[2].value = 30;
console.log(originalArray[2].value); // 30 - 被影响了!深拷贝(Deep Copy)
定义
深拷贝创建一个新对象,并递归复制所有层级的属性,包括嵌套的对象和数组。
深拷贝的实现方法
1. JSON 方法(有限制)
const original = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'swimming'],
address: {
city: 'Shanghai',
coordinates: {
lat: 31.2304,
lng: 121.4737
}
}
};
const copy = JSON.parse(JSON.stringify(original));
copy.address.city = 'Beijing';
copy.hobbies.push('coding');
console.log(original.address.city); // 'Shanghai' - 不受影响
console.log(original.hobbies); // ['reading', 'swimming'] - 不受影响JSON 方法的局限性:
const problematic = {
func: function() { return 'hello'; },
date: new Date(),
undefined: undefined,
symbol: Symbol('test'),
regex: /test/g,
infinity: Infinity,
nan: NaN
};
const copy = JSON.parse(JSON.stringify(problematic));
console.log(copy);
// {
// date: "2023-01-01T00:00:00.000Z", // Date 变成了字符串
// infinity: null, // Infinity 变成了 null
// nan: null // NaN 变成了 null
// // func, undefined, symbol, regex 都丢失了
// }2. 递归实现深拷贝
function deepClone(obj, visited = new WeakMap()) {
// 处理 null 和基本数据类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理循环引用
if (visited.has(obj)) {
return visited.get(obj);
}
// 处理 Date 对象
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// 处理 RegExp 对象
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 处理数组
if (Array.isArray(obj)) {
const clonedArray = [];
visited.set(obj, clonedArray);
for (let i = 0; i < obj.length; i++) {
clonedArray[i] = deepClone(obj[i], visited);
}
return clonedArray;
}
// 处理普通对象
const clonedObj = {};
visited.set(obj, clonedObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key], visited);
}
}
return clonedObj;
}
// 使用示例
const original = {
name: 'Alice',
birthday: new Date('1990-01-01'),
pattern: /test/gi,
nested: {
values: [1, 2, 3],
deep: {
value: 'deep value'
}
}
};
const copy = deepClone(original);
copy.nested.deep.value = 'modified';
console.log(original.nested.deep.value); // 'deep value' - 不受影响3. 使用 Lodash 库
const _ = require('lodash');
const original = {
name: 'Alice',
hobbies: ['reading', 'swimming'],
address: {
city: 'Shanghai'
}
};
const copy = _.cloneDeep(original);
copy.address.city = 'Beijing';
console.log(original.address.city); // 'Shanghai' - 不受影响4. 使用 Ramda 库
const R = require('ramda');
const original = {
name: 'Alice',
hobbies: ['reading'],
address: { city: 'Shanghai' }
};
const copy = R.clone(original);5. structuredClone(现代浏览器)
// 现代浏览器支持的原生方法
const original = {
name: 'Alice',
date: new Date(),
buffer: new ArrayBuffer(16),
nested: {
values: [1, 2, 3]
}
};
const copy = structuredClone(original);
copy.nested.values.push(4);
console.log(original.nested.values); // [1, 2, 3] - 不受影响性能比较
浅拷贝性能测试
const largeObj = {
arr: new Array(100000).fill(0).map((_, i) => ({ id: i, value: `item-${i}` }))
};
console.time('Object.assign');
const copy1 = Object.assign({}, largeObj);
console.timeEnd('Object.assign');
console.time('Spread operator');
const copy2 = { ...largeObj };
console.timeEnd('Spread operator');
// 通常扩展运算符略快于 Object.assign深拷贝性能测试
const deepObj = {
level1: {
level2: {
level3: {
data: new Array(10000).fill(0).map((_, i) => ({ id: i }))
}
}
}
};
console.time('JSON method');
const copy1 = JSON.parse(JSON.stringify(deepObj));
console.timeEnd('JSON method');
console.time('Custom deepClone');
const copy2 = deepClone(deepObj);
console.timeEnd('Custom deepClone');
console.time('Lodash cloneDeep');
const copy3 = _.cloneDeep(deepObj);
console.timeEnd('Lodash cloneDeep');
// 通常 JSON 方法最快,但有局限性
// Lodash 功能最全面,性能也不错实际应用场景
1. 状态管理
// React 中的状态更新
const [state, setState] = useState({
user: { name: 'Alice', age: 25 },
settings: { theme: 'dark' }
});
// 错误的方式 - 直接修改原对象
const handleUpdateUser = () => {
state.user.name = 'Bob'; // 这样不会触发重新渲染
setState(state);
};
// 正确的方式 - 浅拷贝
const handleUpdateUser = () => {
setState({
...state,
user: { ...state.user, name: 'Bob' }
});
};2. 表单数据处理
// 编辑表单时,避免直接修改原数据
const originalData = {
name: 'Alice',
profile: {
email: 'alice@example.com',
preferences: {
notifications: true
}
}
};
// 创建编辑副本
const editData = deepClone(originalData);
// 安全地修改编辑副本
editData.profile.preferences.notifications = false;
// 原数据保持不变
console.log(originalData.profile.preferences.notifications); // true3. 缓存和历史记录
class UndoRedoManager {
constructor() {
this.history = [];
this.currentIndex = -1;
}
saveState(state) {
// 深拷贝状态,避免后续修改影响历史记录
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(deepClone(state));
this.currentIndex++;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
return deepClone(this.history[this.currentIndex]);
}
return null;
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
return deepClone(this.history[this.currentIndex]);
}
return null;
}
}选择指南
何时使用浅拷贝
- 对象结构简单,没有嵌套的引用类型
- 性能要求高,数据量大
- 只需要复制第一层属性
// 适合浅拷贝的场景
const userInfo = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
isActive: true
};
const newUserInfo = { ...userInfo, name: 'Bob' };何时使用深拷贝
- 对象有多层嵌套
- 需要完全独立的副本
- 避免引用污染
// 需要深拷贝的场景
const complexData = {
user: {
profile: {
settings: {
notifications: {
email: true,
sms: false
}
}
}
}
};
const backup = deepClone(complexData);常见陷阱和注意事项
1. 循环引用
const obj = { name: 'test' };
obj.self = obj; // 创建循环引用
// JSON.stringify 会报错
// TypeError: Converting circular structure to JSON
// 需要处理循环引用的深拷贝函数
function deepCloneWithCircular(obj, visited = new WeakMap()) {
if (visited.has(obj)) {
return visited.get(obj);
}
if (obj && typeof obj === 'object') {
const cloned = Array.isArray(obj) ? [] : {};
visited.set(obj, cloned);
for (let key in obj) {
cloned[key] = deepCloneWithCircular(obj[key], visited);
}
return cloned;
}
return obj;
}2. 特殊对象类型
const specialObjects = {
func: () => 'function',
symbol: Symbol('test'),
date: new Date(),
regex: /pattern/g,
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3])
};
// 需要特殊处理这些类型
function handleSpecialTypes(obj) {
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) return new Map(obj);
if (obj instanceof Set) return new Set(obj);
// ... 处理其他特殊类型
}3. 性能考虑
// 大数据量时选择合适的方法
const largeArray = new Array(1000000).fill().map((_, i) => ({
id: i,
data: `item-${i}`
}));
// 如果只需要浅拷贝,不要使用深拷贝
const shallowCopy = [...largeArray]; // 快速
const deepCopy = deepClone(largeArray); // 慢,且可能不必要总结
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 第一层属性 | 独立复制 | 独立复制 |
| 嵌套对象 | 共享引用 | 独立复制 |
| 性能 | 快 | 慢 |
| 内存使用 | 少 | 多 |
| 适用场景 | 简单对象 | 复杂嵌套对象 |
选择建议:
- 简单对象 → 使用浅拷贝(
...或Object.assign) - 复杂嵌套对象 → 使用深拷贝
- 无特殊类型 → 可考虑
JSON.parse(JSON.stringify()) - 有特殊类型 → 使用 Lodash 或自定义深拷贝函数
- 现代环境 → 可使用
structuredClone
理解浅拷贝和深拷贝的区别,选择合适的拷贝方式,是 JavaScript 开发中的重要技能。这不仅能避免意外的数据修改,还能提高代码的可维护性和性能。
