本文深入解析JavaScript中深拷贝与浅拷贝的区别。通过基本类型与引用类型的内存机制,阐述Object.assign、slice、concat等浅拷贝方法的局限。重点介绍JSON.parse/stringify深拷贝的缺陷,并提供支持原型链、循环引用及正则日期对象的手写深拷贝函数实现,帮助开发者选择正确的数据复制策略。
预备知识:JS数据类型与复制机制
预备知识
1.1、JS数据类型
JavaScript的数据类型主要分为两类:基本数据类型,包括Boolean、String、Number、null和undefined;以及引用数据类型,涵盖Object、Array、Function、RegExp、Date等复杂类型。
1.2、数据类型的复制
基本数据类型的复制遵循按值传递的原则,即复制的是具体的数值本身。
var a = 1;
var b = a;
b = 2;
console.log(a); // 1
console.lob(b); // 2
引用数据类型的复制则是按引用传值,即复制的是指向内存中对象的地址指针。
var obj1 = {
a: 1;
b: 2;
};
var obj2 = obj1;
obj2.a=3;
console.log(obj1.a); //3
console.log(obj2.a); // 3
1.3、深拷贝与浅拷贝
深拷贝和浅拷贝的概念仅适用于引用数据类型。浅拷贝会逐个拷贝对象的成员,但仅复制内存地址,新旧对象的成员仍共享同一块内存空间;深拷贝则会创建一个完全独立的新对象,新对象与原对象不共享内存,修改新对象不会影响原对象。
两者的核心区别在于:浅拷贝仅复制对象的第一层属性,而深拷贝会对对象属性进行递归复制,确保所有层级的引用类型都被独立创建。
JS浅拷贝:原理与常见实现
二、JS浅拷贝
2.1、赋值与浅拷贝
当将一个对象赋值给新变量时,实际上是将栈内存中的地址赋给了新变量,而非堆内存中的数据。这意味着新旧变量指向同一个存储空间,对任一变量的修改都会反映在另一个变量上,导致两者联动变化。
var obj1 = {
'name' : 'zhangsan',
'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

浅拷贝是按位拷贝对象,它会创建一个新对象并依次拷贝原有对象的成员。若成员为基本类型,则拷贝其值;若为引用类型,则拷贝其内存地址。因此,若新对象中某个引用类型的成员地址发生变化(如重新赋值),不会影响原对象;但若修改该引用类型内部的属性,则会同时影响原对象。
//手写浅拷贝
function shallowCopy(obj1) {
let obj2 = Array.isArray(obj1) ? [] : {}
for (let i in obj1) {
obj2[i] = obj1[i]
}
return obj2
}
var obj1 = {
'name' : 'zhangsan',
'language' : [1,[2,3],[4,5]],
};
var obj2 = shallowCopy(obj1);
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

2.2、浅拷贝的实现
(1)Object.assign()
Object.assign()方法可将源对象自身的可枚举属性拷贝到目标对象,并返回目标对象。该方法执行的是浅拷贝,即拷贝属性的引用而非对象本身。此方法适用于Array和Object类型。
var obj1 = {
'name' : 'zhangsan',
'language' : [1,[2,3],[4,5]],
};
var obj2 = Object.assign({}, obj1);
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

(2)Array.prototype.concat()和Array.prototype.slice()
Array.prototype.concat()和Array.prototype.slice()均为Array原型上的方法,仅适用于数组类型。
var arr1 = [1,3,{
user: 'aaa'
}]
var arr2 = arr1.concat();
arr2[0] = '一';
arr2[2].user = 'AAA';
console.log('arr1',arr1)
console.log('arr2',arr2)
var arr1 = [1,3,{
user: 'aaa'
}]
var arr2 = arr1.slice();
arr2[0] = '一';
arr2[2].user = 'AAA';
console.log('arr1',arr1)
console.log('arr2',arr2)

补充说明:Array的slice和concat方法均不会修改原数组,而是返回一个经过浅拷贝的新数组。与Object.assign()类似,它们仅对第一层属性进行拷贝:若第一层属性为基本数据类型,则拷贝值;若为引用数据类型,则拷贝内存地址。
JS深拷贝:JSON方法与手写实现
三、JS深拷贝
深拷贝要求遍历对象中所有引用类型的值,直到遇到基本类型的值为止。
3.1、深拷贝实现方式
(1)JSON.parse(JSON.stringify())
原理:利用JSON.stringify()将对象转换为JSON字符串,再通过JSON.parse()将字符串解析回对象。
var obj1 = {
'name' : 'zhangsan',
'language' : [1,[2,3],[4,5]],
};
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)

缺点:该方法虽能实现数组、对象和基本数据类型的深拷贝,但无法处理函数,因为JSON.stringify()不接受函数。此外还存在以下局限性:
- 若对象中包含时间对象,拷贝后的时间将变为字符串形式,而非时间对象。
- 若对象中包含RegExp或Error对象,序列化结果将为空。
- 若对象中包含函数或undefined,序列化结果会丢失这些值。
- 若对象中包含NaN、Infinity或-Infinity,序列化结果将变为null。
- JSON.stringify()仅序列化对象的可枚举自有属性。若对象由构造函数生成,拷贝后会丢失constructor属性。
- 若对象中存在循环引用,该方法无法正确实现深拷贝。
(2)手写深拷贝函数
通过递归实现深拷贝。
function deepCopy(obj){
var result= Array.isArray(obj) ? [] : {}
if (obj && typeof(obj) === 'object') {
for (let i in obj) {
if (obj.hasOwnProperty(i)){ // 思考:这句是否有必要?
if (obj[i] && typeof(obj[i]) === 'object') {
result[i] = deepCopy(obj[i])
} else {
result[i] = obj[i]
}
}
}
}
return result
}
var obj1 = {
a: 1,
b: {
c: 2
}
};
var obj2 = deepCopy(obj1);
obj2.a = '一';
obj2.b.c = '二'
console.log('obj1', obj1)
console.log('obj2', obj2)
obj.hasOwnProperty(prop)用于判断对象obj是否包含属性prop,返回布尔值,存在则返回true,否则返回false。
上述简单实现存在缺陷:当遇到两个互相引用的对象时,会导致死循环从而引发栈溢出。为避免此问题,需在遍历时检测是否存在相互引用。
深拷贝函数改进(防止循环递归爆栈)
function deepCopy(obj, parent = null) {
let result = Array.isArray(obj) ? [] : {}
let _parent = parent
// 该字段有父级则需要追溯该字段的父级
while(_parent) {
// 如果该字段引用了它的父级,则为循环引用
if (_parent.originalParent === obj) {
// 循环引用返回同级的新对象
return _parent.currentParent
}
_parent = _parent.parent
}
if (obj && typeof(obj) === 'object') {
for (let i in obj) {
// 如果字段的值也是一个对象
if (obj[i] && typeof(obj[i]) === 'object') {
// 递归执行深拷,将同级的待拷贝对象传递给parent,方便追溯循环引用
result[i] = deepCopy(obj[i], {
originalParent: obj,
currentParent: result,
parent: parent
})
} else {
result[i] = obj[i]
}
}
}
return result
}
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1
var obj2 = deepCopy(obj1)
console.log('obj1', obj1)
console.log('obj2', obj2)
以上代码可直接复制到浏览器中测试运行。
深拷贝函数最终版(支持基本数据类型、Array、Object、原型链、RegExp、Date类型)
function deepCopy(obj, parent = null) {
let result
let _parent = parent
while(_parent) {
if (_parent.originalParent === obj) {
return _parent.currentParent
}
_parent = _parent.parent
}
if (obj && typeof(obj) === 'object') {
if (obj instanceof RegExp) {
result = new RegExp(obj.source, obj.flags)
} else if (obj instanceof Date) {
result = new Date(obj.getTime())
} else {
if (obj instanceof Array) {
result = []
} else {
let proto = Object.getPrototypeOf(obj)
result = Object.create(proto)
}
for (let i in obj) {
if(obj[i] && typeof(obj[i]) === 'object') {
result[i] = deepCopy(obj[i], {
originalParent: obj,
currentParent: result,
parent: parent
})
} else {
result[i] = obj[i]
}
}
}
} else {
return obj
}
return result
}
var obj1 = {
x: 1
}
//试调用
function construct(){
this.a = 1,
this.b = {
x:2,
y:3,
z:[4,5,[6]]
},
this.c = [7,8,[9,10]],
this.d = new Date(),
this.e = /abc/ig,
this.f = function(a,b){
return a+b
},
this.g = null,
this.h = undefined,
this.i = "hello",
this.j = Symbol("foo")
}
construct.prototype.str = "I'm prototype"
var obj1 = new construct()
obj1.k = obj1
obj2 = deepCopy(obj1)
obj2.b.x = 999
obj2.c[0] = 666
console.log('obj1', obj1)
console.log('obj2', obj2)
(3)函数库
也可使用第三方函数库,例如lodash库提供的_.cloneDeep方法用于执行深拷贝。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false
参考
https://www.jb51.net/article/181898.htm
https://www.jb51.net/article/140928.htm
以上就是详解JS深拷贝与浅拷贝的详细内容,更多关于JS深拷贝与浅拷贝的资料请关注本站其它相关文章!