本文介绍JavaScript中实现深度复制的两种主要方法。首先通过递归遍历属性解决浅复制导致的引用共享问题,其次利用ES5的JSON.stringify和JSON.parse进行转换。文章对比了两种方案的实现细节与性能差异,建议优先使用内置JSON方法以获得更高效率。
浅复制与深复制的概念辨析
在代码复用模式中,存在一种被称为“复制属性模式”(copying properties pattern)的技术。当讨论代码复用时,人们往往首先联想到继承性(inheritance),但需明确的是,复用的最终目标在于代码的再利用,而继承仅是实现这一目标的手段之一,并非唯一途径。复制属性模式与继承有着本质区别:它通过将一个对象的成员直接复制到另一个对象来实现复用。熟悉jQuery的开发者可能了解其提供的$.extend()方法,该方法不仅用于扩展第三方插件,还可用于执行属性复制操作。以下展示一个模拟extend()函数的实现代码(注:此处并非jQuery源码,仅为简易示例):
function extend(parent, child) {
var i;
//如果不传入第二参数child
//那么就创建一个新的对象
child = child || {};
//遍历parent对象的所有属性
//并且过滤原型上的属性
//然后将自身属性复制到child对象上
for(i in parent) {
if(parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
//返回目标对象child
return child;
}
上述代码提供了一个基础的实现逻辑,即遍历父对象的所有成员并将其复制到子对象中。接下来,我们将利用该extend()方法进行测试:
var dad = {name: "Adam"};
var kid = extend(dad);
console.log(kid.name); //Adam
测试结果表明,extend()方法能够正常运行。然而,这种方法存在一个显著缺陷:它仅执行所谓的浅复制(shallow clone)。在浅复制场景下,若子对象的某个属性本身是对象,修改该属性会同时影响父对象,这通常不符合预期需求。具体场景如下:
var dad = {
counts: [1, 2, 3],
reads: {paper: true}
};
var kid = extend(dad) //调用extend()方法将dad的属性复制到kid上面
kid.counts.push(4); //把4追加到kid.counts数组里面
console.log(dad.counts); //[1, 2, 3, 4]
通过上述示例可见,当修改子对象的kid.counts属性(追加元素4)后,父对象的dad.counts也随之改变。原因在于浅复制过程中,对象通过引用传递,即kid.counts与dad.counts指向内存中的同一数组(或同一堆地址)。
为解决此问题,我们需要修改extend()函数以支持深度复制。核心思路是检查父对象的每个属性,若属性为对象,则递归复制该对象的成员。此外,还需区分数组与对象,因为两者的字面量创建语法不同(数组为[],对象为{})。可通过Object.prototype.toString()方法检测数组,若返回"[object Array]"则判定为数组。以下是深度复制版本的extend()函数代码:
基于递归的深度复制实现
function extendDeep(parent, child) {
child = child || {};
for(var i in parent) {
if(parent.hasOwnProperty(i)) {
//检测当前属性是否为对象
if(typeof parent[i] === "object") {
//如果当前属性为对象,还要检测它是否为数组
//这是因为数组的字面量表示和对象的字面量表示不同
//前者是[],而后者是{}
child[i] = (Object.prototype.toString.call(parent[i]) === "[object Array]") ? [] : {};
//递归调用extend
extendDeep(parent[i], child[i]);
} else {
child[i] = parent[i];
}
}
}
return child;
}
深度复制函数编写完成后,我们需验证其是否按预期工作,即能否真正实现深度复制:
var dad = {
counts: [1, 2, 3],
reads: {paper: true}
};
var kid = extendDeep(dad);
//修改kid的counts属性和reads属性
kid.counts.push(4);
kid.reads.paper = false;
console.log(kid.counts); //[1, 2, 3, 4]
console.log(kid.reads.paper); //false
console.log(dad.counts); //[1, 2, 3]
console.log(dad.reads.paper); //true
测试结果显示,即使修改了子对象的kid.counts和kid.reads,父对象的dad.counts和dad.reads并未发生任何变化,这表明深度复制的目的已成功达成。
总结实现深度复制的基本思路如下:
1. 检测当前属性是否为对象类型。
2. 鉴于数组属于特殊对象,在确认属性为对象后,需进一步检测其是否为数组。
3. 若为数组,则创建一个[]空数组;否则创建{}空对象,并将其赋值给子对象的当前属性。随后,递归调用extendDeep函数继续处理。
上述示例展示了通过递归算法实现深度复制的方法。实际上,ES5新增的JSON对象提供的两个方法JSON.stringify()和JSON.parse()也可用于实现深度复制。前者将对象序列化为字符串,后者将字符串反序列化为对象。利用该方法实现的深度复制函数如下:
基于JSON方法的深度复制方案
function extendDeep(parent, child) {
var i,
proxy;
proxy = JSON.stringify(parent); //把parent对象转换成字符串
proxy = JSON.parse(proxy) //把字符串转换成对象,这是parent的一个副本
child = child || {};
for(i in proxy) {
if(proxy.hasOwnProperty(i)) {
child[i] = proxy[i];
}
}
proxy = null; //因为proxy是中间对象,可以将它回收掉
return child;
}
以下是该方法的测试示例:
var dad = {
counts: [1, 2, 3],
reads: {paper: true}
};
var kid = extendDeep(dad);
//修改kid的counts属性和reads属性
kid.counts.push(4);
kid.reads.paper = false;
console.log(kid.counts); //[1, 2, 3, 4]
console.log(kid.reads.paper); //false
console.log(dad.counts); //[1, 2, 3]
console.log(dad.reads.paper); //true
测试证实,该方法同样实现了深度复制。通常推荐采用此方法,因为JSON.parse和JSON.stringify为内置函数,处理效率较高。相比之下,前一种方法依赖递归调用,而递归算法在性能上通常较低。
关于JavaScript深度复制(deep clone)的实现方法介绍至此,希望以上内容能对大家有所帮助!