JavaScript 浅复制与深复制
JavaScript 浅复制与深复制
1 赋值
在JavaScript中,基本数据类型存在栈中,对象作为引用数据类型存在堆中。
JavaScript赋值,对于栈中基本数据类型的赋值,操作的是栈,传递的是值;对于堆中对象的赋值,操作的是栈,传递的是引用。
以下主要讨论堆中的赋值。
如 obj = obj1; 那么obj和obj1指向同一块内存地址,修改obj或obj1的属性,都会引起内存地址存储内容的修改。
var obj = {a:{b:1}};
var obj1 = obj;
// obj1.a.b = 2;
obj.a.b = 2;
console.log(obj.a.b,obj1.a.b);//2 2
赋值的缺陷在于源对象和赋值对象指向相同的内存地址,联系过于紧密。一旦一方修改,另一方也会随之修改。
克隆很好解决了这个问题,克隆对象和被克隆对象,分别存在于两个不同的内存地址之中。
2 浅克隆
上面谈到,克隆和赋值的最大区别在于,赋值双方指向同一个内存地址,而克隆双方存储在不同的内存地址之中。
克隆又分为浅克隆和深克隆。浅克隆和深克隆主要的差异在于
- 不同点:对于深层属性,浅克隆存在与对源对象的引用关系,而深克隆与源对象之间完全没有引用关系。
- 相同点:相对于赋值来说,克隆和被克隆对象存储在不同内存地址,赋值双方存储在相同内存地址。
下面介绍浅克隆的常见方式。
2.1 for-in 浅克隆 只能克隆对象第一层的可枚举属性
广度遍历,不做赘述
2.2 cloneNode(false) 浅克隆
cloneNode的作用对象是DOM结点,当参数为false表示浅克隆,为true表示深克隆
cloneNode(false)浅克隆不能克隆子结点,只能进行一层克隆
这种方式存在的问题是:
- 针对结点,无法操作非节点对象
- 不能克隆被addEventListener或onlcick这些用js绑定的事件
- 就算是cloneNode(true),也只能克隆DOM子树,不存在与祖先节点的链接关系。
- 以上两点参考MDN
//cloneNode浅克隆
var node = document.querySelector("#aaa");
node.obj = {
a: {
b: 2
}
}
var cNode = node.cloneNode(false);
document.body.appendChild(cNode);
console.log(node,cNode);
console.log(node.childNodes.length,cNode.childNodes.length);//3 0
2.3 Object.assign() 浅克隆
Object.assign()用于将所有可枚举属性的值从一个或多个源对象复制到目标对象
对于Object.assign()来说, 它拷贝的是属性值。假如源对象的属性值是另一个对象的引用,那么它也只指向那个引用。
let source = { 'name':'prop',obj:{'name':'objprop',innerObj:{num:1}} };
let clone = Object.assign({}, source);
source.name = 'prop1';
source.obj.name="objprop1";
source.obj.innerObj.num=99; //改变克隆目标对象的对象属性,克隆对象也随之改变,说明存在引用关系
console.log('srcObj', clone);
2.4 解构 浅克隆
解构是ES6中对赋值运算符的一种拓展,可以很方便地在复杂对象中提取数据字段.
解构可以克隆对象深层属性,然而需要注意的是,对于复杂类型,解构克隆的依然是引用。
var source = {
a: 1,
b: function () { },
c: {
d: {
num: 2,
}
}
}
var clone = { ...source};
source.c.d.num = 6;//改变source,clone也随之改变
console.log('clone', clone);
3 深克隆
3.1 JSON序列化实现深克隆及其缺陷 JSON.parse(JSON.stringify(obj))
序列化期间,存在将对象状态写入到临时或持久性存储区的过程,此后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
因此JSON.parse(JSON.stringify(obj))转换之后不存在引用关系。
var source = {
a: 1,
c: {
d: {
num: 2,
}
}
}
var clone = JSON.parse(JSON.stringify(source));
source.c.d.num = 6;
console.log('clone', clone);//clone.c.d.num依然为2,说明不存在引用关系,深层属性也完全被拷贝,看似不存在问题。
这种方法表面上实现了深复制,然而在改变测试用例时,结果却大相径庭。
可以发现函数与symbol被忽略,而正则对象变成了空对象{},日期对象也变成了字符串,没有被还原。
JSON.stringfiy()的问题:
遇到对象中undefined、symbol、函数会自动忽略,跳过他们进行序列化
遇到Date对象会转成字符串,正则对象变为空对象。
非数组对象的属性不能保证以特定顺序出现在序列化后的字符串中
var source = {
c: () => console.log('clone'),
d: /clone/,
e: Symbol(''),
f: new Date()
}
console.log(JSON.parse(JSON.stringify(source)));
对于循环引用,会抛出异常
//循环引用,父属性和子属性指向同一个对象
let obj1 = {};
var c = {a:1}
obj1.b = c;
obj1.b.c = c;
let obj2 = (JSON.parse(JSON.stringify(obj1)));//Converting circular structure to JSON
问题的根源在于JSON.stringify的机制。对此,可参考MDN对SON.stingify()序列化的限制做出的描述说明
3.2 关于实现深克隆的思考
最近逛论坛也看到过很多实现深克隆的方法,但有些方法存在一些问题。
既然是克隆,免不了对属性对象做广度和深度的遍历。
常见的广度遍历的方式有for-in,Object.keys(),Object.assign()等等,常见的深度遍历手法是递归。
然而不少案例在进行广度遍历时,欠缺对属性可枚举性enumerable的考虑,导致克隆方法存在显著缺陷。
举个栗子:
function deepClone1(obj) {
var objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === "object") {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepClone1(obj[key]);
} else {
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
以上方法使用for-in进行广度遍历,而for-in是无法对非枚举属性进行访问的。
对于正则对象的source属性和flag属性这些不可枚举的属性,采用for-in、Object.assign()、Object.keys()等广度遍历方式均无法访问,从而造成克隆的失败。
3.3 实现深克隆
此方式是对3.1、3.2所列举方式的改进,对一些特殊的对象如正则、日期、函数进行了具体处理。
思路:
/*
对象的深复制:
1 初始化返回的目标对象
如果没有指定返回目标对象,则利用源对象的构造函数创建目标对象,判断源对象类型,正则对象和日期对象分开复制
如果源对象是正则对象,抽取源对象属性source和flag放入构造参数中新建正则对象
如果源对象是日期对象,抽取源对象放入构造参数中新建日期对象
如果指定了目标对象,进入2
2 复制属性并返回结果
首先用getOwnProperty(此方法可获取对象的可枚举不不可枚举属性)获取源对象的一级属性名数组props[],遍历此数组获取属性名对应的属性描述对象desc。
使用typeof 把对象和函数分开处理。
如果是对象:
将源对象的属性描述对象设置到对应的目标对象属性中
考虑到对象存在深度关系,设置对象属性的操作用递归处理
如果是函数:
正则提取参数列表和函数体,使用构造创建函数的形式,将二者作为参数传进去
设置value为fn,其他三个描述属性参照描述属性对象
如果是字面量
直接将源属性对应的属性名和属性描述设置给目标属性
*/
实现:
function deepClone(source, clone) {
//是否存在源对象
if (!source) return {};
//是否指定返回对象
if (!clone) {
//正则和日期对象分别处理
switch (source.constructor) {
case RegExp:
clone = new RegExp(source.source, source.flag);
break;
case Date:
clone = new Date(source);
break;
default:
clone = new source.constructor();
break;
}
}
//获取对象一级属性名数组
let props = Object.getOwnPropertyNames(source);
for (let i = 0; i < props.length; i++) {
//获取属性描述对象
let desc = Object.getOwnPropertyDescriptor(source, props[i]);
if (typeof desc.value === "object") {
//递归
let o = deepClone(desc.value);
//设置目标对象属性
Object.defineProperty(clone, props[i], {
configurable: desc.configurable,
writable: desc.writable,
enumerable: desc.enumerable,
value: o
})
}
else if (typeof desc.value === "function") {
let fnStr = desc.value.toString().replace(/\n/g, "");
//非贪婪匹配,获取参数列表
let arg = fnStr.match(/\((.*?)\)/)[1];
//贪婪匹配,获取函数体
let content = fnStr.match(/{(.*)}/)[1];
let fn = new Function(arg, content);
//设置函数名
Object.defineProperty(fn, "name", {
writable: true,
value: desc.value.name
});
//设置函数属性
Object.defineProperty(clone, props[i], {
configurable: desc.configurable,
enumerable: desc.enumerable,
writable: desc.writable,
value: fn
});
}
else {
// 属性值是字面量
Object.defineProperty(clone, props[i], desc);
}
}
return clone;
}
测试用例
var obj = {
a: 1,
b: "a",
c: true,
d: function (a, b) {
if (a > 10) a = 10;
if (b > 5) {
b = 5;
} else if (b < 0) {
b = 0;
}
console.log(a + b);
},
e: [1, 2, 3, 4],
f: {
g: ["a", "b", "c"],
h: new Date(),
i: /^[a-z]+$/g,
j: {
k: {
},
l: [true, false],
m: [
{ id: 1001, name: "abc1" },
{ id: 1002, name: "abc2" },
{ id: 1003, name: "abc3" }
]
}
}
}
Object.defineProperties(obj.f.j, {
n: {
value: function () {
console.log("abcd");
}
},
o: {
value: 10,
enumerable: true
},
p: {
value: [1, 2, 3],
writable: true
},
q: {
value: true,
writable: true,
enumerable: true
}
});
Object.defineProperties(obj.f.j.k, {
r: {
value: function () {
},
writable: true
},
s: {
value: { a: 1 },
enumerable: true
}
});
var obj1 = deepClone(obj);
obj.f.j.p[2] = 10;//obj改变,obj1不改变,不存在引用关系
console.log(obj, obj1);
效果
至此,方法已经趋于成熟,能对一些常见的对象进行深拷贝,应付日常所需。但是对于一些复杂的情况,如拷贝Vue实例等,可能还存在问题。
这里推荐一个更强大的第三方库lodash作为参考 loadash cloneDeep。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端