对象深拷贝与浅拷贝
对象深拷贝与浅拷贝
引用类型的特性
在JavaScript中,数据类型可以分为基本类型(如Number、String、Boolean等)和引用类型(如Object、Array、Function等)。基本类型的值直接存储在栈内存中,而引用类型的值存储在堆内存中,变量实际上只保存了对象在堆内存中的引用地址。
这种存储机制导致了一个重要特性:当我们将一个对象赋值给另一个变量时,实际上只是复制了引用地址,而非对象本身。
const original = { name: "张三", age: 25 };
const copy = original;
copy.name = "李四";
console.log(original.name); // 输出: "李四"
在上面的例子中,修改copy
对象的属性同时也改变了original
对象,这是因为它们指向同一个内存地址。这种现象在某些场景下可能导致意外的副作用,因此我们需要了解如何创建对象的真实副本。
浅拷贝(Shallow Copy)
浅拷贝创建一个新对象,其属性值是原始对象属性的精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是引用类型,拷贝的就是内存地址。
1. 使用Object.assign()
Object.assign()
方法可以将一个或多个源对象的所有可枚举属性复制到目标对象。
const original = { name: "张三", age: 25, address: { city: "北京" } };
const shallowCopy = Object.assign({}, original);
shallowCopy.name = "李四";
shallowCopy.address.city = "上海";
console.log(original.name); // 输出: "张三" (未受影响)
console.log(original.address.city); // 输出: "上海" (受影响)
2. 使用展开运算符(Spread Operator)
ES6引入的展开运算符提供了一种更简洁的浅拷贝方式:
const original = { name: "张三", age: 25, address: { city: "北京" } };
const shallowCopy = { ...original };
shallowCopy.name = "李四";
shallowCopy.address.city = "上海";
console.log(original.name); // 输出: "张三" (未受影响)
console.log(original.address.city); // 输出: "上海" (受影响)
3. 数组的浅拷贝方法
对于数组,除了使用展开运算符外,还可以使用以下方法:
// 使用Array.slice()
const originalArray = [1, 2, {a: 1}];
const sliceCopy = originalArray.slice();
// 使用Array.from()
const fromCopy = Array.from(originalArray);
// 使用concat()
const concatCopy = [].concat(originalArray);
// 修改测试
sliceCopy[2].a = 2;
console.log(originalArray[2].a); // 输出: 2 (受影响)
浅拷贝的局限性
浅拷贝的主要局限在于它只复制一层对象。当对象包含嵌套的引用类型属性时,这些嵌套属性仍然共享相同的引用。这意味着修改副本中的嵌套对象也会影响原始对象。
深拷贝(Deep Copy)
深拷贝会创建一个全新的对象,包括嵌套的所有层级,完全独立于原始对象。
1. 使用JSON方法
最简单的深拷贝方法是使用JSON.stringify()
和JSON.parse()
:
const original = {
name: "张三",
age: 25,
address: { city: "北京", district: "海淀" }
};
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.name = "李四";
deepCopy.address.city = "上海";
console.log(original.name); // 输出: "张三" (未受影响)
console.log(original.address.city); // 输出: "北京" (未受影响)
JSON方法的局限性:
虽然这种方法简单易用,但有几个重要的限制:
- 无法复制函数、RegExp、Date等特殊对象
- 无法处理循环引用
- 会丢失原型链
- 无法复制undefined和Symbol类型的属性
const original = {
func: function() { console.log('Hello'); },
date: new Date(),
reg: /test/,
undef: undefined,
sym: Symbol('test'),
nested: { inner: 'value' }
};
// 循环引用示例
original.self = original;
// 这将抛出错误: "TypeError: Converting circular structure to JSON"
const deepCopy = JSON.parse(JSON.stringify(original));
2. 递归实现深拷贝
为了解决JSON方法的局限性,我们可以实现一个递归的深拷贝函数:
function deepClone(obj, hash = new WeakMap()) {
// 处理null或非对象
if (obj === null || typeof obj !== 'object') return obj;
// 处理日期对象
if (obj instanceof Date) return new Date(obj);
// 处理正则对象
if (obj instanceof RegExp) return new RegExp(obj);
// 处理循环引用
if (hash.has(obj)) return hash.get(obj);
// 获取对象的所有属性描述符
const allDesc = Object.getOwnPropertyDescriptors(obj);
// 创建一个新对象,并保留原型链
const cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);
// 存储已复制的对象,用于处理循环引用
hash.set(obj, cloneObj);
// 递归复制所有可枚举的属性
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (typeof obj[key] === 'object' && obj[key] !== null)
? deepClone(obj[key], hash)
: obj[key];
}
return cloneObj;
}
// 使用示例
const original = {
name: "张三",
info: { age: 25, address: { city: "北京" } },
hobbies: ["读书", "旅行"],
greet: function() { return `你好,我是${this.name}`; },
date: new Date(),
pattern: /test/i,
[Symbol('id')]: 12345
};
// 创建循环引用
original.self = original;
const deepCopy = deepClone(original);
// 测试修改
deepCopy.name = "李四";
deepCopy.info.address.city = "上海";
deepCopy.hobbies.push("编程");
console.log(original.name); // 输出: "张三"
console.log(original.info.address.city); // 输出: "北京"
console.log(original.hobbies.length); // 输出: 2
console.log(deepCopy.greet()); // 输出: "你好,我是李四"
这个实现考虑了以下几点:
- 使用WeakMap处理循环引用
- 保留原型链
- 处理特殊对象类型如Date和RegExp
- 支持Symbol类型的键
- 复制不可枚举属性
3. 使用结构化克隆算法
现代浏览器提供了结构化克隆算法的实现,可以通过MessageChannel API来使用:
function structuredClone(obj) {
return new Promise(resolve => {
const channel = new MessageChannel();
channel.port1.onmessage = ev => resolve(ev.data);
channel.port2.postMessage(obj);
});
}
// 使用示例
async function test() {
const original = {
name: "张三",
nested: { value: 42 },
date: new Date()
};
const clone = await structuredClone(original);
clone.nested.value = 100;
console.log(original.nested.value); // 输出: 42
}
test();
注意:结构化克隆也有一些限制,如不能克隆函数、DOM节点等。
4. 使用第三方库
在实际项目中,通常推荐使用成熟的第三方库来处理深拷贝,如lodash的_.cloneDeep()
:
// 使用lodash
const _ = require('lodash');
const original = { name: "张三", nested: { value: 42 } };
const deepCopy = _.cloneDeep(original);
性能考量
深拷贝通常比浅拷贝消耗更多的资源,特别是对于大型嵌套对象。在选择拷贝方法时,应考虑以下因素:
对象大小和复杂度:对于简单对象,任何方法都可以;对于大型复杂对象,可能需要优化或分段处理。
拷贝频率:如果在性能关键的代码中频繁进行拷贝,应选择更高效的方法。
特殊对象需求:如果需要处理函数、循环引用等特殊情况,JSON方法可能不适用。
实际应用场景
1. 状态管理
在React或Vue等框架中,深拷贝常用于创建状态的不可变副本:
// React示例
this.setState(prevState => {
const newState = JSON.parse(JSON.stringify(prevState));
newState.user.preferences.theme = 'dark';
return newState;
});
2. 缓存对象
当需要缓存对象但不希望后续修改影响缓存时:
function cacheUserData(userData) {
// 深拷贝后再存储,防止外部修改影响缓存
const cachedData = deepClone(userData);
cache.set(userData.id, cachedData);
}
3. 保存历史状态
在撤销/重做功能中保存对象的历史状态:
function saveHistorySnapshot(document) {
history.push(deepClone(document));
}
总结
浅拷贝只复制对象的第一层属性,内部嵌套的引用类型仍然共享同一引用。适用于简单对象或明确知道只需要复制第一层的场景。
深拷贝创建完全独立的副本,包括所有嵌套层级。适用于需要完全隔离原始对象和副本的场景。
选择合适的方法取决于具体需求:
- 简单场景:使用Object.assign()或展开运算符进行浅拷贝
- 一般场景:使用JSON方法进行深拷贝
- 复杂场景:使用递归实现或第三方库
注意性能影响:深拷贝可能对大型对象造成性能问题,应谨慎使用。
通过合理选择和使用拷贝方法,可以有效避免引用类型带来的副作用,编写更可靠的JavaScript代码。