更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端
------ 如下是正文 ------webpack
在上一篇文章中介紹瞭如何實現一個深拷貝,分別說明了對象、數組、循環引用、引用丟失、Symbol
和遞歸爆棧等狀況下的深拷貝實踐,今天咱們來看看 Lodash
如何實現上述以外的函數、正則、Date、Buffer、Map、Set、原型鏈等狀況下的深拷貝實踐。本篇文章源碼基於 Lodash
4.17.11 版本。git
更多內容請查看 GitHubgithub
入口文件是 cloneDeep.js
,直接調用核心文件 baseClone.js
的方法。web
// 木易楊
const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4
function cloneDeep(value) {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}
複製代碼
第一個參數是須要拷貝的對象,第二個是位掩碼(Bitwise),關於位掩碼的詳細介紹請看下面拓展部分。面試
而後咱們進入 ./.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
函數這部分就是核心代碼了,各功能分割以下,詳細功能實現部分將對各個功能詳細解讀。
// 木易楊
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
:添加標誌位 a 和 bmask & a
:取出標誌位 amask & ~a
:清除標誌位 amask ^ a
:取出與 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
。判斷邏輯是 一、數組長度大於 0,二、數組第一個元素是字符串類型,三、數組存在 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
的修改也會反應到棧中。
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
和 原型鏈屬性的拷貝,經過標誌位 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)
}
複製代碼
進階系列文章彙總以下,內有優質前端資料,以爲不錯點個star。
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!