zoukankan      html  css  js  c++  java
  • 一文带你了解js数据储存及深复制(深拷贝)与浅复制(浅拷贝)

    背景

    在日常开发中,偶尔会遇到需要复制对象的情况,需要进行对象的复制。

    由于现在流行标题党,所以,一文带你了解js数据储存及深复制(深拷贝)与浅复制(浅拷贝)

    理解

    首先就需要理解 js 中的数据类型了
    js 数据类型包含

    1. 基础类型:StringNumbernullundefinedBoolean以及ES6引入的Symboles10中的BigInt
    2. 引用类型:Object

    由于 js 对变量的储存是栈内存堆内存完成的。

    • 基础类型将数据保存在栈内存
    • 引用类型将数据保存在堆内存

    由于 js 在数据读取和写入的时候,对基础类型是直接读写栈内存中的数据,引用类型是将一个内存地址保存在栈内存中,读写都是修改栈内存中指向堆内存的地址

    以如下代码为例

    let obj = {
      a:1,
      arr:[1,3,5,7,9],
      b:2,
      c:{
        num:100
      }
    }
    let num = 10
    

    在内存中的表现为
    内存中的展示

    我们声明个obj1

    let obj1 = obj;
    console.log(obj1 == obj);//true
    

    因为这个赋值,把内存变成了这样

    赋值后

    然后,内存中只是给js栈内存新增了一个指向堆内存的地址而已,这种就叫做浅复制。因为如图可以看到,如果我们修改obj.a的话,实际修改的是堆内存0x88888888中的变量a,由于obj1也指向这个地址,所以obj1.a也被修改了

    深复制是指,不单单复制引用地址,连堆内存都复制一遍,使objobj1不指向同一个地址。

    代码

    分开来看深复制浅复制

    浅复制

    由上述图可知,浅复制只是复制了堆内存的引用地址,通常在业务需求中出现的浅复制是指复制引用对象的第一层,也就是,基本类型复制新值,引用类型复制引用地址

    浅复制可以使用的方案有循环赋值扩展运算符object.assign(),

    let obj = {
      a:1,
      arr:[1,3,5,7,9],
      b:2,
      c:{
        num:100
      }
    }
    function clone1(obj){ // 使用循环赋值
      let b = {};
      for(let key in obj){
        b[key] = obj[key]
      }
      return b
    }
    function clone2(obj){ // 使用扩展运算符
      let b = {
        ...obj
      };
      return b
    }
    function clone3(obj){ // 使用object.assign()
      let b = {};
      Object.assign(b,obj)
      return b
    }
    let obj1 = clone1(obj);
    let obj2 = clone2(obj);
    let obj3 = clone3(obj);
    
    console.log(obj1 === obj); //false 代表复制成功了
    console.log(obj2 === obj); //false 代表复制成功了
    console.log(obj3 === obj); //false 代表复制成功了
    console.log('obj0.c.num修改前',obj.c.num); //100
    console.log('obj1.c.num修改前',obj1.c.num); //100
    console.log('obj2.c.num修改前',obj2.c.num); //100
    console.log('obj3.c.num修改前',obj3.c.num); //100
    obj0.c.num = 555;
    console.log('obj0.c.num修改后',obj.c.num); //555
    console.log('obj1.c.num修改后',obj1.c.num); //555
    console.log('obj2.c.num修改后',obj2.c.num); //555
    console.log('obj3.c.num修改后',obj3.c.num); //555
    

    由于是浅复制,所以引用类型只是复制了内存地址,修改其中一个对象的子属性后,引用这个地址的值都会被修改。

    浅克隆图解如下
    浅克隆图解

    深复制

    由于浅复制只是复制第一层,为了解决引用类型的复制,需要使用深复制来完成对象的复制,基本类型复制新值,引用类型开辟新的堆内存

    深复制可以使用的方案有JSON.parse(JSON.stringify(obj))循环赋值

    JSON.parse(JSON.stringify(obj))

    let obj = {
      a:1,
      arr:[1,3,5,7,9],
      c:{
        num:100
      },
      fn:function(){
         console.log(1)
      },
      date:new Date(),
      reg:/.*/g
    }
    function clone1(obj){ // 使用JSON.parse(JSON.stringify(obj))
      return JSON.parse(JSON.stringify(obj))
    }
    let obj1 = clone1(obj);
    console.log(obj === obj1); //false 代表复制成功了
    obj.c.num = 555;
    
    console.log(obj.c.num,obj1.c.num) // 555,100
    

    看起来是复制成功了!!~地址也变了,修改obj,obj1的引用地址不会跟着变化。

    但是我们来console一下obj以及obj1

    console.log(obj)
    console.log(obj1)
    

    打印结果

    似乎发现了离奇的事情,只有obj.a以及obj.c正确的复制了,日期类型方法正则表达式均没有复制成功,发生了一些奇怪的事情

    循环赋值 deepClone

    那么为了解决这种事情,就需要写一个deepClone方法来完成深复制了,参考了许多开源库的写法,将所有的复制项单独拆出,方便未来对特殊类型进行扩展,也防止不同功能间的变量互相干扰

     //既然是深复制,一定要传入一个object,再return 一个新的 Object
    function deepClone(obj){
        let newObj;
        if(obj instanceof Array){ // 数组的话,要new一个数组
          newObj = []
        }else if(obj instanceof Object){  // 对象的话,要new一个对象
          newObj = {}
        }
        if(obj === null) {
          return cloneNull(obj)
        }
        if(typeof obj=='function'){
            return cloneFunction(obj)
        }
        if(typeof obj!='object') {
            return cloneOther(obj)
        }
        if(obj instanceof RegExp) {
            return cloneRegExp(obj)
        }
        if(obj instanceof Date){
            return cloneDate(obj)
        }
        if(obj instanceof Array){
            for(let index in obj){
                newObj[index] = deepClone(obj[index]); // 对数组子项进行复制
            }
        }
        if(obj instanceof Object){
            for(let key in obj){
                newObj[key] = deepClone(obj[key]); // 对对象子项进行复制
            }
        }
        return newObj;
    }
    function cloneNull(obj){ // 复制NULL
      return obj
    }
    function cloneFunction(obj){ // 复制方法,
      // 复制一个新方法,将原方法转成字符串,并new一个新的function
      return new Function('return '+obj.toString())()
    }
    function cloneOther(obj){ // 复制非对象的数据
      return obj
    }
    function cloneRegExp(obj){ // 复制正则对象
      return new RegExp(obj)
    }
    function cloneDate(obj){ // 复制日期对象
      return new Date(obj)
    }
    

    这样一个基本上满足功能的深复制就完成了。先测试一下

    let obj = {
      a:1,
      arr:[1,3,5,7,9],
      c:{
        num:100
      },
      fn:function(){
         console.log(1)
      },
      date:new Date(),
      reg:/.*/g
    }
    
    let obj1 = deepClone(obj);
    console.log(obj.c === obj1.c); // false  代表复制成功
    console.log(obj.fn === obj1.fn);// false  代表复制成功 
    console.log(obj.date === obj1.date);// false  代表复制成功
    console.log(obj.reg === obj1.reg);// false  代表复制成功
    

    console一下

    console.log(obj)
    console.log(obj1)
    

    打印结果

    这样,就完成了deepClone深复制方法

    经过深复制后,图解如下
    深复制后内存图解

    优化 deepClone

    上述代码还有优化空间,参考了lodash库,在进行 new 对象时,可以使用 constructor构造函数 来进行创建新的实例,这样

    1. 可以不用判断递归中,是数组还是对象
    2. 如果深复制的某一项是某个原型的实例,深复制完成后,依然是该原型的实例
    function deepClone(obj){
        let newObj = new obj.constructor;
        if(obj === null) {
          return cloneNull(obj)
        }
        if(typeof obj=='function'){
            return cloneFunction(obj)
        }
        if(typeof obj!='object') {
            return cloneOther(obj)
        }
        if(obj instanceof RegExp) {
            return cloneRegExp(obj)
        }
        if(obj instanceof Date){
            return cloneDate(obj)
        }
        if(obj instanceof Array){
            for(let index in obj){
                newObj[index] = deepClone(obj[index]); // 对数组子项进行复制
            }
        }
        if(obj instanceof Object){
            for(let key in obj){
                newObj[key] = deepClone(obj[key]); // 对对象子项进行复制
            }
        }
        return newObj;
    }
    function cloneNull(obj){ // 复制NULL
      return obj
    }
    function cloneFunction(obj){ // 复制方法,
      // 复制一个新方法,将原方法转成字符串,并new一个新的function
      return new Function('return '+obj.toString())()
    }
    function cloneOther(obj){ // 复制非对象的数据
      return obj
    }
    function cloneRegExp(obj){ // 复制正则对象
      return new RegExp(obj)
    }
    function cloneDate(obj){ // 复制日期对象
      return new Date(obj)
    }
    

    最终版本 deepClone

    然后可以有一个合并版本的,比较节省代码,将下方区分开的复制方法,合并到deepClone中,可以极大地减少代码体积

    function deepClone(obj){ //
        let newObj = new obj.constructor;
        if(obj === null) return obj
        if(typeof obj=='function') return new Function('return '+obj.toString())()
        if(typeof obj!='object') return obj
        if(obj instanceof RegExp) return new RegExp(obj)
        if(obj instanceof Date) return new Date(obj)
        // 运行到这里,基本上只存在数组和对象两种类型了
        for(let index in obj){
            newObj[index] = deepClone(obj[index]); // 对子项进行递归复制
        }
        return newObj;
    }
    
  • 相关阅读:
    使用gulp搭建一个传统的多页面前端项目的开发环境
    抓包工具使用
    selectors 模块
    I/O模型
    协程
    进程池
    进程的同步
    进程间通讯的三种方式
    多进程调用
    生产者消费者模型
  • 原文地址:https://www.cnblogs.com/mr-xiao-han/p/13038127.html
Copyright © 2011-2022 走看看