答应我,最后一次了(Javascript 深拷贝)

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

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

本文代码参考lodash源码,能够应对大部分常见类型数组

本文对lodash源码进行了简化markdown

程序框架

准备

通过我一顿分析以后,总结了如下关键步骤框架

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

嗯,就这两个,深拷贝其实是对各种型初始化方法的考察函数

你可能暂时有些不承认个人观点,但不妨继续看下去ui

梳理可拷贝类型

什么叫作梳理可拷贝类型呢?编码

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

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方法,这里则是预约义了可拷贝和不可拷贝的对象,若是你有本身实现的特殊对象,彻底能够在这里加上标签,而后在后续本身定义初始化和拷贝方法prototype

首先这里确定没问题,这是咱们梳理的可拷贝类型和不可拷贝类型(全部不包括的也都是咱们认定的不可拷贝类型)

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

写的出来,厉害

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

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

  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.constructor === "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);
}
复制代码

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

相关文章
相关标签/搜索