lodash源码解读之 forEach

1,170 阅读6分钟

介绍

lodash中的forEach跟js原生的forEach功能类似,不同的是,原生forEach只能遍历数组或则类数组,但是lodash中的forEach是用来遍历集合(collection,对象也是一种集合)的,所以,对象也是可以用forEach遍历的,其接收两个参数:

  • collection,遍历的集合
  • iteratee,迭代函数,接收三个参数(value, index|key, collection),跟原生forEach迭代函数接收的三个参数一致

用法

_.forEach([1,2], (value) => console.log(value))
//  => Logs `1` then `2`.

forEach({ 'a': 1, 'b': 2 }, (value, key) => console.log(key))
// => Logs 'a' then 'b' 

forEach({ 'a': 1, 'b': 2 }, (value, key, collection) => console.log(collection))
// => Logs { 'a': 1, 'b': 2 } then { 'a': 1, 'b': 2 }

源码解析

至于lodash的forEach为什么既能遍历数组,又能遍历对象呢,我们看下源码内部就知道了:

function forEach(collection, iteratee) {
  const func = Array.isArray(collection) ? arrayEach : baseEach
  return func(collection, iteratee)
}

在lodash的中的forEach函数,代码很简单,就只有两行,首先判断了传入的collection是否是个数组,如果是数组,就返回arrayEach,否则就返回baseEach,这里就是为什么forEach既能遍历数组,又能遍历对象的原因,因为数组和对象是区分开使用不同的方法的,接下来我们先看下数组是怎么处理的,也就是arrayEach方法内部的代码:

处理数组

function arrayEach(array, iteratee) {
  let index = -1
  const length = array.length

  while (++index < length) {
    if (iteratee(array[index], index, array) === false) {
      break
    }
  }
  return array
}

arrayEach内部也很简单,整体上是一个while循环,因为while循环条件是使用的 ++index判断,所以第一次判断的时候就是从 0 开始判断的,循环次数没有问题
while循环内部,只有一个条件判断,也就是我们传入的迭代函数如果显示的返回了false,那么循环就会被打断,如果不满足if条件,那么就一直循环到while条件不满足
因为每次循环都会执行if的条件判断,所以每次循环都会调用一遍iteratee(迭代函数),传入的三个参数分别为,array[index](当前循环项)、index(当前循环的数组下标)、array(目标数组),这里调用迭代函数时传入的三个参数,就是我们前面介绍lodash时所说的迭代函数的三个参数,正好对应上
那如果传入的是对象等其他集合呢?下面看看baseEach方法的代码

处理非数组集合

function baseEach(collection, iteratee) {
  if (collection == null) {
    return collection
  }
  if (!isArrayLike(collection)) {
    return baseForOwn(collection, iteratee)
  }
  const length = collection.length
  const iterable = Object(collection)
  let index = -1

  while (++index < length) {
    if (iteratee(iterable[index], index, iterable) === false) {
      break
    }
  }
  return collection
}
  • 可以看到在baseEach内部,首先判断了传入的collection如果为null或则undefined,就直接返回collection,不进行迭代
  • 如果collection满足isArrayLike,则返回baseForOwn(collection, iteratee)的执行结果,isArrayLike我们就不在深入了,lodash基础函数库的一个方法,前面的文章中有解读过,就是判断目标元素是否是数组或则类数组的,我们直接看baseForOwn方法: 在baseForOwn方法内部,没有任何东西,只是在传入的collection存在的情况下,返回baseFor(object, iteratee, keys)函数的执行结果,这里就又涉及到两个函数:
  • baseFor:遍历非数组集合的主体函数
  • keys:就是一个获取目标元素key值集合的函数 先看下keys函数

keys函数内部又对目标元素进行了类型区分,如果是类数组或则数组,就调用arrayLikeKeys函数,如果不是,就是用Object.keys(Object(object))来取元素的键(这里我们就先看成使用Object.keys(object)来取键,后面我们在详细说一下加一个Object方法和不加的区别)

数组或则类数组是如何提取其键

arrayLikeKeys方法:

const skipIndexes = isArr || isArg || isBuff || isType
const length = value.length
const result = new Array(skipIndexes ? length : 0)
let index = skipIndexes ? -1 : length
while (++index < length) {
    result[index] = `${index}`
}

在函数内部,isArr等参数我们不用取过多的关注,从其命名就能知道其就是一个表示目标元素是否是数组的变量

  • skipIndexes:元素是否是数组、函数argumentsBuffer对象、TypeArray
  • length:目标元素的length属性
  • result:初始值为一个长度为length或则0的数组
  • index:初始值为-1或则lengthwhile循环,什么时候会执行while循环呢?就是在目标元素是数组、函数argumentsBuffer对象、TypeArray这四种类型的时候会执行while循环,然后为result填充值(0、1、2...、length)
    如果未执行while循环,result就是一个长度为0的空数组
for (const key in value) {
    // key是自身的元素,且并不是0,1,2这些类似index的元素,也不是length属性,就往result末尾添加key
    if ((inherited || hasOwnProperty.call(value, key)) &&
        !(skipIndexes && (
        // 因为buffer、函数arguments等是属于类数组,所以其具有length属性,并且可能是可枚举的
          (key === 'length' ||
           isIndex(key, length))
        ))) {
      result.push(key)
    }
}

while循环之后,就是一个for in循环,我们都知道for in是既可以遍历数组,也可以遍历对象的(类数组本质上是一个对象),而且会遍历其原型链上的值,所以在循环内部有使用hasOwnProperty来判断是否是元素自身的属性,inherited参数在前面调用arrayLikeKeys方法的时候并没有传,条件判断时是使用的||逻辑运算符,所以这里就不用看这个参数了
isIndex方法可以不用看,就是判断元素是否是一个index的方法,length不为0且为数字或则数字字符串时返回true
综合来看:

  • 当目标元素是数组、函数argumentsBuffer对象、TypeArray等类型时,result就是0-自身长度加上其自身除掉length之外的所有属性的集合
  • 目标元素是类数组,但不是函数argumentsBuffer对象、TypeArray等类型时,result就是其自身除掉length之外的所有属性的集合 keys方法的作用介绍完毕,下面就看下baseFor函数的作用,在baseForOwn中调用时:baseFor(object, iteratee, keys)
  • object:目标元素
  • iteratee:迭代函数
  • keys:前面介绍的keys函数,是用来获取元素的keys集合的,返回的是一个数组
function baseFor(object, iteratee, keysFunc) {
  const iterable = Object(object)
  const props = keysFunc(object)
  let { length } = props
  let index = -1

  while (length--) {
    const key = props[++index]
    if (iteratee(iterable[key], key, iterable) === false) {
      break
    }
  }
  return object
}
  • iterable:这里又出现了Object(object)的写法,还是先当成其就是复制了一份object
  • props:前面已经介绍过keys函数,也就是这里的keysFunc,返回值为一个keys的集合集,是一个数组
  • length:props的长度 接着看while循环体,跟前面遍历数组时的while循环类似,同样是当迭代函数显示的返回false时,打断循环,否则,循环调用iteratee(iterable[key], key, iterable),循环次数为length
  • iterable[key]:遍历集合的遍历到的当前的属性
  • key:遍历集合的键
  • iterable:当前遍历的集合、

总结

  • forEach遍历数组时,直接使用while循环点用迭代函数,当迭代函数显式的返回false时,循环结束,否则,遍历次数就是数组长度
  • forEach遍历对象时,直接使用for in再结合hasOwnProperty提取出其自身的属性(也就是键)集合,在根据这个集合的长度来进行遍历,同样,迭代函数显式的返回false将打断循环
  • forEach遍历arguments等类数组时,先组合一个自身length属性长度的数组result,从0开始填充(类似于数组的下标),然后在跟处理对象一样的操作,将key pushresult中并返回,后面的操作就跟处理对象时一模一样了 值得注意到是,因为for in遍历会存在顺序改变的问题,所以使用时需要注意

tips

lodash源码中曾多次出现Object(object),这样的代码,其有什么作用呢?
其实际作用跟 new Object类似,会返回Object包装类的实例,例:

const obj  = Object({ name:"tcc"});//与加new等效
console.log(obj instanceof Object);//true
console.log(obj.name);//tcc

const ostr = Object("tcc");//Object构造函数也会像工厂方法一样,根据传入的值的类型返回相应的基本包装类型的实例
ostr.age = 18;
console.log(ostr instanceof String);//true,传入的是一个字符串,所以返回的是一个Stirng包装类型的一个对象,其实是一个引用数据类型,跟 new String()效果一样
console.log(ostr.age);//18  可以存数据

const str = String("tcc");//这是转型函数,其它的还有Nmber()、 Boolean()、 Array()
const str = new String("tcc");//这是才是引用类型

类似的还有:

const obj = Object(null);  // {}
const obj1 = Object(undefined);   {}