摘要:用于檢測自己是否在自己的原型鏈上如果是函數,則取出該函數的原型對象否則,取出對象的原型對象其中,的判斷,是為了確定的類型是對象或數組。相當于,而的構造函數是一個函數對象。
前言
接著上一篇文章 lodash 是如何實現(xiàn)深拷貝的(上),今天會繼續(xù)解讀 _.cloneDeep 的源碼,來看看 lodash 是如何處理對象、函數、循環(huán)引用等的深拷貝問題的。
baseClone 的源碼實現(xiàn)先回顧一下它的源碼,以及一些關鍵的注釋
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) } } // 用 “棧” 處理循環(huán)引用 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 }處理對象和函數
一些主要的判斷入口,已經加上了注釋。
const isArr = Array.isArray(value) const tag = getTag(value) if (isArr) { ... // 剛才數組的處理 } else { // 開始處理對象 // 對象是函數的標志位 const isFunc = typeof value == "function" // 處理 Buffer(緩沖區(qū))對象 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 中的概念,用來創(chuàng)建一個專門存放二進制數據的緩存區(qū),可以讓 Node.js 處理二進制數據。
在 baseClone 的外面,還定義了一個對象 cloneableTags,里面只有 error 和 weakmap 類型會返回 false,所以 !cloneableTags[tag] 的意思就是,不是 error 或 weakmap 類型。
接下來,來看如何初始化一個新的 Object 對象。
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() 方法用于創(chuàng)建一個新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的 __proto__。
object.constructor 相當于 new Object(),而 Object 的構造函數是一個函數對象。
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 提供了訪問二進制數據的能力。
循環(huán)引用// 如果有 stack 作為參數傳入,就用參數中的 stack // 不然就 new 一個 Stack stack || (stack = new Stack) const stacked = stack.get(value) if (stacked) { return stacked } stack.set(value, result)
與 「前端面試題系列9」淺拷貝與深拷貝的含義、區(qū)別及實現(xiàn) 最后提到的 cloneForce 方案類似,利用了棧來解決循環(huán)引用的問題。
如果 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
這兩個類型的深拷貝利用了遞歸的思想,只是添加元素的方式有區(qū)別,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 }Symbol 和 原型鏈
// 獲取數組 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 的實現(xiàn)。
// ./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 // 賦值的基礎實現(xiàn) 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,類型數組等。用了棧的思想,解決循環(huán)引用的問題。Map 和 Set 的添加元素方法比較類似,分別為 set 和 add。NaN 是不等于自身的。
深拷貝的源碼解讀,到此已經完結了。本篇的寫作過程,同樣地耗費了好幾個晚上的時間,感覺真的是自己在跟自己較勁。只因為我想盡可能地把源碼的實現(xiàn)過程說明白,其中查找資料外加理解思考,就耗費了許多時間,好在最終沒有放棄,收獲也是頗豐的,一些從源碼中學到的技巧,也被我用到了實際項目中,提升了性能與可讀性。。
近階段因為工作原因,寫文章有所懈怠了,痛定思痛還是要繼續(xù)寫下去。自此,《超哥前端小棧》恢復更新,同時每篇文章也會同步更新到 掘金、segmentfault 和 github 上。
個人的時間精力有限,在表述上有紕漏的地方,還望讀者能多加指正,多多支持,期待能有更多的交流,感謝~
PS:歡迎關注我的公眾號 “超哥前端小棧”,交流更多的想法與技術。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105730.html
摘要:上對位運算的解釋是它經常被用來創(chuàng)建處理以及讀取標志位序列一種類似二進制的變量。位運算,常用于處理同時存在多個布爾選項的情形。掩碼中的每個選項的值都是的冪,位運算是位的。位運算,說白了就是直接對某個數據在內存中的二進制位,進行運算操作。 showImg(https://segmentfault.com/img/bVbrC56?w=2208&h=1242); 前言 上一篇文章 「前端面試題...
摘要:引用類型之所以會出現(xiàn)深淺拷貝的問題,實質上是由于對基本類型和引用類型的處理不同。另外方法可以視為數組對象的淺拷貝。上面描述過的復雜問題依然存在,可以說是最簡陋但是日常工作夠用的深拷貝方式。 一直想梳理下工作中經常會用到的深拷貝的內容,然而遍覽了許多的文章,卻發(fā)現(xiàn)對深拷貝并沒有一個通用的完美實現(xiàn)方式。因為對深拷貝的定義不同,實現(xiàn)時的edge case過多,在深拷貝的時候會出現(xiàn)循環(huán)引用等問...
摘要:深拷貝是一件看起來很簡單的事情,但其實一點兒也不簡單。我們也可以利用這個實現(xiàn)對象的深拷貝。而是利用之前已經拷貝好的值。深拷貝的詳細的源碼可以在這里查看。大功告成我們雖然的確解決了深拷貝的大部分問題。 js深拷貝是一件看起來很簡單的事情,但其實一點兒也不簡單。對于循環(huán)引用的問題還有一些內置數據類型的拷貝,如Map, Set, RegExp, Date, ArrayBuffer 和其他內置...
摘要:展開語法木易楊通過代碼可以看出實際效果和是一樣的。木易楊可以看出,改變之后的值并沒有發(fā)生變化,但改變之后,相應的的值也發(fā)生變化。深拷貝使用場景木易楊完全改變變量之后對沒有任何影響,這就是深拷貝的魔力。木易楊情況下,轉換結果不正確。 一、賦值(Copy) 賦值是將某一數值或對象賦給某個變量的過程,分為下面 2 部分 基本數據類型:賦值,賦值之后兩個變量互不影響 引用數據類型:賦址,兩個...
摘要:它將返回目標對象。有些文章說是深拷貝,其實這是不正確的。深拷貝相比于淺拷貝速度較慢并且花銷較大。拷貝前后兩個對象互不影響。使用深拷貝的場景完全改變變量之后對沒有任何影響,這就是深拷貝的魔力。 一、賦值(Copy) 賦值是將某一數值或對象賦給某個變量的過程,分為: 1、基本數據類型:賦值,賦值之后兩個變量互不影響 2、引用數據類型:賦址,兩個變量具有相同的引用,指向同一個對象,相互之間有...
閱讀 2796·2021-11-16 11:44
閱讀 969·2021-10-09 09:58
閱讀 4489·2021-09-24 09:48
閱讀 4250·2021-09-23 11:56
閱讀 2407·2021-09-22 15:48
閱讀 1892·2021-09-07 10:07
閱讀 3204·2021-08-31 09:46
閱讀 504·2019-08-30 15:56