本文详细解析JavaScript中深拷贝与浅拷贝的区别。通过指针指向、对象属性赋值及原型链继承等示例,阐明浅拷贝在引用类型下的局限性,并介绍递归实现深拷贝及JSON序列化两种常见方案,帮助开发者理解内存引用机制并选择合适的拷贝方式。
在编程中,拷贝(也常被称为克隆或复制)是一个常见操作,但根据实现方式的不同,主要分为深拷贝和浅拷贝两种类型。
这一概念在逻辑上并不复杂,一旦理清思路便很容易掌握。尽管初学者可能会觉得有些绕,但其理解难度远低于闭包等高级概念。
为了理解为何需要区分这两种拷贝,我们先来看一个具体的例子。
var person={
name:"张三",
age:22
}
var person1=person;
console.log(person);
console.log(person1);

表面上看,对象似乎被成功拷贝了。然而,如果后续修改了 person1 的属性值,你会发现 person 对象的属性值也随之发生了改变。
person1.name="李四";
console.log(person);
console.log(person1);

这很容易理解:上述操作只是让两个对象的指针指向了栈内存中的同一个位置。在前文讲解引用数据类型时,我们已经介绍过相关概念。
补充说明:
无论是使用 对象.属性 还是 对象[属性] 的语法,本质上都是在操作对象的属性值,这仅仅是两种不同的写法而已。
由此可见,单纯的指针赋值并不等同于真正的拷贝。真正的拷贝是指创建一个新对象,该对象拥有原对象的所有属性,但两者在内存中完全独立,互不影响。
基于这种理解,拷贝的本质实际上是将一个对象的属性循环赋值给另一个新对象的过程。
那么,为什么要区分浅拷贝和深拷贝呢?从本质上讲,它们之间有什么区别吗?
实际上,两者在核心机制上没有本质区别,最大的不同在于处理条件以及拷贝过程中涉及的属性类型不同。
按照惯例,我们先通过代码来观察。
浅拷贝
var person={
name:"张三",
age:22
}
var person1={};
for( key in person){
console.log(key);
person1[key]=person[key];
}
console.log(person);
console.log(person1);

person1.name="李四";
console.log(person);
console.log(person1);

从结果可以看出,修改基本数据类型属性时彼此互不影响。但这引出了一个新的问题:如果 person 对象的属性是引用类型(如数组或对象)会怎样?
var person={
name:"张三",
age:22,
son:{
firstSon:"张大毛"
}
}
var person1={};
for( key in person){
console.log(key);
person1[key]=person[key];
}
console.log(person);
console.log(person1);

现在,我们尝试修改 person1 的 son 属性。
person1.son={firstSon:"李大毛"};
console.log(person);
console.log(person1);

看起来似乎也没有相互影响?但需要注意的是,这里通过 person1.son = ... 这种赋值方式实际上是重写了 person1 的 son 属性,从而断开了与原对象 son 属性的引用关系,因为两者的内存地址已经不同。但如果进行如下修改呢?
person1.son.secondeSon="李大毛";
console.log(person);
console.log(person1);

结果出人意料,两者依然产生了相互影响。这时就需要引入深拷贝的概念,其核心在于处理那些可能为引用类型的属性值。
补充说明:
如果 person 的原型链上存在属性,person1 也能访问到这些属性。前文曾提到,此时可以使用 hasOwnProperty 来判断属性是否直接属于对象自身。
深拷贝
相信看到这里,大家已经对深拷贝和浅拷贝的区别有了大致的了解,关键在于是否考虑了属性值的类型。
// 上面补充了说了原型上的值也会被拷贝下来,为了方便直接用对象的原型链最后Object添加属性。
Object.prototype.address="你猜";
var person={
name:"张三",
age:22,
son:{
firstSon:"张大毛"
}
}
strtype=Object.prototype.toString;
var person1={};
// 为了方便这个地方用递归方法
function coleFun(origin,target){
// 防止目标对象本身有属性
target=target||{}
for( key in origin){
if(origin.hasOwnProperty(key))
if(strtype.call(origin[key])=="[object Object]"){
target[key]={};
target[key]=coleFun(origin[key],target[key])
}else{
target[key]=origin[key];
}
}
return target;
}
person1=coleFun(person,person1)
console.log(person);
console.log(person1);

查看结果似乎没有问题,现在尝试修改属性值进行测试。
person1.son.secondeSon="李大毛";
console.log(person);
console.log(person1);

此时观察一切正常。
所谓的深浅拷贝,归根结底就是判断属性值是否包含引用类型,并据此进行相应的拷贝操作。如果上述代码难以理解,建议重温引用数据类型与基本数据类型的区别、this 指向以及数据类型判断方法,这些内容在前面的文章中均有涉及。
补充
有读者在评论区提问:如果使用 JavaScript 中的 JSON 方法进行拷贝,属于深拷贝还是浅拷贝?
这很容易验证:直接拷贝一个包含引用数据类型的对象,然后判断修改后是否相互影响即可。
首先看一下其包含的两种方法及其作用:
| 方法 | 作用 |
|---|---|
JSON.parse() |
用于将一个JSON字符串转换为 JavaScript 对象。 |
JSON.stringify() |
用于将 JavaScript 值转换为JSON字符串。 |
接下来通过代码演示:
var person={
name:"张三",
age:22,
son:{
firstSon:"张大毛"
}
}
var str=JSON.stringify(person);
var person1=JSON.parse(str);
console.log(person);
console.log(person1);

从拷贝结果来看,至少表面上没有问题。
现在开始测试是否存在相互影响。
person1.son.secondeSon="李大毛";
console.log(person);
console.log(person1);

可见两者没有相互影响。通过 JSON 进行拷贝是 JavaScript 中一种常见且便捷的方式,其本质是将对象转换为 JSON 格式的字符串,再通过字符串生成新对象,因此这也是一种深拷贝实现方式。
总结
本文到此结束,希望能为您提供帮助,也欢迎您关注本站以获取更多相关内容!