引言
在上一篇文章中介紹瞭如何實現一個深拷貝,分別說明了物件、陣列、迴圈引用、引用丟失、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
函式 - 非物件
- 陣列 & 正則
- 物件 & 函式
- 迴圈引用
- Map & Set
- Symbol & 原型鏈
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
:新增標誌位 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
。判斷邏輯是 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。
我是木易楊,網易高階前端工程師,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!