上一篇文章 「前端面試題系列9」淺拷貝與深拷貝的含義、區別及實現 中提到了深拷貝的實現方法,從遞歸調用,到 JSON,再到終極方案 cloneForce。前端
不經讓我想到,lodash 中的 _.cloneDeep
方法。它是如何實現深拷貝的呢?今天,就讓咱們來具體地解讀一下 _.cloneDeep 的源碼實現。面試
源碼中的內容比較多,爲了能將知識點講明白,也爲了更好的閱讀體驗,將會分爲上下 2 篇進行解讀。今天主要會涉及位掩碼、對象判斷、數組和正則的深拷貝寫法。segmentfault
ok,如今就讓咱們深刻源碼,共同探索吧~數組
它的源碼內容不多,由於主要仍是靠 baseClone 去實現。微信
/** Used to compose bitmasks for cloning. */ const CLONE_DEEP_FLAG = 1 const CLONE_SYMBOLS_FLAG = 4 function cloneDeep(value) { return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG) }
剛看到前兩行的常量就懵了,它們的用意是什麼?而後,傳入 baseClone 的第二個參數,彷佛還將那兩個常量作了運算,其結果是什麼?這麼作的目的是什麼?ui
一番查找以後,終於明白這裏其實涉及到了 位掩碼
與 位運算
的概念。下面就來詳細講解一下。spa
回到第一行註釋:Used to compose bitmasks for cloning
。意思是,用於構成克隆方法的位掩碼。prototype
從註釋看,這裏的 CLONE_DEEP_FLAG
和 CLONE_SYMBOLS_FLAG
就是位掩碼了,而 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG
實際上是 位運算 中的 按位或
方法。code
這裏有個不常見的概念:位運算
。MDN 上對位運算的解釋是:它常常被用來建立、處理以及讀取標誌位序列——一種相似二進制的變量。雖然可使用變量代替標誌位序列,可是這樣能夠節省內存(1/32)。對象
不過實際開發中,位運算用得不多,主要是由於位運算操做的是二進制位,對開發者來講不太好理解。用得少,就容易生疏。但實際上,位運算是一種很棒的思想,它計算得更快,代碼量還更少。位運算,經常使用於處理同時存在多個布爾選項的情形。掩碼中的每一個選項的值都是 2 的冪,位運算是 32 位的。
在計算機程序的世界裏,全部的數據都是以二進制的形式儲存的。位運算,說白了就是直接對某個數據在內存中的二進制位,進行運算操做。好比 &
、|
、~
、^
、>>
,這些都是 按位運算符,它們有一些神奇的用法。以系統權限爲例:
const PERMISSION_A = 1; // 0001 const PERMISSION_B = 2; // 0010 const PERMISSION_C = 4; // 0100 const PERMISSION_D = 8; // 1000 // 當一個用戶同時擁有 權限A 和 權限C 時,就產生了一個新的權限 const mask = PERMISSION_A | PERMISSION_C; // 0101,十進制爲 5 // 判斷該用戶是否有 權限C,能夠取出 權限C 的位掩碼 if (mask & PERMISSION_C) { ... } // 該用戶沒有 權限A,也沒有 權限C const mask2 = ~(PERMISSION_A | PERMISSION_C); // ~0101 => 1010 // 取出 與權限A 不一樣的部分 const mask3 = mask ^ PERMISSION_A; // 0101 ^ 0001 => 0100
回到源碼的 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG
就獲得一個新的結果傳入 baseClone 中,十進制爲 5,至於它是用來幹什麼的,就須要繼續深刻到 baseClone 的源碼中去看了。
先貼一下源碼,其中一些關鍵的判斷已經作了註釋
function baseClone(value, bitmask, customizer, key, object, stack) { let result // 根據位掩碼,切分判斷入口 const isDeep = bitmask & CLONE_DEEP_FLAG const isFlat = bitmask & CLONE_FLAT_FLAG const isFull = bitmask & CLONE_SYMBOLS_FLAG // 自定義 clone 方法,用於 _.cloneWith if (customizer) { result = object ? customizer(value, key, object, stack) : customizer(value) } if (result !== undefined) { return result } // 過濾出原始類型,直接返回 if (!isObject(value)) { return value } const isArr = Array.isArray(value) const tag = getTag(value) if (isArr) { // 處理數組 result = initCloneArray(value) if (!isDeep) { // 淺拷貝數組 return copyArray(value, result) } } else { // 處理對象 const isFunc = typeof value == 'function' if (isBuffer(value)) { return cloneBuffer(value, isDeep) } if (tag == objectTag || tag == argsTag || (isFunc && !object)) { result = (isFlat || isFunc) ? {} : initCloneObject(value) if (!isDeep) { return isFlat ? copySymbolsIn(value, copyObject(value, keysIn(value), result)) : copySymbols(value, Object.assign(result, value)) } } else { if (isFunc || !cloneableTags[tag]) { return object ? value : {} } result = initCloneByTag(value, tag, isDeep) } } // 用 「棧」 處理循環引用 stack || (stack = new Stack) const stacked = stack.get(value) if (stacked) { return stacked } stack.set(value, result) // 處理 Map if (tag == mapTag) { value.forEach((subValue, key) => { result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) return result } // 處理 Set if (tag == setTag) { value.forEach((subValue) => { result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack)) }) return result } // 處理 typedArray if (isTypedArray(value)) { return result } const keysFunc = isFull ? (isFlat ? getAllKeysIn : getAllKeys) : (isFlat ? keysIn : keys) const props = isArr ? undefined : keysFunc(value) // 遍歷賦值 arrayEach(props || value, (subValue, key) => { if (props) { key = subValue subValue = value[key] } // Recursively populate clone (susceptible to call stack limits). assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) return result }
/** Used to compose bitmasks for cloning. */ const CLONE_DEEP_FLAG = 1 // 深拷貝標誌位 const CLONE_FLAT_FLAG = 2 // 原型鏈標誌位 const CLONE_SYMBOLS_FLAG = 4 // Symbol 標誌位 function baseClone(value, bitmask, customizer, key, object, stack) { // 根據位掩碼,取出位掩碼,切分判斷入口,bitmask 的十進制爲 5 const isDeep = bitmask & CLONE_DEEP_FLAG // 5 & 1 => 1 => true const isFlat = bitmask & CLONE_FLAT_FLAG // 5 & 2 => 0 => false const isFull = bitmask & CLONE_SYMBOLS_FLAG // 5 & 4 => 4 => true ... }
每一個常量基本都加了註釋,以前傳入 baseClone 的 bitmask 爲十進制的 5,其目的就是爲了在 baseClone 中進行判斷入口的切分。
// 若是不是對象,則直接返回該值 if (!isObject(value)) { return value } // ./isObject.js function isObject(value) { const type = typeof value return value != null && (type == 'object' || type == 'function') }
這裏須要說的就是,是否爲對象的判斷。用的基本方法是 typeof
,可是由於 typeof null 的值也是 'object',因此最後的 return 須要對 null 作額外處理。
const isArr = Array.isArray(value) if (isArr) { result = initCloneArray(value) if (!isDeep) { return copyArray(value, result) } } else { ... // 非數組的處理 } // 用於檢測對象自身的屬性 const hasOwnProperty = Object.prototype.hasOwnProperty // 初始化須要克隆的數組 function initCloneArray(array) { const { length } = array const result = new array.constructor(length) // Add properties assigned by `RegExp#exec`. if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { result.index = array.index result.input = array.input } return result }
爲了避免干擾源數組的數據,這裏首先會用 initCloneArray 初始化一個全新的數組。
其中,new array.constructor(length)
至關於 new Array(length)
,只是換了種不常見的寫法,做用是同樣的。
接下來的這個判斷,讓我一頭霧水。
// Add properties assigned by `RegExp#exec`. if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { result.index = array.index result.input = array.input }
判斷條件首先肯定 length > 0,而後 array[0] 的類型是 string,最後 array 擁有 index 這個屬性。
看到判斷條件裏的兩條執行語句更懵了,須要賦值 index
和 input
,這又是爲何?/(ㄒoㄒ)/~~
回頭看到第一行註釋,有個關鍵點 RegExp#exec
。MDN 中給的解釋:exec() 方法在一個指定字符串中執行一個搜索匹配。返回一個結果數組或 null。文檔下方有個例子:
var re = /quick\s(brown).+?(jumps)/ig; var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog'); console.log(result); // 輸出的 result 是一個數組,有 3 個元素和 4 個屬性 // 0: "Quick Brown Fox Jumps" // 1: "Brown" // 2: "Jumps" // groups: undefined // index: 4 // input: "The Quick Brown Fox Jumps Over The Lazy Dog" // length: 3
哇哦~ 原來 index
和 input
在這裏。因此,源碼中的爲什麼要那樣賦值,就迎刃而解了。
再回到 baseClone 中來,若是不是深拷貝,那就只要作數組的第一層數據的賦值便可。
if (!isDeep) { return copyArray(value, result) } // ./copyArray.js function copyArray(source, array) { let index = -1 const length = source.length array || (array = new Array(length)) while (++index < length) { array[index] = source[index] } return array }
位掩碼技術,是一種很棒的思想,能夠寫出更爲簡潔的代碼,運行得也更快。對象的判斷,須要特別注意 null,它的 typeof 值 也是 object。正則的 exec() 方法會返回一個結果數組或 null,其中就會有 index 和 input 屬性。
閱讀源碼的過程比較痛苦,深感自身的不足。從不懂到查閱資料,再到寫出來,耗費了我大量的時間,不過寫做的過程也給了我不小的收穫。修行之路任重而道遠,給本身打打氣,繼續砥礪前行吧。
未完待續。。。
莉莉絲遊戲招 高級前端
啦!!!
你玩過《小冰冰傳奇([刀塔傳奇])》麼?你玩過《劍與家園》麼?還有本篇的封面,爲我司的新遊戲《AFK arena》,現已佔領各大海外應用市場(友情提示:要當心,這遊戲有毒嗷~
)。
有興趣的同窗,能夠 關注下面的公衆 號加我微信 詳聊哈~