介绍
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:元素是否是数组、函数
arguments、Buffer对象、TypeArray - length:目标元素的
length属性 - result:初始值为一个长度为
length或则0的数组 - index:初始值为-1或则
length看while循环,什么时候会执行while循环呢?就是在目标元素是数组、函数arguments、Buffer对象、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
综合来看:
- 当目标元素是数组、函数
arguments、Buffer对象、TypeArray等类型时,result就是0-自身长度加上其自身除掉length之外的所有属性的集合 - 目标元素是类数组,但不是函数
arguments、Buffer对象、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 push到result中并返回,后面的操作就跟处理对象时一模一样了 值得注意到是,因为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); {}