接着上一篇文章 lodash 是如何實現深拷貝的(上),今天會繼續解讀 _.cloneDeep 的源碼,來看看 lodash 是如何處理對象、函數、循環引用等的深拷貝問題的。前端
先回顧一下它的源碼,以及一些關鍵的註釋node
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
}
複製代碼
一些主要的判斷入口,已經加上了註釋。git
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)
}
// 若是 tag 是 '[object Object]'
// 或 tag 是 '[object Arguments]'
// 或 是函數但沒有父對象(object 由 baseClone 傳入,是 value 的父對象)
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
// 初始化 result
// 若是是原型鏈或函數時,設置爲空對象
// 不然,新開一個對象,並將源對象的鍵值對依次拷貝進去
result = (isFlat || isFunc) ? {} : initCloneObject(value)
if (!isDeep) {
// 進入對象的淺拷貝
return isFlat
// 若是是原型鏈,則須要拷貝自身,還有繼承的 symbols
? copySymbolsIn(value, copyObject(value, keysIn(value), result))
// 不然,只要拷貝自身的 symbols
: copySymbols(value, Object.assign(result, value))
}
} else {
// 是函數 或者 不是error類型 或者 不是weakmap類型時
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
// 按須要初始化 cloneableTags 對象中剩餘的類型
result = initCloneByTag(value, tag, isDeep)
}
}
複製代碼
其中,isBuffer
會處理 Buffer 類的拷貝,它是 Node.js 中的概念,用來建立一個專門存放二進制數據的緩存區,可讓 Node.js 處理二進制數據。github
在 baseClone 的外面,還定義了一個對象 cloneableTags,裏面只有 error 和 weakmap 類型會返回 false,因此 !cloneableTags[tag]
的意思就是,不是 error 或 weakmap 類型。面試
接下來,來看如何初始化一個新的 Object 對象。segmentfault
function initCloneObject(object) {
return (typeof object.constructor == 'function' && !isPrototype(object))
? Object.create(Object.getPrototypeOf(object))
: {}
}
// ./isPrototype.js
const objectProto = Object.prototype
// 用於檢測本身是否在本身的原型鏈上
function isPrototype(value) {
const Ctor = value && value.constructor
// 若是 value 是函數,則取出該函數的原型對象
// 不然,取出對象的原型對象
const proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto
return value === proto
}
複製代碼
其中,typeof object.constructor == 'function'
的判斷,是爲了肯定 value 的類型是對象或數組。數組
而後用 Object.create
生成新的對象。Object.create()
方法用於建立一個新對象,使用現有的對象來提供新建立的對象的 __proto__
。緩存
object.constructor
至關於 new Object()
,而 Object 的構造函數是一個函數對象。bash
const obj = new Object();
console.log(typeof obj.constructor);
// 'function'
複製代碼
對象的原型,能夠經過 Object.getPrototypeOf(obj)
獲取,它至關於過去使用的 __proto__
。函數
initCloneByTag
方法會處理剩餘的多種類型的拷貝,有原始類型,也有如 dateTag
、dataViewTag
、float32Tag
、int16Tag
、mapTag
、setTag
、regexpTag
等等。
其中,cloneTypedArray
方法用於拷貝類型數組。類型數組,是一種相似數組的對象,它由 ArrayBuffer、TypedArray、DataView 三類對象構成,經過這些對象爲 JavaScript 提供了訪問二進制數據的能力。
// 若是有 stack 做爲參數傳入,就用參數中的 stack
// 否則就 new 一個 Stack
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
複製代碼
與 「前端面試題系列9」淺拷貝與深拷貝的含義、區別及實現 最後提到的 cloneForce 方案相似,利用了棧來解決循環引用的問題。
若是 stacked
有值,則代表已經在棧中存在,否則就 value
和 result
入棧。在 Stack 中的 set 方法:
constructor(entries) {
const data = this.__data__ = new ListCache(entries)
this.size = data.size
}
set(key, value) {
let data = this.__data__
// data 是否在 ListCache 的構造函數中存在
if (data instanceof ListCache) {
const pairs = data.__data__
// LARGE_ARRAY_SIZE 爲 200
if (pairs.length < LARGE_ARRAY_SIZE - 1) {
pairs.push([key, value])
this.size = ++data.size
return this
}
// 超出200,則重置 data
data = this.__data__ = new MapCache(pairs)
}
// data 不在 ListCache 的構造函數中,則直接進行 set 操做
data.set(key, value)
this.size = data.size
return this
}
複製代碼
這兩個類型的深拷貝利用了遞歸的思想,只是添加元素的方式有區別,Map
用 set
,Set
用 add
。
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
複製代碼
// 獲取數組 keys
const keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys)
const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
// 若是 props 有值,則替換 key 和 subValue
if (props) {
key = subValue
subValue = value[key]
}
// 遞歸克隆(易受調用堆棧限制)
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
// ./getAllKeysIn
// 返回一個包含 自身 和 原型鏈上的屬性名 以及Symbol 的數組
function getAllKeysIn(object) {
const result = []
for (const key in object) {
result.push(key)
}
if (!Array.isArray(object)) {
result.push(...getSymbolsIn(object))
}
return result
}
// ./getAllKeys
// 返回一個包含 自身 和 Symbol 的數組
function getAllKeys(object) {
const result = keys(object)
if (!Array.isArray(object)) {
result.push(...getSymbols(object))
}
return result
}
// ./keysIn
// 返回一個 自身 和 原型鏈上的屬性名 的數組
function keysIn(object) {
const result = []
for (const key in object) {
result.push(key)
}
return result
}
// ./keys
// 返回一個 自身屬性名 的數組
function keys(object) {
return isArrayLike(object)
? arrayLikeKeys(object)
: Object.keys(Object(object))
}
複製代碼
最後來看下 assignValue
的實現。
// ./assignValue
const hasOwnProperty = Object.prototype.hasOwnProperty
function assignValue(object, key, value) {
const objValue = object[key]
if (!(hasOwnProperty.call(object, key) && eq(objValue, value))) {
// value 非零或者可用
if (value !== 0 || (1 / value) == (1 / objValue)) {
baseAssignValue(object, key, value)
}
// value 未定義,而且 object 中沒有 key
} else if (value === undefined && !(key in object)) {
baseAssignValue(object, key, value)
}
}
// ./baseAssignValue
// 賦值的基礎實現
function baseAssignValue(object, key, value) {
if (key == '__proto__') {
Object.defineProperty(object, key, {
'configurable': true,
'enumerable': true,
'value': value,
'writable': true
})
} else {
object[key] = value
}
}
// ./eq
// 比較兩個值是否相等
function eq(value, other) {
return value === other || (value !== value && other !== other)
}
複製代碼
最後的 eq 方法中的判斷 value !== value && other !== other
,這樣的寫法是爲了判斷 NaN。具體的能夠參考這篇 「讀懂源碼系列2」我從 lodash 源碼中學到的幾個知識點
cloneDeep 中囊括了各類類型的深拷貝方法,好比 node 中的 buffer,類型數組等。用了棧的思想,解決循環引用的問題。Map 和 Set 的添加元素方法比較相似,分別爲 set 和 add。NaN 是不等於自身的。
深拷貝的源碼解讀,到此已經完結了。本篇的寫做過程,一樣地耗費了好幾個晚上的時間,感受真的是本身在跟本身較勁。只由於我想盡量地把源碼的實現過程說明白,其中查找資料外加理解思考,就耗費了許多時間,好在最終沒有放棄,收穫也是頗豐的,一些從源碼中學到的技巧,也被我用到了實際項目中,提高了性能與可讀性。。
近階段由於工做緣由,寫文章有所懈怠了,痛定思痛仍是要繼續寫下去。自此,《超哥前端小棧》恢復更新,同時每篇文章也會同步更新到 掘金、segmentfault 和 github 上。
我的的時間精力有限,在表述上有紕漏的地方,還望讀者能多加指正,多多支持,期待能有更多的交流,感謝~
PS:歡迎關注個人公衆號 「超哥前端小棧」,交流更多的想法與技術。