likes
comments
collection
share

深浅拷贝指南,探索高效实战策略。--JS基础篇(十)

作者站长头像
站长
· 阅读数 27

写在前面

最近写了两篇类型判断的文章,内容不难,但是要搞懂还是有点绕的。一个月来连续分享了十几篇JS内容,估计基础篇还有几章就完结了。今天我们一起来聊聊深浅拷贝,这是引用类型数据的一个相关概念。

一、区分引用传递与拷贝

不知道刚开始学JS的时候,你有没有碰到这么一个问题?

const novalic1 = {
    name:"novalic",
    school:"ECUT"
}

const novalic2 = novalic1;

这里有两个同名的学生,都叫novalic。但是,他们不在同一所学校,但是我还想偷个懒,就把novalic一号赋给novalic二号,再去修改novalic二号的学校属性,就得到了两个对象。

于是我继续写:

novalic2.school = "MIT"
console.log(novalic1);
console.log(novalic2);

我们来看一下结果:

深浅拷贝指南,探索高效实战策略。--JS基础篇(十)

意外地,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);

结果如图:

深浅拷贝指南,探索高效实战策略。--JS基础篇(十)

我们去验证一下:修改顶层name属性和son内部的name属性后对比原对象。

newObject.name = "王二虎";
newObject.son.name = "王建军";
console.log(grandfather)

结果如图:

深浅拷贝指南,探索高效实战策略。--JS基础篇(十)

浅拷贝后顶层的基本值属性独立了,没有被修改,但是顶层的引用类型属性被牵连了,浅拷贝就是这样的效果。

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)

深浅拷贝指南,探索高效实战策略。--JS基础篇(十)

简单粗暴,帅到没朋友的一个方法。

**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)

深浅拷贝指南,探索高效实战策略。--JS基础篇(十)

依旧是顶层原始值不受影响,内部对象受影响。

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,说明是一个原始值,直接传递即可。

看看效果:

深浅拷贝指南,探索高效实战策略。--JS基础篇(十)

完美,当我们修改深拷贝得到的对象的属性时,原对象的属性丝毫不受影响。

实现了业务功能,我们再来思考一下是否有值得优化的地方。

还真有一个,深拷贝深度遍历时我们使用的是递归调用的方式,这样在对一个超级复杂的对象时,可能会出现爆栈的情况。

五、优化:

实战过程中,迭代法是一个更优的选择:

  • 使用迭代法:
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
评论
请登录