答應我,最後一次了(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);
}
複製代碼

終於寫完了,不知道本文章有沒有把你講明白呢?

相關文章
相關標籤/搜索