介绍
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); {}