「讀懂原始碼系列3」lodash 是如何實現深拷貝的(上)

Micherwa發表於2019-05-08

前言

上一篇文章 「前端面試題系列9」淺拷貝與深拷貝的含義、區別及實現 中提到了深拷貝的實現方法,從遞迴呼叫,到 JSON,再到終極方案 cloneForce。

不經讓我想到,lodash 中的 _.cloneDeep 方法。它是如何實現深拷貝的呢?今天,就讓我們來具體地解讀一下 _.cloneDeep 的原始碼實現。

原始碼中的內容比較多,為了能將知識點講明白,也為了更好的閱讀體驗,將會分為上下 2 篇進行解讀。今天主要會涉及位掩碼、物件判斷、陣列和正則的深拷貝寫法。

ok,現在就讓我們深入原始碼,共同探索吧~

_.cloneDeep 的原始碼實現

它的原始碼內容很少,因為主要還是靠 baseClone 去實現。

/** 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 的第二個引數,似乎還將那兩個常量做了運算,其結果是什麼?這麼做的目的是什麼?

一番查詢之後,終於明白這裡其實涉及到了 位掩碼位運算 的概念。下面就來詳細講解一下。

位掩碼技術

回到第一行註釋:Used to compose bitmasks for cloning。意思是,用於構成克隆方法的位掩碼。

從註釋看,這裡的 CLONE_DEEP_FLAGCLONE_SYMBOLS_FLAG 就是位掩碼了,而 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG 其實是 位運算 中的 按位或 方法。

這裡有個不常見的概念:位運算。MDN 上對位運算的解釋是:它經常被用來建立、處理以及讀取標誌位序列——一種類似二進位制的變數。雖然可以使用變數代替標誌位序列,但是這樣可以節省記憶體(1/32)。

不過實際開發中,位運算用得很少,主要是因為位運算操作的是二進位制位,對開發者來說不太好理解。用得少,就容易生疏。但實際上,位運算是一種很棒的思想,它計算得更快,程式碼量還更少。位運算,常用於處理同時存在多個布林選項的情形。掩碼中的每個選項的值都是 2 的冪,位運算是 32 位的。

在計算機程式的世界裡,所有的資料都是以二進位制的形式儲存的。位運算,說白了就是直接對某個資料在記憶體中的二進位制位,進行運算操作。比如 &|~^>>,這些都是 按位運算子,它們有一些神奇的用法。以系統許可權為例:

const PERMISSION_A = 1; // 0001
const PERMISSION_B = 2; // 0010
const PERMISSION_C = 4; // 0100
const PERMISSION_D = 8; // 1000

// 當一個使用者同時擁有 許可權A 和 許可權C 時,就產生了一個新的許可權
const mask = PERMISSION_A | PERMISSION_C; // 0101,十進位制為 5

// 判斷該使用者是否有 許可權C,可以取出 許可權C 的位掩碼
if (mask & PERMISSION_C) {
    ...
}

// 該使用者沒有 許可權A,也沒有 許可權C
const mask2 = ~(PERMISSION_A | PERMISSION_C); // ~0101 => 1010

// 取出 與許可權A 不同的部分
const mask3 = mask ^ PERMISSION_A; // 0101 ^ 0001 => 0100
複製程式碼

回到原始碼的 CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG 就得到一個新的結果傳入 baseClone 中,十進位制為 5,至於它是用來幹什麼的,就需要繼續深入到 baseClone 的原始碼中去看了。

baseClone 的原始碼實現

先貼一下原始碼,其中一些關鍵的判斷已經做了註釋

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)
    }
  }
  // 用 “棧” 處理迴圈引用
  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 // 深拷貝標誌位
const CLONE_FLAT_FLAG = 2 // 原型鏈標誌位
const CLONE_SYMBOLS_FLAG = 4 // Symbol 標誌位

function baseClone(value, bitmask, customizer, key, object, stack) {
    // 根據位掩碼,取出位掩碼,切分判斷入口,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 做額外處理。

處理陣列和正則

const isArr = Array.isArray(value)

if (isArr) {
    result = initCloneArray(value)
    if (!isDeep) {
        return copyArray(value, result)
    }
} else {
    ... // 非陣列的處理
}

// 用於檢測物件自身的屬性
const hasOwnProperty = Object.prototype.hasOwnProperty

// 初始化需要克隆的陣列
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
}
複製程式碼

為了不干擾源陣列的資料,這裡首先會用 initCloneArray 初始化一個全新的陣列。

其中,new array.constructor(length) 相當於 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 這個屬性。

看到判斷條件裡的兩條執行語句更懵了,需要賦值 indexinput,這又是為什麼?/(ㄒoㄒ)/~~

回頭看到第一行註釋,有個關鍵點 RegExp#execMDN 中給的解釋:exec() 方法在一個指定字串中執行一個搜尋匹配。返回一個結果陣列或 null。文件下方有個例子:

var re = /quick\s(brown).+?(jumps)/ig;
var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
console.log(result);

// 輸出的 result 是一個陣列,有 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
複製程式碼

哇哦~ 原來 indexinput 在這裡。所以,原始碼中的為何要那樣賦值,就迎刃而解了。

再回到 baseClone 中來,如果不是深拷貝,那就只要做陣列的第一層資料的賦值即可。

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
}
複製程式碼

總結

位掩碼技術,是一種很棒的思想,可以寫出更為簡潔的程式碼,執行得也更快。物件的判斷,需要特別注意 null,它的 typeof 值 也是 object。正則的 exec() 方法會返回一個結果陣列或 null,其中就會有 index 和 input 屬性。

閱讀原始碼的過程比較痛苦,深感自身的不足。從不懂到查閱資料,再到寫出來,耗費了我大量的時間,不過寫作的過程也給了我不小的收穫。修行之路任重而道遠,給自己打打氣,繼續砥礪前行吧。

未完待續。。。

崗位內推

莉莉絲遊戲招 高階前端 啦!!!

你玩過《小冰冰傳奇([刀塔傳奇])》麼?你玩過《劍與家園》麼?還有本篇的封面,為我司的新遊戲《AFK arena》,現已佔領各大海外應用市場(友情提示:要小心,這遊戲有毒嗷~)。

有興趣的同學,可以 關注下面的公眾 號加我微信 詳聊哈~

「讀懂原始碼系列3」lodash 是如何實現深拷貝的(上)

相關文章