「讀懂源碼系列2」我從 lodash 源碼中學到的幾個知識點

圖片描述

前言

上一篇文章 「前端面試題系列8」數組去重(10 種濃縮版) 的最後,簡單介紹了 lodash 中的數組去重方法 _.uniq,它能夠實現咱們平常工做中的去重需求,可以去重 NaN,並保留 {...}前端

今天要講的,是我從 _.uniq 的源碼實現文件 baseUniq.js 中學到的幾個很基礎,卻又容易被忽略的知識點。面試

三個 API

讓咱們先從三個功能相近的 API 講起,他們分別是:_.uniq_.uniqBy_.uniqWith。它們三個背後的實現文件,都指向了 .internal 下的 baseUniq.js編程

區別在於 _.uniq 只需傳入一個源數組 array, _.uniqBy 相較於 _.uniq 要多傳一個迭代器 iteratee,而 _.uniqWith 要多傳一個比較器 comparator。iterateecomparator 的用法,會在後面說到。segmentfault

以 _.uniqWith 爲例,它是這樣調用 _.baseUniq 的:數組

function uniqWith(array, comparator) {
  comparator = typeof comparator == 'function' ? comparator : undefined
  return (array != null && array.length)
    ? baseUniq(array, undefined, comparator)
    : []
}

baseUniq 的實現原理

baseUniq 的源碼並很少,但比較繞。先貼一下的源碼。app

const LARGE_ARRAY_SIZE = 200

function baseUniq(array, iteratee, comparator) {
  let index = -1
  let includes = arrayIncludes
  let isCommon = true

  const { length } = array
  const result = []
  let seen = result

  if (comparator) {
    isCommon = false
    includes = arrayIncludesWith
  }
  else if (length >= LARGE_ARRAY_SIZE) {
    const set = iteratee ? null : createSet(array)
    if (set) {
      return setToArray(set)
    }
    isCommon = false
    includes = cacheHas
    seen = new SetCache
  }
  else {
    seen = iteratee ? [] : result
  }
  outer:
  while (++index < length) {
    let value = array[index]
    const computed = iteratee ? iteratee(value) : value

    value = (comparator || value !== 0) ? value : 0
    if (isCommon && computed === computed) {
      let seenIndex = seen.length
      while (seenIndex--) {
        if (seen[seenIndex] === computed) {
          continue outer
        }
      }
      if (iteratee) {
        seen.push(computed)
      }
      result.push(value)
    }
    else if (!includes(seen, computed, comparator)) {
      if (seen !== result) {
        seen.push(computed)
      }
      result.push(value)
    }
  }
  return result
}

爲了兼容剛纔說的三個 API,就產生了很多的干擾項。若是先從 _.uniq 入手,去掉 iteratee 和 comparator 的干擾,就會清晰很多。ide

function baseUniq(array) {
    let index = -1
    const { length } = array
    const result = []

    if (length >= 200) {
        const set = createSet(array)
        return setToArray(set)
    }

    outer:
    while (++index < length) {
        const value = array[index]
        if (value === value) {
            let resultIndex = result.length
            while (resultIndex--) {
                if (result[resultIndex] === value) {
                    continue outer
                }
            }
            result.push(value)
        } else if (!includes(seen, value)) {
            result.push(value)
        }
    }
    return result
}

這裏有 2 個知識點。函數式編程

知識點1、NaN === NaN 嗎?

在源碼中有一個判斷 value === value,乍一看,會以爲這是句廢話!?!但其實,這是爲了過濾 NaN 的狀況。函數

MDN 中對 NaN 的解釋是:它是一個全局對象的屬性,初始值就是 NaN。它一般都是在計算失敗時,做爲 Math 的某個方法的返回值出現的。性能

判斷一個值是不是 NaN,必須使用 Number.isNaN()isNaN(),在執行自比較之中:NaN,也只有 NaN,比較之中不等於它本身。

NaN === NaN;        // false
Number.NaN === NaN; // false
isNaN(NaN);         // true
isNaN(Number.NaN);  // true

因此,在源碼中,當遇到 NaN 的狀況時,baseUniq 會轉而去執行 !includes(seen, value) 的判斷,去處理 NaN 。

知識點2、冒號的特殊做用

在源碼的主體部分,while 語句以前,有一行 outer:,它是幹什麼用的呢? while 中還有一個 while 的內部,有一行 continue outer,從語義上理解,好像是繼續執行 outer,這又是種什麼寫法呢?

outer:
while (++index < length) {
    ...
    while (resultIndex--) {
        if (result[resultIndex] === value) {
            continue outer
        }
    }
}

咱們都知道 Javascript 中,經常使用到冒號的地方有三處,分別是:A ? B : C 三元操做符、switch case 語句中、對象的鍵值對組成

但其實還有一種並不常見的特殊做用:標籤語句。在 Javascript 中,任何語句均可以經過在它前面加上標誌符和冒號來標記(identifier: statement),這樣就能夠在任何地方使用該標記,最經常使用於循環語句中。

因此,在源碼中,outer 只是看着有點不習慣,多看兩遍就行了,語義上仍是很好理解的。

_.uniqBy 的 iteratee

_.uniqBy 可根據指定的 key 給一個對象數組去重,一個官網的例子以下:

// The `_.property` iteratee shorthand.
_.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');
// => [{ 'x': 1 }, { 'x': 2 }]

這裏的 'x'_.property('x') 的縮寫,它指的就是 iteratee

從給出的例子和語義上看,還挺好理解的。可是爲何 _.property 就能實現對象數組的去重了呢?它又是如何實現的呢?

@param {Array|string} path The path of the property to get.
@returns {Function} Returns the new accessor function.

function property(path) {
  return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path)
}

從註釋看,property 方法會返回一個 Function,再看 baseProperty 的實現:

@param {string} key The key of the property to get.
@returns {Function} Returns the new accessor function.

function baseProperty(key) {
  return (object) => object == null ? undefined : object[key]
}

咦?怎麼返回的仍是個 Function ?感受它什麼也沒幹呀,那個參數 object 又是哪裏來的?

知識點3、純函數的概念

純函數,是函數式編程中的概念,它表明這樣一類函數:對於指定輸出,返回指定的結果。不存在反作用

// 這是一個簡單的純函數
const addByOne = x => x + 1;

也就是說,純函數的返回值只依賴其參數,函數體內不能存在任何反作用。若是是一樣的參數,則必定能獲得一致的返回結果。

function baseProperty(key) {
  return (object) => object == null ? undefined : object[key]
}

baseProperty 返回的就是一個純函數,在符合條件的狀況下,輸出 object[key]。在函數式編程中,函數是「一等公民」,它能夠只是根據參數,作簡單的組合操做,再做爲別的函數的返回值。

因此,在源碼中,object 是調用 baseProperty 時傳入的對象。 baseProperty 的做用,是返回指望結果爲 object[key] 的函數。

_.uniqWith 的 comparator

仍是先從官網的小例子提及,它會徹底地給對象中全部的鍵值對,進行比較。

var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];

_.uniqWith(objects, _.isEqual);
// => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]

而在 baseUniq 的源碼中,能夠看到最終的實現,須要依賴 arrayIncludesWith 方法,如下是它的源碼:

function arrayIncludesWith(array, target, comparator) {
  if (array == null) {
    return false
  }

  for (const value of array) {
    if (comparator(target, value)) {
      return true
    }
  }
  return false
}

arrayIncludesWith 沒什麼複雜的。comparator 做爲一個參數傳入,將 targetarray 的每一個 value 進行處理。從官網的例子看,_.isEqual 就是 comparator,就是要比較它們是否相等。

接着就追溯到了 _.isEqual 的源碼,它的實現文件是 baseIsEqualDeep.js。在裏面看到一個讓我犯迷糊的寫法,這是一個判斷。

/** Used to check objects for own properties. */
const hasOwnProperty = Object.prototype.hasOwnProperty
...

const objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__')

hasOwnProperty ?call, '__wrapped__' ?

知識點4、對象的 hasOwnProperty

再次查找到了 MDN 的解釋:全部繼承了 Object 的對象都會繼承到 hasOwnProperty 方法。它能夠用來檢測一個對象是否含有特定的自身屬性;會忽略掉那些從原型鏈上繼承到的屬性。

o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop');             // 返回 true
o.hasOwnProperty('toString');         // 返回 false
o.hasOwnProperty('hasOwnProperty');   // 返回 false

call 的用法能夠參考這篇 細說 call、apply 以及 bind 的區別和用法

那麼 hasOwnProperty.call(object, '__wrapped__') 的意思就是,判斷 object 這個對象上是否存在 '__wrapped__' 這個自身屬性。

wrapped 是什麼屬性?這就要說到 lodash 的延遲計算方法 _.chain,它是一種函數式風格,從名字就能夠看出,它實現的是一種鏈式的寫法。好比下面這個例子:

var names = _.chain(users)
  .map(function(user){
    return user.user;
  })
  .join(" , ")
  .value();

若是你沒有顯樣的調用value方法,使其當即執行的話,將會獲得以下的LodashWrapper延遲表達式:

LodashWrapper {__wrapped__: LazyWrapper, __actions__: Array[1], __chain__: true, constructor: function, after: function…}

由於延遲表達式的存在,所以咱們能夠屢次增長方法鏈,但這並不會被執行,因此不會存在性能的問題,最後直到咱們須要使用的時候,使用 value() 顯式當即執行便可。

因此,在 baseIsEqualDeep 源碼中,才須要作 hasOwnProperty 的判斷,而後在須要的狀況下,執行 object.value()

總結

閱讀源碼,在一開始會比較困難,由於會遇到一些看不明白的寫法。就像一開始我卡在了 value === value 的寫法,不明白它的用意。一旦知道了是爲了過濾 NaN 用的,那後面就會通暢不少了。

因此,閱讀源碼,是一種很棒的重溫基礎知識的方式。遇到看不明白的點,不要放過,多查多問多看,才能不斷地夯實基礎,讀懂更多的源碼思想,體會更多的原生精髓。若是我在一開始看到 value === value 時就放棄了,那或許就不會有今天的這篇文章了。

PS:歡迎關注個人公衆號 「超哥前端小棧」,交流更多的想法與技術。

相關文章
相關標籤/搜索