【進階4-4期】Lodash是如何實現深拷貝的

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端

------ 如下是正文 ------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),關於位掩碼的詳細介紹請看下面拓展部分。面試

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 和 b
  • mask & a:取出標誌位 a
  • mask & ~a:清除標誌位 a
  • mask ^ 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 返回的數組,拷貝屬性 indexinput。判斷邏輯是 一、數組長度大於 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
// ]
複製代碼

若是不是深拷貝,傳入valueresult,直接返回淺拷貝後的數組。這裏的淺拷貝方式就是循環而後複製。

// 木易楊
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)
    }
}
複製代碼

經過上面代碼能夠發現,函數、errorweakmap 時返回空對象 {},並不會真正拷貝函數。

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)
複製代碼

若是當前須要拷貝的值已存在於棧中,說明有環,直接返回便可。棧中沒有該值時保存到棧中,傳入 valueresult。這裏的 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.setSet.add

Symbol & 原型鏈

這裏咱們介紹下 Symbol 和 原型鏈屬性的拷貝,經過標誌位 isFullisFlat 來控制是否拷貝。

// 木易楊
// 主線代碼
// 類型化數組對象
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
}
複製代碼

上面經過 keysInkeys 獲取常規可枚舉屬性,經過 getSymbolsIngetSymbols 獲取 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)
}
複製代碼

參考

lodash

lodash深拷貝源碼探究

按位操做符

RegExp.prototype.exec()

進階系列目錄

  • 【進階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。

github.com/yygmind/blo…

我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!

相關文章
相關標籤/搜索