【進階4-4期】Lodash是如何實現深拷貝的
在上一篇文章中介紹瞭如何實現一個深拷貝,分別說明了物件、陣列、迴圈引用、引用丟失、 Symbol
和遞迴爆棧等情況下的深拷貝實踐,今天我們來看看 Lodash
如何實現上述之外的函式、正則、Date、Buffer、Map、Set、原型鏈等情況下的深拷貝實踐。本篇文章原始碼基於 Lodash
4.17.11 版本。
更多內容請檢視 GitHub
整體流程
入口
入口檔案是 cloneDeep.js
,直接呼叫核心檔案 baseClone.js
的方法。
// 木易楊 const CLONE_DEEP_FLAG = 1 const CLONE_SYMBOLS_FLAG = 4 function cloneDeep(value) { return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG) } 複製程式碼
第一個引數是需要拷貝的物件,第二個是位掩碼(Bitwise),關於位掩碼的詳細介紹請看下面拓展部分。
baseClone 方法
然後我們進入 ./.internal/baseClone.js
路徑檢視具體方法,主要實現邏輯都在這個方法裡。
先介紹下該方法的引數 baseClone(value, bitmask, customizer, key, object, stack)
-
value:需要拷貝的物件
-
bitmask:位掩碼,其中 1 是深拷貝,2 拷貝原型鏈上的屬性,4 是拷貝 Symbols 屬性
-
customizer:定製的
clone
函式 -
key:傳入 value 值的 key
-
object:傳入 value 值的父物件
-
stack:Stack 棧,用來處理迴圈引用
我將分成以下幾部分進行講解,可以選擇自己感興趣的部分閱讀。
clone
baseClone 完整程式碼
這部分就是核心程式碼了,各功能分割如下,詳細功能實現部分將對各個功能詳細解讀。
// 木易楊 function baseClone(value, bitmask, customizer, key, object, stack) { let result // 標誌位 const isDeep = bitmask & CLONE_DEEP_FLAG// 深拷貝,true const isFlat = bitmask & CLONE_FLAT_FLAG// 拷貝原型鏈,false const isFull = bitmask & CLONE_SYMBOLS_FLAG// 拷貝 Symbol,true // 自定義 clone 函式 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 } // Symbol & 原型鏈 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] } assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) // 返回結果 return result } 複製程式碼
詳細功能實現
位掩碼
上面簡單介紹了位掩碼,引數定義如下。
// 木易楊 // 主線程式碼 const CLONE_DEEP_FLAG = 1// 1 即 0001,深拷貝標誌位 const CLONE_FLAT_FLAG = 2// 2 即 0010,拷貝原型鏈標誌位, const CLONE_SYMBOLS_FLAG = 4// 4 即 0100,拷貝 Symbols 標誌位 複製程式碼
位掩碼用於處理同時存在多個布林選項的情況,其中 掩碼中的每個選項的值都等於 2 的冪 。相比直接使用變數來說,優點是可以節省記憶體(1/32)(來自MDN)
// 木易楊 // 主線程式碼 // cloneDeep.js 新增標誌位,1 | 4 即 0001 | 0100 即 0101 即 5 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG // baseClone.js 取出標誌位 let result // 初始化返回結果,後續程式碼需要,和位掩碼無關 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 複製程式碼
常用的基本操作如下
a | b mask & a mask & ~a mask ^ a
// 木易楊 var FLAG_A = 1; // 0001 var FLAG_B = 4; // 0100 // 新增標誌位 a 和 b => a | b var mask = FLAG_A | FLAG_B => 0101 => 5 // 取出標誌位 a => mask & a mask & FLAG_A => 0001 => 1 mask & FLAG_B => 0100 => 4 // 清除標記位 a => mask & ~a mask & ~FLAG_A => 0100 => 4 // 取出與 a 的不同部分 => mask ^ a mask ^ FLAG_A => 0100 => 4 mask ^ FLAG_B => 0001 => 1 FLAG_A ^ FLAG_B => 0101 => 5 複製程式碼
定製 clone
函式
// 木易楊 // 主線程式碼 if (customizer) { result = object ? customizer(value, key, object, stack) : customizer(value) } if (result !== undefined) { return result } 複製程式碼
上面程式碼比較清晰,存在定製 clone
函式時,如果存在 value 值的父物件,就傳入 value、key、object、stack
這些值,不存在父物件直接傳入 value
執行定製函式。函式返回值 result
不為空則返回執行結果。
這部分是為了定製 clone
函式暴露出來的方法。
非物件
// 木易楊 // 主線程式碼 //判斷要拷貝的值是否是物件,非物件直接返回本來的值 if (!isObject(value)) { return value; } // ../isObject.js function isObject(value) { const type = typeof value; return value != null && (type == 'object' || type ='function'); } 複製程式碼
這裡的處理和我在【進階3-3】的處理一樣,有一點不同在於物件的判斷中加入了 function
,對於函式的拷貝詳見下面函式部分。
陣列 & 正則
// 木易楊 // 主線程式碼 const isArr = Array.isArray(value) const hasOwnProperty = Object.prototype.hasOwnProperty if (isArr) { // 陣列 result = initCloneArray(value) if (!isDeep) { return copyArray(value, result) } } else { ... // 非陣列,後面解析 } // 初始化一個數組 function initCloneArray(array) { const { length } = array // 構造相同長度的新陣列 const result = new array.constructor(length) // 正則 `RegExp#exec` 返回的陣列 if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { result.index = array.index result.input = array.input } return result } // ... 未完待續,最後部分有陣列遍歷賦值 複製程式碼
傳入的物件是陣列時,構造一個相同長度的陣列 new array.constructor(length)
,這裡相當於 new Array(length)
,因為 array.constructor === Array
。
// 木易楊 var a = []; a.constructor === Array; // true var a = new Array; a.constructor === Array // true 複製程式碼
如果存在正則 RegExp#exec
返回的陣列,拷貝屬性 index
和 input
。判斷邏輯是 1、陣列長度大於 0,2、陣列第一個元素是字串型別,3、陣列存在 index
屬性。
// 木易楊 if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { result.index = array.index result.input = array.input } 複製程式碼
其中正則表示式 regexObj.exec(str)
匹配成功時,返回一個數組,並更新正則表示式物件的屬性。返回的陣列將完全匹配成功的文字作為第一項,將正則括號裡匹配成功的作為陣列填充到後面。匹配失敗時返回 null
。
// 木易楊 var re = /quick\s(brown).+?(jumps)/ig; var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog'); console.log(result); // [ //0: "Quick Brown Fox Jumps" // 匹配的全部字串 //1: "Brown"// 括號中的分組捕獲 //2: "Jumps" //groups: undefined //index: 4// 匹配到的字元位於原始字串的基於0的索引值 //input: "The Quick Brown Fox Jumps Over The Lazy Dog" // 原始字串 //length: 3 // ] 複製程式碼
如果不是深拷貝,傳入 value
和 result
,直接返回淺拷貝後的陣列。這裡的淺拷貝方式就是迴圈然後複製。
// 木易楊 if (!isDeep) { return copyArray(value, result) } // 淺拷貝陣列 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 } 複製程式碼
物件 & 函式
// 木易楊 // 主線程式碼 const isArr = Array.isArray(value) const tag = getTag(value) if (isArr) { ... // 陣列情況,詳見上面解析 } else { // 函式 const isFunc = typeof value == 'function' // 如果是 Buffer 物件,拷貝並返回 if (isBuffer(value)) { return cloneBuffer(value, isDeep) } // Object 物件、類陣列、或者是函式但沒有父物件 if (tag == objectTag || tag == argsTag || (isFunc && !object)) { // 拷貝原型鏈或者 value 是函式時,返回 {},不然初始化物件 result = (isFlat || isFunc) ? {} : initCloneObject(value) if (!isDeep) { return isFlat ? copySymbolsIn(value, copyObject(value, keysIn(value), result)) : copySymbols(value, Object.assign(result, value)) } } else { // 在 cloneableTags 中,只有 error 和 weakmap 返回 false // 函式或者 error 或者 weakmap 時, if (isFunc || !cloneableTags[tag]) { // 存在父物件返回value,不然返回空物件 {} return object ? value : {} } // 初始化非常規型別 result = initCloneByTag(value, tag, isDeep) } } 複製程式碼
通過上面程式碼可以發現,函式、 error
和 weakmap
時返回空物件 {},並不會真正拷貝函式。
value
型別是 Object
物件和類陣列時,呼叫 initCloneObject
初始化物件,最終呼叫 Object.create
生成新物件。
// 木易楊 function initCloneObject(object) { // 建構函式並且自己不在自己的原型鏈上 return (typeof object.constructor == 'function' && !isPrototype(object)) ? Object.create(Object.getPrototypeOf(object)) : {} } // 本質上實現了一個instanceof,用來測試自己是否在自己的原型鏈上 function isPrototype(value) { const Ctor = value && value.constructor // 尋找對應原型 const proto = (typeof Ctor == 'function' && Ctor.prototype) || Object.prototype return value === proto } 複製程式碼
其中 Object
的建構函式是一個函式物件。
// 木易楊 var obj = new Object(); typeof obj.constructor; // 'function' var obj2 = {}; typeof obj2.constructor; // 'function' 複製程式碼
對於非常規型別物件,通過各自型別分別進行初始化。
// 木易楊 function initCloneByTag(object, tag, isDeep) { const Ctor = object.constructor switch (tag) { case arrayBufferTag: return cloneArrayBuffer(object) case boolTag: // 布林與時間型別 case dateTag: return new Ctor(+object) // + 轉換為數字 case dataViewTag: return cloneDataView(object, isDeep) case float32Tag: case float64Tag: case int8Tag: case int16Tag: case int32Tag: case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: return cloneTypedArray(object, isDeep) case mapTag: // Map 型別 return new Ctor case numberTag: // 數字和字串型別 case stringTag: return new Ctor(object) case regexpTag: // 正則 return cloneRegExp(object) case setTag: // Set 型別 return new Ctor case symbolTag: // Symbol 型別 return cloneSymbol(object) } } 複製程式碼
拷貝正則型別
// 木易楊 // \w 用於匹配字母,數字或下劃線字元,相當於[A-Za-z0-9_] const reFlags = /\w*$/ function cloneRegExp(regexp) { // 返回當前匹配的文字 const result = new regexp.constructor(regexp.source, reFlags.exec(regexp)) // 下一次匹配的起始索引 result.lastIndex = regexp.lastIndex return result } 複製程式碼
初始化 Symbol
型別
// 木易楊 const symbolValueOf = Symbol.prototype.valueOf function cloneSymbol(symbol) { return Object(symbolValueOf.call(symbol)) } 複製程式碼
迴圈引用
構造了一個棧用來解決迴圈引用的問題。
// 木易楊 // 主線程式碼 stack || (stack = new Stack) const stacked = stack.get(value) // 已存在 if (stacked) { return stacked } stack.set(value, result) 複製程式碼
如果當前需要拷貝的值已存在於棧中,說明有環,直接返回即可。棧中沒有該值時儲存到棧中,傳入 value
和 result
。這裡的 result
是一個物件引用,後續對 result
的修改也會反應到棧中。
Map & Set
value
值是 Map
型別時,遍歷 value
並遞迴其 subValue
,遍歷完成返回 result
結果。
// 木易楊 // 主線程式碼 if (tag == mapTag) { value.forEach((subValue, key) => { result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) return result } 複製程式碼
value
值是 Set
型別時,遍歷 value
並遞迴其 subValue
,遍歷完成返回 result
結果。
// 木易楊 // 主線程式碼 if (tag == setTag) { value.forEach((subValue) => { result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack)) }) return result } 複製程式碼
上面的區別在於新增元素的 API 不同,即 Map.set
和 Set.add
。
Symbol & 原型鏈
這裡我們介紹下 Symbol
和 原型鏈屬性的拷貝,通過標誌位 isFull
和 isFlat
來控制是否拷貝。
// 木易楊 // 主線程式碼 // 型別化陣列物件 if (isTypedArray(value)) { return result } const keysFunc = isFull // 拷貝 Symbol 標誌位 ? (isFlat // 拷貝原型鏈屬性標誌位 ? getAllKeysIn // 包含自身和原型鏈上可列舉屬性名以及 Symbol : getAllKeys)// 僅包含自身可列舉屬性名以及 Symbol : (isFlat ? keysIn // 包含自身和原型鏈上可列舉屬性名的陣列 : keys)// 僅包含自身可列舉屬性名的陣列 const props = isArr ? undefined : keysFunc(value) arrayEach(props || value, (subValue, key) => { if (props) { key = subValue subValue = value[key] } // 遞迴拷貝(易受呼叫堆疊限制) assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) return result 複製程式碼
我們先來看下怎麼獲取自身、原型鏈、Symbol 這幾種屬性名組成的陣列 keys
。
// 木易楊 // 建立一個包含自身和原型鏈上可列舉屬性名以及 Symbol 的陣列 // 使用 for...in 遍歷 function getAllKeysIn(object) { const result = keysIn(object) if (!Array.isArray(object)) { result.push(...getSymbolsIn(object)) } return result } // 建立一個僅包含自身可列舉屬性名以及 Symbol 的陣列 // 非 ArrayLike 陣列使用 Object.keys function getAllKeys(object) { const result = keys(object) if (!Array.isArray(object)) { result.push(...getSymbols(object)) } return result } 複製程式碼
上面通過 keysIn
和 keys
獲取常規可列舉屬性,通過 getSymbolsIn
和 getSymbols
獲取 Symbol
可列舉屬性。
// 木易楊 // 建立一個包含自身和原型鏈上可列舉屬性名的陣列 // 使用 for...in 遍歷 function keysIn(object) { const result = [] for (const key in object) { result.push(key) } return result } // 建立一個僅包含自身可列舉屬性名的陣列 // 非 ArrayLike 陣列使用 Object.keys function keys(object) { return isArrayLike(object) ? arrayLikeKeys(object) : Object.keys(Object(object)) } // 測試程式碼 function Foo() { this.a = 1 this.b = 2 } Foo.prototype.c = 3 keysIn(new Foo) // ['a', 'b', 'c'] (迭代順序無法保證) keys(new Foo) // ['a', 'b'] (迭代順序無法保證) 複製程式碼
常規屬性遍歷原型鏈用的是 for.. in
,那麼 Symbol
是如何遍歷原型鏈的呢,這裡通過迴圈以及使用 Object.getPrototypeOf
獲取原型鏈上的 Symbol
。
// 木易楊 // 建立一個包含自身和原型鏈上可列舉 Symbol 的陣列 // 通過迴圈和使用 Object.getPrototypeOf 獲取原型鏈上的 Symbol function getSymbolsIn (object) { const result = [] while (object) { // 迴圈 result.push(...getSymbols(object)) object = Object.getPrototypeOf(Object(object)) } return result } // 建立一個僅包含自身可列舉 Symbol 的陣列 // 通過 Object.getOwnPropertySymbols 獲取 Symbol 屬性 const nativeGetSymbols = Object.getOwnPropertySymbols const propertyIsEnumerable = Object.prototype.propertyIsEnumerable function getSymbols (object) { if (object == null) { // 判空 return [] } object = Object(object) return nativeGetSymbols(object) .filter((symbol) => propertyIsEnumerable.call(object, symbol)) } 複製程式碼
我們回到主線程式碼,獲取到 keys
組成的 props
陣列之後,遍歷並遞迴。
// 木易楊 // 主線程式碼 const props = isArr ? undefined : keysFunc(value) arrayEach(props || value, (subValue, key) => { // props 時替換 key 和 subValue,因為 props 裡面的 subValue 只是 value 的 key if (props) { key = subValue subValue = value[key] } // 遞迴拷貝(易受呼叫堆疊限制) assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)) }) // 返回結果,主線結束 return result 複製程式碼
我們看下 arrayEach
的實現,主要實現了一個遍歷,並在 iteratee
返回為 false 時退出。
// 木易楊 // 迭代陣列 // iteratee 是每次迭代呼叫的函式 function arrayEach(array, iteratee) { let index = -1 const length = array.length while (++index < length) { if (iteratee(array[index], index, array) === false) { break } } return array } 複製程式碼
我們看下 assignValue
的實現,在值不相等情況下,將 value 分配給 object[key]
。
// 木易楊 const hasOwnProperty = Object.prototype.hasOwnProperty // 如果現有值不相等,則將 value 分配給 object[key]。 function assignValue(object, key, value) { const objValue = object[key] // 不相等 if (! (hasOwnProperty.call(object, key) && eq(objValue, value)) ) { // 值可用 if (value !== 0 || (1 / value) == (1 / objValue)) { baseAssignValue(object, key, value) } // 值未定義而且鍵 key 不在物件中 } else if (value === undefined && !(key in object)) { baseAssignValue(object, key, value) } } // 賦值基本實現,其中沒有值檢查。 function baseAssignValue(object, key, value) { if (key == '__proto__') { Object.defineProperty(object, key, { 'configurable': true, 'enumerable': true, 'value': value, 'writable': true }) } else { object[key] = value } } // 比較兩個值是否相等 // (value !== value && other !== other) 是為了判斷 NaN function eq(value, other) { return value === other || (value !== value && other !== other) } 複製程式碼
參考
進階系列目錄
- 【進階1期】 呼叫堆疊
- 【進階2期】 作用域閉包
- 【進階3期】 this全面解析
- 【進階4期】 深淺拷貝原理
- 【進階5期】 原型Prototype
- 【進階6期】 高階函式
- 【進階7期】 事件機制
- 【進階8期】 Event Loop原理
- 【進階9期】 Promise原理
- 【進階10期】Async/Await原理
- 【進階11期】防抖/節流原理
- 【進階12期】模組化詳解
- 【進階13期】ES6重難點
- 【進階14期】計算機網路概述
- 【進階15期】瀏覽器渲染原理
- 【進階16期】webpack配置
- 【進階17期】webpack原理
- 【進階18期】前端監控
- 【進階19期】跨域和安全
- 【進階20期】效能優化
- 【進階21期】VirtualDom原理
- 【進階22期】Diff演算法
- 【進階23期】MVVM雙向繫結
- 【進階24期】Vuex原理
- 【進階25期】Redux原理
- 【進階26期】路由原理
- 【進階27期】VueRouter原始碼解析
- 【進階28期】ReactRouter原始碼解析
交流
進階系列文章彙總如下,內有優質前端資料,覺得不錯點個star。
我是木易楊,網易高階前端工程師,跟著我 每週重點攻克一個前端面試重難點 。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!
