lodash源码解读之includes

467 阅读7分钟

该文章基于lodash4.0.0

方法介绍

_.includes(collection, value, [fromIndex=0])

我们知道es6中也有includes方法,那lodashincludes的跟es6中的有什么区别呢?

其实lodash中的includes方法跟原生includes方法类似,也是检测value值是否在collection集合中,collection集合可以是字符串、数组和对象,如果collection是一个字符串,那么检测value值(子字符串)是否在字符串中,否则使用SaveValueZero做等值比较,fromIndex表示从何处开始检测,如果是负数,则从集合末尾开始检测

参数

  • collection:要检索的集合,类型为(Array|Object|string)
  • value:要检索的值,可以为任何类型
  • [fromIndex=0]:要检索的索引起始位置,number类型,为负数时从末尾开始检索(注意:这里的从末尾开始检索跟原生indexOf的从末尾开始检索有点不一样,比如formIndex为-4,表示从末尾检索,检索到倒数第四个下标处,也就是检索长度为4,其实也不是从末尾开始检索的,后面会详细说明原因)

返回值

该方法会返回一个boolean值,如果在集合中找到value,返回true,否则返回false

用法用例

_.includes([1, 2, 3], 1);
// => true
 
_.includes([1, 2, 3], 1, 2);
// => false

_.includes([1, 2, 3], 1, -1);
// => false

_.includes([1, 2, 3], 1, -3);
// => true
 
_.includes({ 'user': 'fred', 'age': 40 }, 'fred');
// => true
 
_.includes('pebbles', 'eb');
// => true

源码解读

lodash的includes长这样的:

includes

function includes(collection, value, fromIndex, guard) {
  // 这里对collection做一下判断,如果是数组,就直接返回collection,否则使用values方法取其值
  collection = isArrayLike(collection) ? collection : values(collection);
  // 如果传入了formIndex且为真,!guard为真,使用toInteger对formIndex进行处理,否则为0
  // toInteger就是将目标元素处理成数组的方法
  fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0;

  // 取collection的长度,这里的collection一定具有legnth属性的
  var length = collection.length;
  if (fromIndex < 0) {
  // nativeMax就是Math.max,这里主要时为了避免formIndex的绝对值超过了集合的长度
    fromIndex = nativeMax(length + fromIndex, 0);
  }
  return isString(collection)
  // collectiopn如果是字符串,就直接使用es的indexOf判断
    ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1)
    // 否则,如果length存在且不为0,就返回baseIndexOf的执行结果
    : (!!length && baseIndexOf(collection, value, fromIndex) > -1);
}

从源码中我们可以看到,其实includes方法是接收四个参数的,那第四个参数是干什么的呢?往下看

看函数的第一行,有个values方法,这个方法也就是lodash提供给我们调用的values方法,其目的是取一个集合的value值,返回是一个数组,values方法内部还设计到keys方法,都是lodash暴露出来的方法,这两个方法我会在后面的文章详细解读,这里就大概知道它是干什么的就好
所以这里就能确定,在函数内部的collection一定是具有length属性的(数组或类数组),然后,在函数内部又重新对formIndex进行赋值了,条件分三种情况:

  • formIndex没有传,formIndex就默认是0
  • formIdnex有值且为真,同时也传入了guard,并且也为真,formIndex还是为0
  • formIndex有值且为真,!guard也为真,formIndex就为toInteger(fromIndex)的返回值 toInteger这个方法是将元素转为数字,跟上面的valueskeys一样,都是lodash暴露出来的可以调用的方法(_.toInteger(value)),那内部到底是如果做的呢?直接看其源码

toInteger

function toInteger(value) {
  if (!value) {
    // 这个三木运算其实是没必要的,直接返回0即可
    return value === 0 ? value : 0;
  }
  // 经过toNumber之后,value就是为数字类型,如果vlaue为object等类型,则返回NaN,如果为
  // 只包含数字的字符串,则就是将字符串转为了数字,否则返回NaN
  value = toNumber(value);
  if (value === INFINITY || value === -INFINITY) {
    var sign = (value < 0 ? -1 : 1);
    // MAX_INTEGER为 1.7976931348623157e+308,sign为 -1 或 1
    return sign * MAX_INTEGER;
  }
  // 对 value 取模,value为整数,remainder就为0,否则为value的小数部分,注意,会有精度丢失的问题
  var remainder = value % 1;
  // 当value是字符串等类型,value === value就不成立(NaN!==NaN,'1' !== 1),就返回 0
  // value为整数,remainder就为0,返回值就为value,否则为value - remainder
  return value === value ? (remainder ? value - remainder : value) : 0;
}

INFINITY就是无限大,当一个数除以 0 时,其结果就是无限大,如果value不为无限大,remaindervalue的小数部分(value为整数,其小数就为0,因为使用了运算符,所以会造成精度丢失的问题),其返回结果三木运算的意思就是:

  • value === value不成立(传入的value不为数字,也不是纯数字的字符串),就返回0
  • value === value成立,value为整数就直接返回value,不为整数,就返回(value - value % 1),其值也是整数,跟Math.floor()的结果一致

回到includes函数,此时formIndex就是整数了,lengthcollection.length,其值也是正整数,当formIndex小于0时:

fromIndex = nativeMax(length + fromIndex, 0); 

这样可以防止传入的formIndex的绝对值大于集合本身的长度,如果是这种情况,就直接从0开始检索; 但是这样有一个问题就是formIndex为正数了,在collection为字符串时,是直接调用esindexOf取检索的,那这样就无法实现传入的formIndex为负数时,从集合末尾开始检索了,正是因为这里的代码,所以collection是字符串时,formIndex为负数,才导致其并非是跟indexOf中的从末尾开始检索一样的方式

lodash中的includes方法,当collection为字符串,formIndex为负数,其检索位置是从字符串长度减去formIndex的绝对值处开始,一直到末尾

如果collection不是字符串,且不是空数组、空对象、undefined等对象时,就使用baseIndexOf方法检索,看下baseIndexOf方法具体是怎么处理的:

baseIndexOf

function baseIndexOf(array, value, fromIndex) {
    // 如果value值是NaN,调用 indexOfNaN 判断
    if (value !== value) { 
      return indexOfNaN(array, fromIndex);
    }
    // 这里index 取 fromIndfex - 1,因为下面while循环中是使用++index判断的
    var index = fromIndex - 1,
        length = array.length;

    // 循环遍历array的每一项,当其值等于value时,跳出循环,返回index的值,也就是数组下标
    while (++index < length) {
      if (array[index] === value) {
        return index;
      }
    }
    // 循环结束,array中没有与value相等的值,返回 -1
    return -1;
}

可以看到,如果value不是NaN,就会走while循环遍历array(也就是collectionvalues),找出array中与value值相等的项,并返回其下标,如果没有找到,则返回 -1;

注意:在while循环中判断值是否相等是使用的 === 判断,也就是不存在隐式类型转换,而且,如果是引用数据类型的话,只能判断其在堆内存中的地址是否一样

最后,我们看下如果valueNaN的情况,lodash是如何处理的:

indexOfNaN

function indexOfNaN(array, fromIndex, fromRight) {
    var length = array.length,
    // 如果fromRight没有传,则是正向检索,因为下面是++index,所以先将formIndex - 1
        index = fromIndex + (fromRight ? 0 : -1);
    // formRight代表是否是从formIndex处往起始位置开始检索
    while ((fromRight ? index-- : ++index < length)) {
      var other = array[index];
      // 判断一个元素是否是NaN最好的办法就是判断其自身是否等于自身,因为 NaN !== NaN
      if (other !== other) {
        return index;
      }
    }
    return -1;
}

可以看到,indexOfNaN其实是接收三个参数的,第三个参数fromRight代表是否是从右往左开始检索,也就是是否从fromIndex处往array的起始处开始检索,在while循环中,直接判断array的每一项是否等于自身,不等于就直接返回其下标,因为valueNaN的话,我们只需要找出array中的NaN元素就行,而判断一个元素是否是NaN最好的办法就是判断其自身是否等于自身(NaN !== NaN);如果array中没有NaN元素,则返回 -1

至此,lodashincludes解析完毕,最后对includes总结一下

总结

_.includes(collection, value, [fromIndex=0])

includes方法接收三个参数(其实是四个参数,但官方文档只写出了三个参数,因为第四个参数无关紧要,我们可以不用管,之所以会存在这个参数,是因为lodash内部自身调用includes函数时需要用到这个参数),collection表示要检索的集合,value表示要检索的值,formIndex表示要检索的索引位置

  • 其中,formIndex可以为负数,官方文档解释为负数是从集合结尾开始检索,其实内部并不是从结尾开始检索的,也是正向检索,只不过是从formIndex的绝对值的下标处开始检索到结尾,跟从结尾处开始检索的返回的结果是一致的
  • formIndex只能是数字或则纯数字的字符串,否则,会使用默认值 0
  • collection可以是字符串、数组和对象,字符串和数组很常规的检索,但是是对象的时候,是检索的对象的values,而且判断时使用的 === ,不存在隐式类型转换的同时,也无法比较引用数据类型
  • 如果valueNaN,则是直接检索values的数组中是否存在NaN,并返回其下标,没有则返回 -1