摘要:上對位運算的解釋是它經(jīng)常被用來創(chuàng)建處理以及讀取標(biāo)志位序列一種類似二進制的變量。位運算,常用于處理同時存在多個布爾選項的情形。掩碼中的每個選項的值都是的冪,位運算是位的。位運算,說白了就是直接對某個數(shù)據(jù)在內(nèi)存中的二進制位,進行運算操作。
前言
上一篇文章 「前端面試題系列9」淺拷貝與深拷貝的含義、區(qū)別及實現(xiàn) 中提到了深拷貝的實現(xiàn)方法,從遞歸調(diào)用,到 JSON,再到終極方案 cloneForce。
不經(jīng)讓我想到,lodash 中的 _.cloneDeep 方法。它是如何實現(xiàn)深拷貝的呢?今天,就讓我們來具體地解讀一下 _.cloneDeep 的源碼實現(xiàn)。
源碼中的內(nèi)容比較多,為了能將知識點講明白,也為了更好的閱讀體驗,將會分為上下 2 篇進行解讀。今天主要會涉及位掩碼、對象判斷、數(shù)組和正則的深拷貝寫法。
ok,現(xiàn)在就讓我們深入源碼,共同探索吧~
_.cloneDeep 的源碼實現(xiàn)它的源碼內(nèi)容很少,因為主要還是靠 baseClone 去實現(xiàn)。
/** 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 的第二個參數(shù),似乎還將那兩個常量做了運算,其結(jié)果是什么?這么做的目的是什么?
一番查找之后,終于明白這里其實涉及到了 位掩碼 與 位運算 的概念。下面就來詳細講解一下。
位掩碼技術(shù)回到第一行注釋:Used to compose bitmasks for cloning。意思是,用于構(gòu)成克隆方法的位掩碼。
從注釋看,這里的 CLONE_DEEP_FLAG 和 CLONE_SYMBOLS_FLAG 就是位掩碼了,而 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG 其實是 位運算 中的 按位或 方法。
這里有個不常見的概念:位運算。MDN 上對位運算的解釋是:它經(jīng)常被用來創(chuàng)建、處理以及讀取標(biāo)志位序列——一種類似二進制的變量。雖然可以使用變量代替標(biāo)志位序列,但是這樣可以節(jié)省內(nèi)存(1/32)。
不過實際開發(fā)中,位運算用得很少,主要是因為位運算操作的是二進制位,對開發(fā)者來說不太好理解。用得少,就容易生疏。但實際上,位運算是一種很棒的思想,它計算得更快,代碼量還更少。位運算,常用于處理同時存在多個布爾選項的情形。掩碼中的每個選項的值都是 2 的冪,位運算是 32 位的。
在計算機程序的世界里,所有的數(shù)據(jù)都是以二進制的形式儲存的。位運算,說白了就是直接對某個數(shù)據(jù)在內(nèi)存中的二進制位,進行運算操作。比如 &、|、~、^、>>,這些都是 按位運算符,它們有一些神奇的用法。以系統(tǒng)權(quán)限為例:
const PERMISSION_A = 1; // 0001 const PERMISSION_B = 2; // 0010 const PERMISSION_C = 4; // 0100 const PERMISSION_D = 8; // 1000 // 當(dāng)一個用戶同時擁有 權(quán)限A 和 權(quán)限C 時,就產(chǎn)生了一個新的權(quán)限 const mask = PERMISSION_A | PERMISSION_C; // 0101,十進制為 5 // 判斷該用戶是否有 權(quán)限C,可以取出 權(quán)限C 的位掩碼 if (mask & PERMISSION_C) { ... } // 該用戶沒有 權(quán)限A,也沒有 權(quán)限C const mask2 = ~(PERMISSION_A | PERMISSION_C); // ~0101 => 1010 // 取出 與權(quán)限A 不同的部分 const mask3 = mask ^ PERMISSION_A; // 0101 ^ 0001 => 0100
回到源碼的 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG 就得到一個新的結(jié)果傳入 baseClone 中,十進制為 5,至于它是用來干什么的,就需要繼續(xù)深入到 baseClone 的源碼中去看了。
baseClone 的源碼實現(xiàn)先貼一下源碼,其中一些關(guān)鍵的判斷已經(jīng)做了注釋
function baseClone(value, bitmask, customizer, key, object, stack) { let result // 根據(jù)位掩碼,切分判斷入口 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) { // 處理數(shù)組 result = initCloneArray(value) if (!isDeep) { // 淺拷貝數(shù)組 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 }位掩碼的作用
/** Used to compose bitmasks for cloning. */ const CLONE_DEEP_FLAG = 1 // 深拷貝標(biāo)志位 const CLONE_FLAT_FLAG = 2 // 原型鏈標(biāo)志位 const CLONE_SYMBOLS_FLAG = 4 // Symbol 標(biāo)志位 function baseClone(value, bitmask, customizer, key, object, stack) { // 根據(jù)位掩碼,取出位掩碼,切分判斷入口,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 做額外處理。
處理數(shù)組和正則const isArr = Array.isArray(value) if (isArr) { result = initCloneArray(value) if (!isDeep) { return copyArray(value, result) } } else { ... // 非數(shù)組的處理 } // 用于檢測對象自身的屬性 const hasOwnProperty = Object.prototype.hasOwnProperty // 初始化需要克隆的數(shù)組 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 }
為了不干擾源數(shù)組的數(shù)據(jù),這里首先會用 initCloneArray 初始化一個全新的數(shù)組。
其中,new array.constructor(length) 相當(dāng)于 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 這個屬性。
看到判斷條件里的兩條執(zhí)行語句更懵了,需要賦值 index 和 input,這又是為什么?/(ㄒoㄒ)/~~
回頭看到第一行注釋,有個關(guān)鍵點 RegExp#exec。MDN 中給的解釋:exec() 方法在一個指定字符串中執(zhí)行一個搜索匹配。返回一個結(jié)果數(shù)組或 null。文檔下方有個例子:
var re = /quicks(brown).+?(jumps)/ig; var result = re.exec("The Quick Brown Fox Jumps Over The Lazy Dog"); console.log(result); // 輸出的 result 是一個數(shù)組,有 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 中來,如果不是深拷貝,那就只要做數(shù)組的第一層數(shù)據(jù)的賦值即可。
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 }總結(jié)
位掩碼技術(shù),是一種很棒的思想,可以寫出更為簡潔的代碼,運行得也更快。對象的判斷,需要特別注意 null,它的 typeof 值 也是 object。正則的 exec() 方法會返回一個結(jié)果數(shù)組或 null,其中就會有 index 和 input 屬性。
閱讀源碼的過程比較痛苦,深感自身的不足。從不懂到查閱資料,再到寫出來,耗費了我大量的時間,不過寫作的過程也給了我不小的收獲。修行之路任重而道遠,給自己打打氣,繼續(xù)砥礪前行吧。
未完待續(xù)。。。
崗位內(nèi)推莉莉絲游戲招 高級前端 啦!!!
你玩過《小冰冰傳奇([刀塔傳奇])》么?你玩過《劍與家園》么?還有本篇的封面,為我司的新游戲《AFK arena》,現(xiàn)已占領(lǐng)各大海外應(yīng)用市場(友情提示:要小心,這游戲有毒嗷~)。
有興趣的同學(xué),可以 關(guān)注下面的公眾 號加我微信 詳聊哈~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/104193.html
摘要:用于檢測自己是否在自己的原型鏈上如果是函數(shù),則取出該函數(shù)的原型對象否則,取出對象的原型對象其中,的判斷,是為了確定的類型是對象或數(shù)組。相當(dāng)于,而的構(gòu)造函數(shù)是一個函數(shù)對象。 showImg(https://segmentfault.com/img/bVbq2N1?w=640&h=437); 前言 接著上一篇文章 lodash 是如何實現(xiàn)深拷貝的(上),今天會繼續(xù)解讀 _.cloneDee...
摘要:總結(jié)綜上所述,數(shù)組的深拷貝比較簡單,方法沒有什么爭議,對象的深拷貝,比較好的方法是用的方法實現(xiàn),或者遞歸實現(xiàn),比較簡單的深復(fù)制可以使用實現(xiàn)參考資料知乎中的深拷貝和淺拷貝深入剖析的深復(fù)制 深淺復(fù)制對比 因為JavaScript存儲對象都是存地址的,所以淺復(fù)制會導(dǎo)致 obj 和obj1 指向同一塊內(nèi)存地址。我的理解是,這有點類似數(shù)據(jù)雙向綁定,改變了其中一方的內(nèi)容,都是在原來的內(nèi)存基礎(chǔ)上做...
摘要:引用類型之所以會出現(xiàn)深淺拷貝的問題,實質(zhì)上是由于對基本類型和引用類型的處理不同。另外方法可以視為數(shù)組對象的淺拷貝。上面描述過的復(fù)雜問題依然存在,可以說是最簡陋但是日常工作夠用的深拷貝方式。 一直想梳理下工作中經(jīng)常會用到的深拷貝的內(nèi)容,然而遍覽了許多的文章,卻發(fā)現(xiàn)對深拷貝并沒有一個通用的完美實現(xiàn)方式。因為對深拷貝的定義不同,實現(xiàn)時的edge case過多,在深拷貝的時候會出現(xiàn)循環(huán)引用等問...
摘要:今天要講的,是我從的源碼實現(xiàn)文件中學(xué)到的幾個很基礎(chǔ),卻又容易被忽略的知識點。在函數(shù)式編程中,函數(shù)是一等公民,它可以只是根據(jù)參數(shù),做簡單的組合操作,再作為別的函數(shù)的返回值。所以,閱讀源碼,是一種很棒的重溫基礎(chǔ)知識的方式。 showImg(https://segmentfault.com/img/bVbpTSY?w=750&h=422); 前言 上一篇文章 「前端面試題系列8」數(shù)組去重(1...
摘要:深拷貝是一件看起來很簡單的事情,但其實一點兒也不簡單。我們也可以利用這個實現(xiàn)對象的深拷貝。而是利用之前已經(jīng)拷貝好的值。深拷貝的詳細的源碼可以在這里查看。大功告成我們雖然的確解決了深拷貝的大部分問題。 js深拷貝是一件看起來很簡單的事情,但其實一點兒也不簡單。對于循環(huán)引用的問題還有一些內(nèi)置數(shù)據(jù)類型的拷貝,如Map, Set, RegExp, Date, ArrayBuffer 和其他內(nèi)置...
閱讀 1411·2021-10-08 10:04
閱讀 733·2021-09-07 09:58
閱讀 2912·2019-08-30 15:55
閱讀 2423·2019-08-29 17:21
閱讀 2126·2019-08-28 18:04
閱讀 3075·2019-08-28 17:57
閱讀 714·2019-08-26 11:46
閱讀 2228·2019-08-23 17:20