「读懂源码系列2」我从 lodash 源码中学到的几个知识点

《「读懂源码系列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 的用法,会在背面说到。

以 _.uniqWith 为例,它是如许挪用 _.baseUniq 的:

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

baseUniq 的完成道理

baseUniq 的源码并不多,但比较绕。先贴一下的源码。

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 的滋扰,就会清楚不少。

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 个知识点。

知识点一、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 。

知识点二、冒号的特殊作用

在源码的主体部份,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 又是哪里来的?

知识点三、纯函数的观点

纯函数,是函数式编程中的观点,它代表如许一类函数:关于指定输出,返回指定的效果。不存在副作用

// 这是一个简朴的纯函数
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__’ ?

知识点四、对象的 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:迎接关注我的民众号 “超哥前端小栈”,交换更多的主意与手艺。

《「读懂源码系列2」我从 lodash 源码中学到的几个知识点》

    原文作者:micherwa
    原文地址: https://segmentfault.com/a/1190000018526929
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞