谈到拷贝,大家都很了解,甚至有人会回答:深浅拷贝是主要技术点,就这一个知识点,恰好是面试提问的问题之一。
下面我们来聊一聊深浅拷贝。
深拷贝:复制一份数据到新地址,修改拷贝后的数据不会对原有的数据造成影响。
浅拷贝:对原始类型来讲,拷贝是的数值;对引用类型来讲,拷贝的是引用地址,虽然拷贝后两份数据在内存中占据不同位置,但是两份数据的属性都指向同一地址。浅拷贝只会复制对象的第一层属性,如果属性内存在嵌套关系,也会对其引用地址复制,而不是值得复制。对此,修改嵌套对象的值,原数据也会收到影响。
深拷贝 | 浅拷贝 | |
修改拷贝后数据对源数据影响 | 无 | 有 |
是否是拷贝地址 | 创建新地址 | 是 |
深拷贝方法
1)第三方库 - loadsh
注:第三库的引入可能会对性能造成影响
2)递归
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
let clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
const a = { a: 1};
const b = deepClone(a);
3)JSON.parse(JSON.stringify)
注:对函数、Date、正则,undefined等慎用。可能存在丢失信息。简单的对象和数组拷贝是个不错的选择
const a = {a : 1}
const b = JSON.parse(JSON.stringify(a));
4)jQ的extend方法
const a = {a: 1};
const b = $.extend(true, {}, a);
5)structuredClone
注:2021引入的新概念,属于web API。函数克隆慎用,会抛出错误。
const a = { a: 1 };
const b = structuredClone(a);
6)proxy的set和get
注:使用proxy的set和get陷阱,追踪和复制原数据的属性变化。Date和正则慎用,他们不会被视为引用类型。由此可见,只有属性变化才会被复制。
function deepClone(obj) {
// 定义一个空对象或数组作为克隆的目标
const cloneTarget = Array.isArray(obj) ? [] : {};
// 使用Proxy来拦截目标对象上的操作
return new Proxy(cloneTarget, {
get(target, key, receiver) {
// 返回属性值
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 在设置属性值时,如果value是个对象,则递归克隆
if (typeof value === 'object' && value !== null && !(value instanceof Date) && !(value instanceof RegExp)) {
value = deepCloneWithProxy(value);
}
// 设置属性值
Reflect.set(target, key, value, receiver);
return true; // 必须返回true以表示设置成功
}
});
}
// 示例使用
const a = { a: 1 };
const cloned = deepClone=(a);
7)类和构造器函数
注:创建类和使用构造器函数可以实现定制化的数据拷贝。
8)MessageChannel
注:MessageChannel属于webAPI,不算是拷贝方法,严格来说是一种通信机制,而我们可以变废为宝的利用它的消息通信机制来实现拷贝,在循环递归中使用效果更加。它在序列化和反序列化是会自动处理循环引用,从而实现拷贝。这个拷贝过程是异步的,想同步拷贝的慎用。
function deepCloneWithMessageChannel(obj) {
return new Promise((resolve) => {
// 创建一个新的MessageChannel
const { port1, port2 } = new MessageChannel();
// 当消息接收到时的处理函数
port1.onmessage = function(event) {
resolve(event.data); // 这里event.data就是深拷贝后的对象
};
// 向另一个端口发送对象,这会自动进行序列化和反序列化
port2.postMessage(obj);
});
}
// 使用示例
async function testDeepClone() {
const original = { a: 1, b: { c: 2 }, d: [3, 4] };
original.self = original; // 添加循环引用
const cloned = await deepCloneWithMessageChannel(original);
cloned.b.c = 5;
cloned.d.push(6);
console.log('Original:', original);
console.log('Cloned:', cloned);
// 检验深拷贝效果和循环引用处理
}
testDeepClone();
浅拷贝方法
1)object.assign
let a = { a: 1, b: { x: 10 } };
let b = Object.assign({}, a);
2)扩展运算符(...)
const a = { aa: 1 };
const b = {...a};
b.aa = 2;
console.log("a", a);
console.log("b", b);
3)循环遍历
const a = { a: 1 };
const b = {};
for(let key in a) {
if(a.hasOwnProperty(key)) {
b[key] = a[key];
}
}
4)jQ中的extend
const a = { a: 1 };
const b = $.extend({}, a);
5)赋值
const a = {a: 1};
const b = a;
6)数组类型的拷贝会多两种方法:cancat和slice。
无论是深拷贝还是浅拷贝,大家都要从实际角度触发,性能方面也要考虑。