深浅拷贝指南,探索高效实战策略。--JS基础篇(十)
写在前面
最近写了两篇类型判断的文章,内容不难,但是要搞懂还是有点绕的。一个月来连续分享了十几篇JS内容,估计基础篇还有几章就完结了。今天我们一起来聊聊深浅拷贝,这是引用类型数据的一个相关概念。
一、区分引用传递与拷贝
不知道刚开始学JS的时候,你有没有碰到这么一个问题?
const novalic1 = {
name:"novalic",
school:"ECUT"
}
const novalic2 = novalic1;
这里有两个同名的学生,都叫novalic
。但是,他们不在同一所学校,但是我还想偷个懒,就把novalic一号赋给novalic二号,再去修改novalic二号的学校属性,就得到了两个对象。
于是我继续写:
novalic2.school = "MIT"
console.log(novalic1);
console.log(novalic2);
我们来看一下结果:
意外地,novalic一号从ECUT一跃成为了"麻省理工的学生"
。这虽然是novalic一号想要的结果,但这显然不是我想要的结果。
为啥没有成功呢?如果是一个原始值,这样写没有任何问题。
let a = 88 ;
let b = a ;
a = 99;
console.log(b); //88
不对,=
意为赋值,说白了也就是传递
。对于原始值来说,是值的传递,但是对于对象(引用数据类型),传递的应该是......引用地址。
看来这样只是把同一个对象的地址分成相同的两份,存在不同的对象字面量中,这脱离了拷贝的定义。
没错,这连浅拷贝都算不上,只能算是对象引用地址的传递。
拷贝:
创建出一份独立的副本,改变副本的属性,原本对象的属性不受影响。
二、区分浅拷贝与深拷贝
对于原始值来说,最简单的=
赋值过后,每个变量都成了独一份
,所以,拷贝这一概念应当是在对象(引用类型) 身上讨论。
我们先来创建一个简单的对象:
const grandfather = {
name:"林二蛋",
age:78,
son:{
name:"林建国",
age:55,
son:{
name:"林朝夕",
age:25
}
}
}
林二蛋今年78岁,有一个55岁的儿子和一个25岁的孙子。对于grandfather这样一个对象来说,其内部有的属性是基本数据类型的值,有的属性是一个对象。
我们已经知道,对于原始值来说,=
的效果是值传递。而对于对象
来说,其内部的非对象属性或者其内部对象的非内部属性,也是一个原始值
。这样一来,就有了一个实现拷贝的基本思路:也就是创建出一个和原对象一样的对象,拥有它的属性并且拥有它的属性值。
注意:上面说的:创建出一个和原对象一样的对象,拥有它的属性并且拥有它的属性值。 这就是拷贝的实际目的,至于拷贝的深浅,就是你的拷贝程度了。
还是以上面的对象grandfather
为例子,除了顶层的name,age,son属性,其属���son内部还嵌套了name,age,son属性,里面那个son还嵌套了name,age属性。
-
浅拷贝:如果我们只遍历顶层属性拷贝,那么就是浅拷贝。
-
深拷贝:如果我们深度遍历对象内部的每个属性以及其子属性进行拷贝,那么这就是深拷贝。
接下来我们实现一下这两种拷贝的效果。
三、原生方法实现浅拷贝和深拷贝
3.1 原生浅拷贝实现
3.1.1 方法一:
对于浅拷贝,JS官方提供的方法是:Object.assign()
对于上述对象浅拷贝应该这样做:
const grandfather = {
name:"林二蛋",
age:78,
son:{
name:"林建国",
age:55,
son:{
name:"林朝夕",
age:25
}
}
}
let newObject = Object.assign({},grandfather);
console.log(newObject);
结果如图:
我们去验证一下:修改顶层name属性和son内部的name属性后对比原对象。
newObject.name = "王二虎";
newObject.son.name = "王建军";
console.log(grandfather)
结果如图:
浅拷贝后顶层的基本值属性独立了,没有被修改,但是顶层的引用类型属性被牵连了,浅拷贝就是这样的效果。
3.1.2 方法二:
ES6开始,官方提供了对象解构的方法,也就是将其顶层属性剥离出来。上面的浅拷贝我们也可以这样实现:
let newObject2 = {...grandfather};
大家可以试一下,效果也是一样的。
3.2 原生深拷贝实现
3.2.1 唯一方法:
- structuredClone()
这个方法是目前唯一一个能够完整实现深拷贝的方法,十分好用。
let newObject = structuredClone(grandfather);
newObject.name = "王二虎";
newObject.son.name = "王建军";
newObject.son.son.name = "王小明";
console.log(grandfather)
简单粗暴,帅到没朋友的一个方法。
**JSON.parse(JSON.stringfy(x))**在之前也会被用来做深拷贝,但是有以下缺点。
- 1.无法识别bigInt类型
- 2.无法拷贝undefined null function Symbol类型
- 3.无法处理循环引用
四、手写实现深浅拷贝
深浅拷贝这样一个概念,我们既然已经搞懂了原理,为何不手搓一个方法呢?😂
请允许我再啰嗦一遍原理:
-
浅拷贝:如果我们只遍历顶层属性拷贝,那么就是浅拷贝。
-
深拷贝:如果我们深度遍历对象内部的每个属性以及其子属性进行拷贝,那么这就是深拷贝。
4.1 浅拷贝自定义实现
思路:
- 创建一个空对象
- 遍历顶层属性
- 使用Object.hasOwnProperty规避对象隐式具有的属性(其原型上的属性)
- 传递属性
- 返回新对象
写法很简单,如下:
const shallowCopy = function(obj){
let newObj = {};
for(key in obj){
//如果是对象独立拥有的,非来自原型
if(obj.hasOwnProperty(key)){
//值传递,这里选择使用方括号而不是点表示法,因为key不是一个固定值,使用点表示法的结果是:key被当成了一个对象里的固有属性名
newObj[key] = obj[key]
}
}
return newObj;
}
写好了我们来试一试:
const newObject = shallowCopy(grandfather);
newObject.name = "王二虎";
newObject.son.name = "王建军";
console.log(grandfather)
依旧是顶层原始值不受影响,内部对象受影响。
4.2 深拷贝的自定义实现
��拷贝相对于浅拷贝来说,多了一步深度遍历的步骤。
思路:
- 创建一个空对象
- 深度遍历属性 如果是对象,则继续递归调用该方法
- 使用Object.hasOwnProperty规避对象隐式具有的属性(其原型上的属性)
- 传递属性
- 返回新对象
const deepCopy = function(obj){
let newObj = {};
for(key in obj){
//如果是对象独立拥有的,非来自原型
if(obj.hasOwnProperty(key)){
if(obj instanceof Object){
newObj[key] = deepCopy(obj[key]);
}else{
newObj[key] = obj[key];
}
}
}
return newObj;
}
在判断非原型上的属性后,继续判断是否为原始对象Object的实例,若为true,则说明还是一个对象,继续遍历内部属性,为false,说明是一个原始值,直接传递即可。
看看效果:
完美,当我们修改深拷贝得到的对象的属性时,原对象的属性丝毫不受影响。
实现了业务功能,我们再来思考一下是否有值得优化的地方。
还真有一个,深拷贝深度遍历时我们使用的是递归调用的方式,这样在对一个超级复杂的对象时,可能会出现爆栈的情况。
五、优化:
实战过程中,迭代法是一个更优的选择:
- 使用迭代法:
const deepCopy = function (obj) {
//创建一个队列放入原对象和新对象
const queue = [[obj,{}]];
let current,source,target;
//队列中有元素可以遍历时
while(queue.length > 0){
//当前对象
current = queue.shift();
//原对象
source = current[0];
//目标对象
target = current[1];
for(let key in source){
if(source.hasOwnProperty(key)){
if(source[key] instanceof Object){
target[key] = {}
//source[key]是一个对象则将其和一个空对象{}放入队列,继续循环遍历
queue.push([source[key],target[key]]);
}else{
target[key] = source[key];
}
}
}
}
return target;
}
这样就可以解决递归调用的爆栈问题了。当然除了以上方法,我们也可以使用第三方库提供的一些深拷贝方法,第三方库的一些深拷贝方法实现了高效安全的算法,也是不错的选择。
总结
以上就是深浅拷贝的内容了,本期我们讲了:
- 区分引用的传递与拷贝
- 区分浅拷贝与深拷贝
- 原生方法实现浅拷贝与深拷贝
- 浅拷贝
- Object.assgin(obj,{});
- {...obj};
- 深拷贝
- structuredClone(obj);
- 浅拷贝
- 手写实现深浅拷贝
- 优化
本期内容就到这里,如果你觉得对你有帮助的话,请给个小赞,这将是我持续创作的动力,感谢!下期见。
个人拙见,若有错误,敬请指正。
转载自:https://juejin.cn/post/7386957406500503603