理解JavaScript中的深拷贝和浅拷贝

本文详细解析了JavaScript中的深拷贝与浅拷贝概念,通过实例对比基本类型与引用类型的差别,并提供了几种实现深拷贝的方法,包括递归深拷贝及处理循环引用的情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在说深拷贝与浅拷贝前,我们先看两个简单的案例:

//案例1
var num1 = 1, num2 = num1;
console.log(num1) //1
console.log(num2) //1
num2 = 2; //修改num2
console.log(num1) //1
console.log(num2) //2
//案例2
var obj1 = {x: 1, y: 2}, obj2 = obj1;
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 2, y: 2}

console.log(obj2) //{x: 2, y: 2}

按照常规思维,obj1应该和num1一样,不会因为另外一个值的改变而改变,而这里的obj1 却随着obj2的改变而改变了。同样是变量,为什么表现不一样呢?这就要引入JS中基本类型和引用类型的概念了。

基本类型和引用类型

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。

目前基本类型有:

Boolean、Null、Undefined、Number、String、Symbol,引用类型有:Object、Array、Function。之所以说“目前”,因为Symbol就是ES6才出来的,之后也可能会有新的类型出来。

再回到前面的案例,案例1中的值为基本类型,案例2中的值为引用类型。案例2中的赋值就是典型的浅拷贝,并且深拷贝与浅拷贝的概念只存在于引用类型。

深拷贝与浅拷贝

既然已经知道了深拷贝与浅拷贝的来由,那么该如何实现深拷贝?我们先分别看看Array和Object自有方法是否支持:

Array


var arr1 = [1, 2], arr2 = arr1.slice();
console.log(arr1); //[1, 2]
console.log(arr2); //[1, 2]
arr2[0] = 3; //修改arr2
console.log(arr1); //[1, 2]
console.log(arr2); //[3, 2]

此时,arr2的修改并没有影响到arr1,看来深拷贝的实现并没有那么难嘛。我们把arr1改成二维数组再来看看:


var arr1 = [1, 2, [3, 4]], arr2 = arr1.slice();
console.log(arr1); //[1, 2, [3, 4]]
console.log(arr2); //[1, 2, [3, 4]]
arr2[2][1] = 5;
console.log(arr1); //[1, 2, [3, 5]]
console.log(arr2); //[1, 2, [3, 5]]

arr2又改变了arr1,看来slice()只能实现一维数组的深拷贝。

具备同等特性的还有:concat、Array.from() 。


Object


1、Object.assign()


var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
   x: 1,
   y: {
       m: 1
   }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}


经测试,Object.assign()也只能实现一维对象的深拷贝。

2、JSON.parse(JSON.stringify(obj))


var obj1 = {
   x: 1,
   y: {
       m: 1
   }
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 2, y: {m: 2}}


JSON.parse(JSON.stringify(obj)) 看起来很不错,不过MDN文档 的描述有句话写的很清楚:


undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。


我们再来把obj1改造下:


var obj1 = {
   x: 1,
   y: undefined,
   z: function add(z1, z2) {
       return z1 + z2
   },
   a: Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}


发现,在将obj1进行JSON.stringify()序列化的过程中,y、z、a都被忽略了,也就验证了MDN文档的描述。既然这样,那JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))简单粗暴,已经满足90%的使用场景了。

经过验证,我们发现JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题。只能祭出大杀器:递归


function deepCopy(obj) {
   // 创建一个新对象
   let result = {}
   let keys = Object.keys(obj),
       key = null,
       temp = null;
   for (let i = 0; i < keys.length; i++) {
       key = keys[i];    
       temp = obj[key];
       // 如果字段的值也是一个对象则递归操作
       if (temp && typeof temp === 'object') {
           result[key] = deepCopy(temp);
       } else {
       // 否则直接赋值给新对象
           result[key] = temp;
       }
   }
   return result;
}
var obj1 = {
   x: {
       m: 1
   },
   y: undefined,
   z: function add(z1, z2) {
       return z1 + z2
   },
   a: Symbol("foo")
};
var obj2 = deepCopy(obj1);
obj2.x.m = 2;
console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}


可以看到,递归完美的解决了前面遗留的所有问题,我们也可以用第三方库:jquery的$.extend和lodash的_.cloneDeep来解决深拷贝。上面虽然是用Object验证,但对于Array也同样适用,因为Array也是特殊的Object。

到这里,深拷贝问题基本可以告一段落了。但是,还有一个非常特殊的场景:


循环引用拷贝


var obj1 = {
   x: 1,
   y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);


此时如果调用刚才的deepCopy函数的话,会陷入一个循环的递归过程,从而导致爆栈。jquery的$.extend也没有解决。解决这个问题也非常简单,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,修改一下代码:


function deepCopy(obj, parent = null) {
   // 创建一个新对象
   let result = {};
   let keys = Object.keys(obj),
       key = null,
       temp= null,
       _parent = parent;
   // 该字段有父级则需要追溯该字段的父级
   while (_parent) {
       // 如果该字段引用了它的父级则为循环引用
       if (_parent.originalParent === obj) {
           // 循环引用直接返回同级的新对象
           return _parent.currentParent;
       }
       _parent = _parent.parent;
   }
   for (let i = 0; i < keys.length; i++) {
       key = keys[i];
       temp= obj[key];
       // 如果字段的值也是一个对象
       if (temp && typeof temp=== 'object') {
           // 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
           result[key] = DeepCopy(temp, {
               originalParent: obj,
               currentParent: result,
               parent: parent
           });
       } else {
           result[key] = temp;
       }
   }
   return result;
}
var obj1 = {
   x: 1,
   y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
console.log(obj1); //太长了去浏览器试一下吧~
console.log(obj2); //太长了去浏览器试一下吧~


至此,已完成一个支持循环引用的深拷贝函数。当然,也可以使用lodash的_.cloneDeep。




内容概要:本文详细探讨了基于MATLAB/SIMULINK的多载波无线通信系统仿真及性能分析,重点研究了以OFDM为代表的多载波技术。文章首先介绍了OFDM的基本原理系统组成,随后通过仿真平台分析了不同调制方式的抗干扰性能、信道估计算法对系统性能的影响以及同步技术的实现与分析。文中提供了详细的MATLAB代码实现,涵盖OFDM系统的基本仿真、信道估计算法比较、同步算法实现不同调制方式的性能比较。此外,还讨论了信道特征、OFDM关键技术、信道估计、同步技术系统级仿真架构,并提出了未来的改进方向,如深度学习增强、混合波形设计硬件加速方案。; 适合人群:具备无线通信基础知识,尤其是对OFDM技术有一定了解的研究人员技术人员;从事无线通信系统设计与开发的工程师;高校通信工程专业的高年级本科生研究生。; 使用场景及目标:①理解OFDM系统的工作原理及其在多径信道环境下的性能表现;②掌握MATLAB/SIMULINK在无线通信系统仿真中的应用;③评估不同调制方式、信道估计算法同步算法的优劣;④为实际OFDM系统的设计优化提供理论依据技术支持。; 其他说明:本文不仅提供了详细的理论分析,还附带了大量的MATLAB代码示例,便于读者动手实践。建议读者在学习过程中结合代码进行调试实验,以加深对OFDM技术的理解。此外,文中还涉及了一些最新的研究方向技术趋势,如AI增强毫米波通信,为读者提供了更广阔的视野。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值