美文网首页前端在路上
Javascript深浅拷贝

Javascript深浅拷贝

作者: 夏末远歌 | 来源:发表于2020-03-30 01:30 被阅读0次

    拷贝

    复制与拷贝

    let user = {
      name: "John"
    };
    let user2=user; //变量名复制,只是持有了源对象的引用
    let userClone=clone(user);//对象克隆,新对象是是源对象的拷贝
    

    复制:将一个对象a赋值给另一个变量b,这个只是存储了对象a的引用地址,是属于同一个对象

    克隆:创建一份独立的对象拷贝,新对象具有源对象项的所有可枚举属性(值),两个对象之间相互独立

    浅拷贝

    思路:声明一个新对象,将源对象的可枚举属性(值)拷贝到新对象上

    实现方式

    1. for...in 复制所有属性值
    • 会拷贝对象自身以及其原型链上的可枚举属性
       let dest = {}; // 新的空对象
       // 复制所有的属性值
       for (let key in src) {
         dest[key] = src[key];
       }
    
    
    1. 采用jQuery使用extend,jQuery.extent(dest,src)以默认配置为优先,用户设置为覆盖
      赋值对象的可枚举属性
    • 会拷贝对象自身以及其原型链上的可枚举属性
    • 无法处理值为undefined的属性/值
    • 只拷贝对象中基本数据类型的属性,对于引用数据类型的数据会保持对象引用,
    1. Object.assign(dest,[ src1, src2, src3...]),将 src1, ..., srcN 这些所有的对象复制到 dest
    • 只拷贝对象中基本数据类型的属性,对于引用数据类型的数据会保持对象引用
    • 如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。
    • 只会拷贝源对象自身可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。
    • String类型和 Symbol 类型的属性都会被拷贝。
    • 在出现错误的情况下,例如,如果属性不可写,会引发TypeError,如果在引发错误之前添加了任何属性,则可以更改target对象。
    • 不会在那些src对象值为 null或 undefined 的时候抛出错误。
    • 原始类型会被包装为对象

    总结

    无法正常处理属性(值)为引用类型的数据,

    深拷贝

    思路:复制的时候应该检查 obj[key] 的每一个值,如果它是一个对象,那么把它也复制一遍

    实现方式

    1. jQuery.extend(true,dest,src),会递归处理对象的中引用数据类型属性(值)

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

    • 无法拷贝对象中Function类型的属性
    • 无法拷贝对象中值为undefined的属性
    • 无法拷贝具有循环引用的对象(可用来检测对象是否循环引用)
    1. 基于递归实现
    var deepClone=function(obj) {
      // 处理数组
      if(isArray(obj)){
        return obj.map(function(ele) {
          return isArray(ele)||isObject(ele)?deepClone(ele):ele
        })
      } else if(isObject(obj)){
        return reduce(obj,function(memo,value,key) {
          memo[key]=isArray(value)||isObject(value)?deepClone(value):value
          return memo
        },{})
      }else {
        return obj
      }
    }
    

    以上版本并未处理循环引用问题,以及特殊的引用数据类型(Set/Map/RegExp等)

    循环引用

    我们先来看个例子

    var man = {
        name: 'amsterdam',
        sex: 'male'
    };
    man['father'] = man;
    

    对象man的属性father又指向了man本身,形成了“环”,如果不能正常处理此类情况,将出现调用栈溢出。

    有一个标准的深拷贝算法,用于解决上面这种和一些更复杂的情况,叫做 结构化克隆算法(Structured cloning algorithm)。

    算法的优点是:

    • 可以复制 RegExp 对象。
    • 可以复制 Blob、File 以及 FileList 对象。
    • 可以复制 ImageData 对象。CanvasPixelArray 的克隆粒度将会跟原始对象相同,并且复制出来相同的像素数据。
    • 可以正确的复制有循环引用的对象

    依然存在的缺陷是:

    • Error 以及 Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。

    • 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERROR 异常。

    • 对象的某些特定参数也不会被保留

      • RegExp 对象的 lastIndex 字段不会被保留
      • 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
      • 原形链上的属性也不会被追踪以及复制。

    可参考lodash等库函数的实现

    手动实现深拷贝

    const deepCloneClourse = (target) => {
      let cached = new WeakMap()
    
      function baseClone (obj) {
        let objectType = getType(obj)
        let cloneObj
        // 检测对象是否已克隆 返回克隆后的对象
        let temp = cache(cached, obj)
        if (temp) {
          return temp
        }
        switch (objectType) {
          // Object
          case 'Object':
            //缓存已克隆对象
            cached.set(obj, cloneObj = {})
            //key-value 类型中Key可能是symbol
            Object.getOwnPropertySymbols(obj).forEach(item => {
              let symbol = Object(Symbol.prototype.valueOf.call(item))
              cloneObj[symbol] = baseClone(obj[item])
            })
            break
          // 容器类
          case 'Set':
            //缓存已克隆对象
            cached.set(obj, cloneObj = new Set())
            obj.forEach((val) => {
              cloneObj.add(baseClone(val, cached))
            })
            break
          case 'Map':
            //缓存已克隆对象
            cached.set(obj, cloneObj = new Map())
            obj.forEach((val, key) => {
              cloneObj.set(key, baseClone(val))
            })
            //key-value 类型中Key可能是symbol
            Object.getOwnPropertySymbols(obj).forEach(item => {
              let symbol = Object(Symbol.prototype.valueOf.call(item))
              cloneObj[symbol] = baseClone(obj[item])
            })
            break
          case 'Array':
            //缓存已克隆对象
            cached.set(obj, cloneObj = [])
            obj.forEach((val) => {
              cloneObj.push(baseClone(val))
            })
            break
          // 普通对象
          case 'RegExp':
            cloneObj = new RegExp(obj.source, obj.flags)
            break
          case 'Date':
            cloneObj = new Date(obj)
            break
          case 'Symbol':
            cloneObj = Object(Symbol.prototype.valueOf.call(obj))
            break
          case 'Boolean':
            cloneObj = Boolean(obj)
            break
          case 'Function':
            cloneObj = function () {
              return obj.apply(this, arguments)
            }
            break
          default://null undefined NaN string number boolean
            cloneObj = obj
        }
        if (typeof obj === 'object') {
          for (let item in obj) {
            if (obj.hasOwnProperty(item)) {
              cloneObj[item] = baseClone(obj[item])
            }
          }
        }
        return cloneObj
      }
    
      return baseClone(target)
    }
    
    

    总结

    • 在实际开发过程中,我们可以预估对象的基本结构,正确的使用深浅拷贝,避免在函数中因修改对象值照成数据异常的情形。
    • 大而全的东西,往往是最昂贵的。

    参考

    相关文章

      网友评论

        本文标题:Javascript深浅拷贝

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