最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 答应我,最后一次了(Javascript 深拷贝)

    正文概述 掘金(ShyWay)   2021-02-07   328

    最近在准备春招,遇到深拷贝的时候发现没有以前想的那么简单,网上很多帖子讲的也不是很清楚,所以写个文章,还不懂的人,希望能给你有些参考吧

    答应我,这是最后一次看深拷贝了!(以后都懂了)

    程序框架

    准备

    经过我一顿分析之后,总结了以下关键步骤

    • 梳理可拷贝类型
    • 定义各拷贝类型初始化方法

    嗯,就这两个,深拷贝实际上是对各类型初始化方法的考察

    你可能暂时有些不认可我的观点,但不妨继续看下去

    梳理可拷贝类型

    什么叫做梳理可拷贝类型呢?

    看一段代码(长但简单代码预警)

    const stringTag = "[object String]";
    const numberTag = "[object Number]";
    const booleanTag = "[object Boolean]";
    const arrayTag = "[object Array]";
    const argsTag = "[object Arguments]";
    const objectTag = "[object Object]";
    const dateTag = "[object Date]";
    const errorTag = "[object Error]";
    const setTag = "[object Set]";
    const mapTag = "[object Map]";
    const weakMapTag = "[object WeakMap]";
    const symbolTag = "[object Symbol]";
    const regexpTag = "[object RegExp]";
    
    const arrayBufferTag = "[object ArrayBuffer]";
    const dataViewTag = "[object DataView]";
    const int8Tag = "[object Int8Array]";
    const int16Tag = "[object Int16Array]";
    const int32Tag = "[object Int32Array]";
    const uint8Tag = "[object Uint8Array]";
    const uint8ClampedTag = "[object Uint8ClampedArray]";
    const float32Tag = "[object Float32Array]";
    const float64Tag = "[object Float64Array]";
    const uint16Tag = "[object Uint16Array]";
    const uint32Tag = "[object Uint32Array]";
    
    const cloneableTags = {};
    
    cloneableTags[stringTag] = 
    cloneableTags[numberTag] = 
    cloneableTags[booleanTag] = 
    cloneableTags[arrayTag] = 
    cloneableTags[argsTag] = 
    cloneableTags[objectTag] = 
    cloneableTags[dateTag] = 
    cloneableTags[setTag] = 
    cloneableTags[mapTag] = 
    cloneableTags[symbolTag] = 
    cloneableTags[regexpTag] = 
    cloneableTags[arrayBufferTag] = 
    cloneableTags[dataViewTag] = 
    cloneableTags[int8Tag] = 
    cloneableTags[int16Tag] = 
    cloneableTags[int32Tag] = 
    cloneableTags[uint8Tag] = 
    cloneableTags[uint8ClampedTag] = 
    cloneableTags[float32Tag] = 
    cloneableTags[float64Tag] = 
    cloneableTags[uint16Tag] = 
    cloneableTags[uint32Tag] = true;
    cloneableTags[weakMapTag] = cloneableTags[errorTag] = false;
    

    拷贝一个类型自然要提前知道是什么类型,自然而然就能想到使用Object.prototype.toString方法,这里则是预定义了可拷贝和不可拷贝的对象,如果你有自己实现的特殊对象,完全可以在这里加上标签,然后在后续自己定义初始化和拷贝方法

    首先这里肯定没问题,这是我们梳理的可拷贝类型和不可拷贝类型(所有不包括的也都是我们认定的不可拷贝类型)

    从上面的类型中,快速看一眼,你都把所有类型的初始化方法以及拷贝方法都写出来吗?

    写的出来,厉害

    写不出来,没事,看完就写的出来了

    我们把上面的可拷贝类型分为两种(我瞎说的)

    1. 不含引用值对象
    2. 含引用值对象

    什么意思呢?除了Map,Set,Array,common Object这种内部可能还有其他引用值(套娃)之外的都是不含引用值的(当然,它本身可能就是引用值,这里可能表达不严谨)

    所以对这种内部还含有引用值的,在深拷贝的过程中我们需要去递归它的成员

    而那些内部没有套娃的对象,我们只需要把它本身深拷贝一份就好了

    除此之外还有一个最基本的,那就是原始值类型我们可以直接赋值,就相当于深拷贝了

    判断非原始值类型的方法

    function isObject(value){
      const type = typeof value;
      return value != null && (type === 'function' || type === 'object');
    }
    

    所以我们的目前的预期是这样的

    function cloneDeep(value) {
      let result;
    
      if (!isObject(value)) return value;
      const tag = getTag(value);
      if(cloneableTags[tag]){
        initCloneByTag(value, tag);
      }
      if (tag == mapTag) {
        value.forEach((subValue, key) => {
          result.set(key, cloneDeep(subValue));
        });
        return result;
      }
      if (tag == setTag) {
        value.forEach((subValue) => {
          result.add(cloneDeep(subValue));
        });
        return result;
      }
      if (isTypedArray(value)) {
    		//......
      }
    	//......
    }
    

    上面这段代码是不是大致能明白我的意思呢?

    不明白也没关系,只需要知道我

    其实就是为了表达我们的值被分为了三种类型

    • 直接能复制(原始类型)
    • 需要递归(含有引用值)
    • 不需要递归(本身是引用值但是不含引用值)

    你能明白为什么要这样分就可以了

    接下来则是第二个重点,初始化可拷贝类型

    初始化可拷贝类型

    什么叫初始话可拷贝类型

    举个例子,你想拷贝下面这个对象

    let user = {
      name: "ShyWay",
      age: "18",
      male: "unKnown"
    }
    

    手动实现的大概过程是这样的

    let copyUser = {}
    copyUser.name = user.name;
    //......
    

    因为只有一层而且都是原始类型,所以可以直接赋值来拷贝,如果有更深层的话,我们就需要递归来拷贝

    其实就是下面这个样子

    function cloneDeep(value){
      let result;
      if(!isObject(value)) return value;
      
      const props = getKeys(value);
      arrayEach(props, (subValue, key) => {
        value[key] = cloneDeep(subValue)
      })
    }
    

    这里我们自己实现了得到对象内部索引方法以及类似于数组的forEach方法

    不过你可以暂时忽略这两点,知道什么意思就可以

    很显然我们之前的例子,在这个函数中,会被递归地调用一层,然后就会因为属性都是原始值而直接返回

    OK,这里没问题我们继续

    如果我们对象内部含有特殊的类型,比如Date,RegExp......会是什么样呢?

    想象一下,其实经过了递归调用之后,我们的子问题就是解决诸如深拷贝Date,RegExp...等特殊的类型

    如果拷贝的对象本身就是这种类型,那其实就是没有触发递归

    也就是在第一段实现cloneDeep中写的逻辑那样,只有内部可能含有引用值才会被递归调用

    这就是梳理可拷贝类型的作用

    如果看到这里不懂的话,可以停留一下多思考

    那么,我们就开始真正地初始化各拷贝类型

    但在此之前我们要把可拷贝类型再分一下类

    • 需要初始化
    • 不需要初始化

    这里其实和之前有一部分重复,什么意思呢?

    如果我们拷贝一个String对象,我们需要初始化吗?

    显然不需要,我们直接实现拷贝方法就好了

    但是像数组,对象,Map这类就需要初始化了

    这里就开始考验我们的核心能力了

    拿刚才的user举例

    一般的普通对象,我们可以用对象字面量初始化

    let copy = {}
    

    但是,如果是一些自己实现的有原型链关系的对象呢?如下

    function Foo(){}
    let foo = new Foo;
    

    这里我们如果再用对象字面量那么就可能发生错误,所以重头戏来了

    Object

    初始化Object

    这里我们需要额外实现一个判断对象是不是为有“特殊”原型链的对象的方法,这个判断方法是lodash中的,但我不确定是否严谨,不过对一般情况应该没问题,如果细究,篇幅过长,以后有机会再探讨

    function initCloneObject(object) {
      return typeof object === "function" && !isPrototype(object)
        ? Object.create(Object.getPrototypeOf(object))//继承原型链
        : {};
    }
    function isPrototype(value) {
      const Ctor = value && value.constructor;
      const proto =
        (typeof Ctor === "function" && Ctor.prototype) || Object.prototype;
      return value === proto;
    }
    
    Array

    这里额外考虑了由Regexp#exec方法产生的特殊数组(带有index和input属性)

    function initCloneArray(array) {
      let result = [];
      const { length } = array;
      if (
        length &&
        typeof array[0] === "string" &&
        Object.prototype.hasOwnProperty.call(array, "index")
      ) {
        result.index = array.index;
        result.input = array.input;
      }
      return result;
    }
    
    其他

    其他类型的初始化没有之前两个那么特殊

    所以我们可以用一个函数总结起来,同时可以把那些不需要初始化的对象直接拷贝(之后判断类型发现这类类型什么都不干等到最后返回就行)

    接下来的部分解释起来很费口舌,直接上代码,我尽量打全注释,如果还有疑问可以再交流

    这部分其实就是深拷贝考察的核心能力了

    function initCloneByTag(object, tag) {
      const Ctor = object.constructor;//获取对象构造函数
      switch (tag) {
        case arrayBufferTag:
          return cloneArrayBuffer(object);//可以直接深拷贝的对象
        case booleanTag:
        case dateTag:
          return new Ctor(+object);//可以直接深拷贝的对象,⚠️这个加号,可以思考一下为什么要+
        case dataViewTag:
          return cloneDataView(object);//可以直接深拷贝的对象
        case uint8Tag:
        case uint8ClampedTag:
        case uint16Tag:
        case uint32Tag:
        case int8Tag:
        case int16Tag:
        case int32Tag:
        case float32Tag:
        case float64Tag:
          return cloneTypedArray(object);//可以直接深拷贝的对象
        case mapTag:
          return new Ctor();//可以简单初始化的对象
        case numberTag:
        case stringTag:
          return new Ctor(object);//可以直接深拷贝的对象
        case symbolTag:
          return cloneSymbol(object);//可以直接深拷贝的对象
        case regexpTag:
          return cloneRegExp(object);//可以直接深拷贝的对象
        case setTag:
          return new Ctor();//可以简单初始化的对象
      }
    }
    

    具体的拷贝方法(当然,你可以忽略下面的代码自行编码)

    ArrayBuffer
    function cloneArrayBuffer(arrayBuffer) {
      const result = new arrayBuffer.constructor(arrayBuffer.byteLength);
      new Uint8Array(result).set(new Uint8Array(arrayBuffer));
      return result;
    }
    
    DataView
    function cloneDataView(dataView) {
      const buffer = cloneArrayBuffer(dataView.buffer);
      return new dataView.constructor(
        buffer,
        dataView.byteOffset,
        dataView.byteLength
      );
    }
    
    TypedArray
    function cloneTypedArray(typedArray) {
      const buffer = cloneArrayBuffer(typedArray.buffer);
      return new typedArray.constructor(
        buffer,
        typedArray.byteOffset,
        typedArray.length
      );
    }
    
    Symbol
    function cloneSymbol(symbol) {
      return Object(Symbol(Symbol.prototype.valueOf.call(symbol)));
    }
    
    RegExp
    function cloneRegExp(regexp) {
      const reFlag = /\w*$/;
      const result = new regexp.constructor(regexp.source, reFlag.exec(regexp));
      result.lastIndex = regexp.lastIndex;
      return result;
    }
    

    到这里我们最核心的部分已经完结,剩下的就是编码能力的考察,和细节的考察

    具体实现

    写到这我感觉篇幅已经很长了,也有点晚了,有些困意,所以接下来代码为主,不喜欢看代码的可以自己去动手实现

    但是在此之前还要考虑一个小问题,那就是循环引用

    循环引用

    其实这是老生常谈的话题,不知道的可以去搜一下,这里防止有人真忽略这个问题

    解决方法很简单只需要用一个Map保存引用,循环引用的时候打断递归就好

    function cloneDeep(value, map) {
      //...
      map || (map = new WeakMap());
      const maped = map.get(value);
      if (maped) return maped;
      map.set(value, result);
    	//...
    }
    

    实现

    这里结合代码更容易理解

    首先,我们会加一个特殊的形参object(父对象)它的作用其实是因为lodash的特殊实现:

    当遇到函数类型的时候

    如果父对象为空(也就是直接拷贝函数),返回空对象

    如果父对象不为空(函数是某个对象的属性),则直接引用(不进行深拷贝)

    当然你也可以把它去掉,进行自己的实现

    function cloneDeep(value, object, map) {
      let result;
    
      if (!isObject(value)) return value;//原始值类型
      
      const isArr = Array.isArray(value);
      const tag = getTag(value);//自己可以用Object#toString方法实现
      if (isArr) {
        result = initCloneArray(value);
      } else {
        const isFunc = typeof value === "function";
        //下面一段就是我在代码之前的那一段话的实现
        if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
          result = isFunc ? {} : initCloneObject(value);
        } else {
          if (isFunc || !cloneableTags[tag]) {
            return object ? value : {};
          }
          result = initCloneByTag(value, tag);//初始化部分类型以及一些特殊类型的直接复制(这里其实可以直接返回了)
        }
      }
      //循环引用
      map || (map = new WeakMap());
      const maped = map.get(value);
      if (maped) return maped;
      map.set(value, result);
      //需要递归复制的类型
      //对于那些已经复制好的特殊类型,这一段其实对了一些冗余的判断,可以自己写个函数再包装一下
      if (tag == mapTag) {
        value.forEach((subValue, key) => {
          result.set(key, cloneDeep(subValue, value, map));
        });
        return result;
      }
      if (tag == setTag) {
        value.forEach((subValue) => {
          result.add(cloneDeep(subValue, value, map));
        });
        return result;
      }
      //这里防止后面数组类型误判导致报错
      if (isTypedArray(value)) {
        return result;
      }
      //是对象则取内部索引
      const props = isArr ? undefined : getAllKeys(value);
      //自己实现的arrayEach,好处是更灵活
      arrayEach(props || value, (subValue, key) => {
        if (props) {
          key = subValue;
          subValue = value[key];
        }
        //特殊的赋值函数,下面再讲
        assignValue(result, key, cloneDeep(subValue, value, map));
      });
      return result;
    }
    

    到这里,这个写了快一个小时的文章终于要完结了

    你可能发现,上面的代码中有几个自己实现的函数,其实都是lodash为了考虑特殊情况写的

    一起来看看吧

    getAllKeys

    得到内部索引,坑在于Symbol类型的索引

    function getAllKeys(value) {
      const result = Object.keys(value);
      //这里可以去掉
      //lodash因为要复用所以才加了这个
      //在我们的实现中则自动过滤这种情况
      if (!Array.isArray(value)) {
        result.push(...getSymbols(value));
      }
      return result;
    }
    function getSymbols(value) {
      return Object.getOwnPropertySymbols(value).filter((key) =>
        Object.prototype.hasOwnProperty.call(value, key)
      );
    }
    

    assignValue

    这里的坑在于,__proto__属性不能直接赋值,需要特殊方法(因为setter的特殊性)

    function assignValue(object, key, value) {
      if (key === "__proto__") {
        Object.defineProperty(object, key, {
          configurable: true,
          writable: true,
          value: value,
          enumerable: true,
        });
      } else {
        object[key] = value;
      }
    }
    

    其他的函数

    比较简单,自行体会吧

    function isTypedArray(value) {
      const re = /^\[object (?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)Array\]$/;
      return isObjectLike(value) && re.test(value);
    }
    //这个函数有些冗余,但是为了复用所以多加了这个来判断
    function isObjectLike(value) {
      return value !== null && typeof value === "object";
    }
    function arrayEach(array, iteratee) {
      let index = -1;
      const { length } = array;
      while (++index < length) {
        if (iteratee(array[index], index) === false) break;
      }
      return array;
    }
    function getTag(object) {
      return Object.prototype.toString.call(object);
    }
    

    终于写完了,不知道本文章有没有把你讲明白呢?


    起源地下载网 » 答应我,最后一次了(Javascript 深拷贝)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元