手写深拷贝

作者: JaniceZD | 来源:发表于2020-07-29 10:35 被阅读0次

    1. 简述深拷贝

    JavaScript 中有2种变量类型:值类型(基本类型)和引用类型。
    深拷贝和浅拷贝都是针对引用类型。

    • 浅拷贝
      创建一个新的对象,把原有对象的属性值,其中包括了基本类型的值,和引用类型的内存地址,完整的拷贝过去。
      如果其中一个对象改变了引用类型的内存地址,就会影响到另一个对象。
    • 深拷贝
      将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
      简单理解: ba的一份拷贝,b中没有对 a 中对象的引用(b的任意一个对象的属性,都没有引用到 a),那就说明 ba 的 深拷贝。

    2. 写之前先考虑

    • 数据类型:基本类型还是引用类型
    • 数据规模:数据有多少属性
    • 性能要求:对时间、速度、空间是否有要求
    • 运行环境:在IE6 还是 Chrome 里运行
    • 其他 ...

    3. 使用JSON序列化 - 反序列化法

    var a = {
        b: 1,
        c: [1,2,3],
        d: { d1: "ddd1", d2: "ddd2"}
    }
    var a2 = JSON.parse(JSON.stringify(a))
    
    a2.b = 2
    console.log(a.b) //1
    
    a2.c[1] = 222
    console.log(a.c[1])  //2
    
    a2.d.d2 = "ccc"
    console.log(a.d.d2) //ddd2
    

    JSON.stringify()方法:将一个JS对象或数组转换为一个JSON字符串
    JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JS值或对象。

    但,此方案的缺点是:

    • 不支持拷贝函数(会忽略掉函数)
    var a = {
        f: function(){ },
        name: "a"
    }
    
    var a2 = JSON.parse(JSON.stringify(a))
    console.log(a2) // {name: "a"}
    
    • 不支持所有JSON不支持的类型,如 undefined
    var a = {
        u: undefined,
        name: "a"
    }
    
    var a2 = JSON.parse(JSON.stringify(a))
    console.log(a2) // {name: "a"}
    
    • 不支持引用
    var a = {
        name: "a"
    }
    a.self = a  //引用自身
    
    var a2 = JSON.parse(JSON.stringify(a))
    console.log(a2)  //报错
    
    • 不支持 Date
    var a = {
        time: new Date(),
        name: "a"
    }
    
    var a2 = JSON.parse(JSON.stringify(a))
    console.log(a2)  //输出了日期 new Date()的字符串形式(ISO8601时间字符串格式)
    
    • 不支持正则表达式
    var a = {
        name: "a",
        regex: /hi/
    }
    
    var a2 = JSON.parse(JSON.stringify(a))
    console.log(a2)  //{name: "a", regex: {}}   输出的是空对象
    

    如果数据类型包含了:Date,引用,函数,undefined等,如何深拷贝?

    4. 递归克隆

    思路:

    先判断节点类型

    • 基本类型:直接拷贝
    • 引用类型(object):分情况讨论
      ①普通 object : 用 for in ? 注意, for in 默认会遍历原型上的属性
      ②数组 array
      ③函数 function
      ④日期 Date

    4.1 基本类型的拷贝

    function deepClone(source) {
      return source
    }
    

    调用 deepClone传递参数时,进行了一份复制,所以 deepClone直接 return出来就行。

    4.2 引用类型的拷贝

    4.2.1 普通对象

    怎么判断是不是对象? → instanceof
    如果是对象,遍历对象的每一个属性,对每个属性进行克隆。

    function deepClone(source) {
      if (source instanceof Object) {
        let dist = new Object()
        for (let key in source) {
          dist[key] = deepClone(source[key])
        }
        return dist
      }
      return source
    }
    
    4.2.2 数组
    function deepClone(source){
        if(source instanceof Object){
            let dist
            if(source instanceof Array){
                dist = new Array()
            }else{
                dist = new Object()
            }
            for(let key in source){
                dist[key] = deepClone(source[key])
            }
            return dist
        }
        return source
    }
    
    4.2.3 函数

    难点:怎么拷贝函数的函数体和参数呢? → 直接调用

    else if (source instanceof Function){
          dist = function () {    //拷贝参数和函数体
               return source.apply(this, arguments)
          }
     }
    
    4.2.4 如果遇到环

    环:window对象里就有环
    window.self === window //true

    代码中用到了递归,如果遇到环,将无法结束递归,该如何解决?
    利用缓存标记,通过缓存检查环,如果第一次出现过,第二次再出现将不克隆。

    let cache = []  //初始化
    function deepClone(source){
        if(source instanceof Object){
            let cacheDist = findCache(source)
            if(cacheDist){    //有缓存
                return cacheDist
            }else{     //没缓存
                let dist
                //...(省略)
                cache.push([source, dist])
                for(let key in source){
                    dist[key] = deepClone(source[key])
                }
                return dist
            }
        }
        return source
    }
    //查找缓存
    function findCache(source) {
      for (let i = 0; i < cache.length; i++) {
        if (cache[i][0] === source) {    //对比 source 返回 dist
          return cache[i][1]
        }
      }
      return undefined
    }
    
    4.2.5 Date
    else if (source instanceof Date) {
         dist = new Date(source)
    }
    
    4.2.6 RegExp

    RegExp的重要属性:

    var a = /hi\d+/gi
    console.log(a.source)   //"hi\d+"
    console.log(a.flags)    //"gi"
    

    代码实现:

    else if (source instanceof RegExp) {
         dist = new RegExp(source.source, source.flags)
    }
    
    4.2.7 是否需拷贝原型上的属性

    一般来说,不拷贝原型上的属性,如果拷贝的话,内存占用太多了。
    注意:使用 for in对每个属性克隆时, for in默认会遍历原型上的属性,完善代码:

    for (let key in source) {
         if (source.hasOwnProperty(key)) {
            dist[key] = deepClone(source[key])
         }
     }
    

    hasOwnProperty()返回一个布尔值,可用于判断对象自身属性中是否有指定的键,会忽略原型上的属性。

    5. 完整代码

    let cache = [] //初始化
    
    function deepClone(source) {
      //先判断是否是Object类型
      if (source instanceof Object) {
        //判断source是否在缓存里
        let cacheDist = findCache(source)
        if (cacheDist) {   //有缓存
          return cacheDist
        } else {       //没缓存
          let dist
          if (source instanceof Array) {
            dist = new Array()
          } else if (source instanceof Function) {
            dist = function () {
              return source.apply(this, arguments)
            }
          } else if (source instanceof Date) {
            dist = new Date(source)
          } else if (source instanceof RegExp) {
            dist = new RegExp(source.source, source.flags)
          } else {
            dist = new Object()
          }
          cache.push([source, dist])
          //遍历每个属性克隆
          for (let key in source) {
            if (source.hasOwnProperty(key)) {
              dist[key] = deepClone(source[key])
            }
          }
          return dist
        }
      }
      return source
    }
    
    //查找缓存
    function findCache(source) {
      for (let i = 0; i < cache.length; i++) {
        if (cache[i][0] === source) {
          return cache[i][1]   //对比 source 返回 dist
        }
      }
      return undefined
    }
    

    以上代码还存在一个问题:cache 会被全局共享,造成互相污染。
    cache每次存入对象后,没有清空,下次deepClone时会和上一次的互相影响。
    改善方法:每次deepClone,只用一个cache,通过面向对象,使用 class

    相关文章

      网友评论

        本文标题:手写深拷贝

        本文链接:https://www.haomeiwen.com/subject/kkdfkktx.html