数组是出现频率最高的数据结构之一, 而遍历是对数组做的最多的操作. 可以用来对进行遍历的函数有很多, 而且每个函数都有各自的适用情景, 要做根据不同的需求中选择最合理的函数, 必须先对这些函数各自的特点有所了解.
本文涉及的数组遍历相关的函数
首先将文中所讨论的所有方法列出, 以便有整体印象. 这些方法可以分为两种, 分别是:
-
数组的遍历方法, 共 7 个:
forEach()
map()
every()
some()
filter()
find()
findIndex()
-
数组的归并方法, 共 2 个:
reduce()
reduceRight()
下面依次讨论并比较所列出方法各自的功能和特点.
数组的遍历方法
上面提到的 7 种数组的遍历方法都接受 2 个参数 (callback, thisArg):
-
第一个是回调函数
callback
, 会对数组的每一个元素都执行这个函数. 回调函数接受三个参数: 正在处理的当前元素( currentElement )、正在处理的当前元素的索引( currentIndex )、当前正在被操作的数组( currentArray ). -
第二个参数是
thisArg
, 是给callback
函数指定的 this. 这个参数是可选的, 如果不指定, 则默认是undefined
.
下面具体讨论每个方法的特点.
forEach()
函数
这个函数是比较常见的, 它不返回任何值, 只对数组的每个元素都执行回调函数. 就像上面说的, 回调函数的参数是数组里的元素、索引和这个数组本身, 例如访问数组的每一个元素和索引:
let array = ['a', 'b', 'c'];
array.forEach(function(curElement, curIndex, curArray){
console.log('索引为 ' + curIndex + ' 的元素值是 ' + curElement);
});
// 索引为 0 的元素值是 a
// 索引为 1 的元素值是 b
// 索引为 2 的元素值是 c
复制代码
几个需要注意的情况
- 当数组的某个位置没有值时, 则这个位置会被跳过. 但值是
undefined
和null
的位置不会被当做空, 例如:
// 设置数组的第二个元素为空, 第 3、 4 个元素是 null 和 undefined
let array = ['a', , null, undefined, 'c'];
array.forEach(function(curElement, curIndex, curArray){ // forEach 遍历这个数组
console.log('索引为 ' + curIndex + ' 的元素值是 ' + curElement);
});
/* 输出的结果中表明跳过了 1 位置的元素, 但是并没有 跳过值为 undefined 和 null 的位置
索引为 0 的元素值是 a
索引为 2 的元素值是 null
索引为 3 的元素值是 undefined
索引为 4 的元素值是 c
*/
复制代码
- 遍历过程中没有办法中止或者跳出 forEach() 循环, 除了通过抛出一个异常来退出。 如果确实需要这样做, 那么使用 forEach() 并不是合适的方法, 其他几种遍历方法中有非常适合这个需求的.
map()
函数
map() 方法对数组的每个元素都运行回调函数, 然后用回调函数的返回值组成一个新数组返回. 也就是说 map()
方法的回调函数需要明确指定返回值是什么. 这就和上面的 forEach()
方法不同了: forEach()
的回调函数并不显式的返回任何值.
通过上面的描述可以知道
map()
方法一般用于对数组的每个元素进行二次加工(映射), 但是又不希望去改变原来数组, 所以返回一个新的数组.
例如, 想得到数组中的元素都乘上2之后的结果, 使用 map 方法就很合适了:
let oldArr = [1, 2, 3];
let newArr = oldArr.map(function(curElement){
return curElement * 2;
});
console.log(newArr);
// Array(3) [2, 4, 6], 可以看到 newArr 中的元素都是 oldArr 的元素乘上2之后的结果
console.log(oldArr);
// Array(3) [1, 2, 3] , 原来的数组并没有被修改
复制代码
或者求每个元素值的平方根
let numbers = [1, 4, 9];
let roots = numbers.map(Math.sqrt); // 将自带的函数作为回调函数传给 map
// roots的值为[1, 2, 3], numbers的值仍为[1, 4, 9]
复制代码
注意: 回调函数的三个参数(curElement, curIndex, curArray)中只有第一个参数 curElement 是必须要指定的, 其他两个参数可以不指定, 但是实际上依然会被传入回调函数中. 这听起来很绕, 可以通过下面的一个在网上流传很广的题目来了解这个知识点.
一道关于 map
函数的著名题目
经常在网上见到这个题目露面, 而且答案看起来很诡异, 不过通过这个题目可以了解到 map
函数的一个知识点.
题目是: 下面这段代码会输出什么
console.log(["1", "2", "3"].map(parseInt));
复制代码
答案是并不是想象中的 [1, 2, 3]
, 而是 [1, NaN, NaN]
. 下面来分析答案为什么是这个.
先明确代码["1", "2", "3"].map(parseInt)
中的主角都有哪些, 从后向前看:
-
parseInt
函数: 这个函数的作用是解析出来一个字符串中的整数, 它接受两个参数: 要解析的字符串和基数, 这个基数表示想要以哪个进制来解析这个字符串. 基数如果不指定, 则默认为 10, 即按照 十进制 来解析字符串中的整数. 在上面的代码中parseInt
作为 map 的回调函数. -
map()
函数, 它会向回调函数传入三个参数, 即使在我们只指定一个参数名的时候, 另外的两个参数也会隐式的向回调函数中传入. -
["1", "2", "3"]
数组, map 函数的调用者.
在明确了三个主角之后, 下面要做的就是理清主角之间的关系: 数组 --调用--> map函数, parseInt 作为 map 的回调函数被调用.
关键点来了, map
会向回调函数 parseInt
传入三个参数: 当前元素,当前索引和当前数组. 那么这时的 parseInt
函数就可以看做:
parseInt(curElement, curIndex, curArray)
复制代码
然而 parseInt
最多接受两个参数, 那么第三个参数(当前数组)就被忽略了 . 于是 parseInt 最终变成 parseInt(curElement, curIndex)
这种调用形式.
这样整个题目就变成了:
console.log(["1", "2", "3"].map(parseInt(curElement, curIndex)));
复制代码
每个元素的 curIndex 作为基数. 将循环拆开, 可以依次看出 parseInt 对每个元素进行的操作:
[ parseInt('1', 0), parseInt('2', 1), parseInt('3', 2)];
// [1 NaN NaN]
复制代码
通过以上就可以理解这个题目的答案是怎么来的了. 当然还要了解关于 parseInt 函数的机制, 这是另外一个话题, 再讨论起来就偏题了. 具体参见 MDN
顾名思义的 every()
和 some()
函数
这两个函数都用回调函数检测数组中的元素是否满足给定的条件, 返回一个布尔值.
对于 every()
来说, 只有每个元素都满足回调函数中定义的条件, 才会返回 true, 否则返回 false.
而对于 some()
来说, 只要有一个元素满足条件, 函数就会返回 true, 否则返回 false.
例如, 检测一个数组中是否所有数值都 > 10:
let arr = [20, 30, 40]; // 定义一个元素都 > 10 的数组
let boolValue = arr.every(function(curElement, curIndex, curArray){
return curElement > 10; // 回调函数返回当前元素是否满足 > 10 的布尔值
});
console.log(boolValue); // true
复制代码
注意: 空数组调用every
这个函数会返回 true.
再例如, 检测一个数组中是不是存在 > 10 的元素:
let arr = [2,3,40];
let boolValue = arr.some(function(curElement, curIndex, curArray){
return curElement > 10; // 回调函数返回当前元素是否满足 > 10 的布尔值
});
console.log(boolValue); // true
复制代码
注意1: 空数组调用some
这个函数会返回 false.
注意2: some
会从前往后遍历数组, 一旦找到一个满足条件的元素, 就会返回 true, 停止遍历, 并不会再遍历后面的数组. 验证代码如下:
[2, 3, 40, 50].some(function(curElement, curIndex, curArray){
console.log(curIndex); // 当前元素的 位置
return curElement > 2; // 返回当前元素是否满足 > 10 这个条件
});
// 输出: 0 1 , 可以看到只访问到了 1 位置, 由于 1 位置的元素 3 > 2, 所以就不向后继续遍历了
复制代码
从上面的输出中可以知道 some
在遍历到第 1 个位置时找到了满足条件的数组项, 函数就停止了执行, 不再遍历之后的数组.
同样顾名思义的 filter()
函数
filter 有过滤的意思, 顾名思义, 这个函数会返回满足回调函数的所有元素所组成的新数组. 如果所有元素都不满足, 则返回一个 空数组.
例如, 返回数组中所有 > 10 的元素:
let arr = [2, 3, 40]; // 创建只有一个元素 > 10 的数组
let newArr = arr.filter(function(curElement, curIndex, curArray){
return curElement > 10; // 回调函数返回当前元素是否满足 > 10 的布尔值
});
console.log(newArr); // [40], 只有 40 > 10, 则返回的数组中只包含 40
复制代码
数组的查找方法 find()
和 findIndex()
函数
find()
函数返回数组中满足回调函数条件的第一个元素. 如果没有元素满足条件就返回 undefined.
findIndex()
函数返回数组中满足回调函数条件的第一个元素的索引, 如果没有元素满足条件就返回 -1.
例如, 想找到数组中第一个 > 10 的元素用 find
, 想找到这个元素的位置用 findIndex
:
let arr = [2, 3, 40, 50]; // 数组中第一个 > 10 的元素是 40, 索引是 2
let item = arr.find(function(curElement, curIndex, curArray){
return curElement > 10; // 返回当前元素是否满足 > 10 这个条件
});
let index = arr.findIndex(function(curElement, curIndex, curArray){
return curElement > 10; // 返回当前元素是否满足 > 10 这个条件
});
console.log(item); // 40
console.log(index); // 2
复制代码
必须要知道的7个函数的共同点
这 7 种方法遍历数组的过程是按索引依次访问数组每一项的过程. 在开始遍历之前会事先确定数组的长度 len
, 从 0 位置依次忠实的访问到 len - 1 这个位置, 不管数组怎么变化, len 的值就像被 const
定义的一样---直到遍历完成之前永远不变. 然而在遍历的过程中数组本身可能会发生变化, 例如长度变化和元素变化, 可分成以下 3 种情况: 1. 数组的长度不变, 但是其中的元素发生了变化 无论每个元素的值怎么变化, 始终按当前的值为准
2. 数组元素增加的情况, 例如使用 `push` 方法往数组里塞进一个新的元素.
假设遍历之前数组的长度值是 len, 则只会访问到 len - 1 位置, 无论数组增加了多少元素, len - 1 之后的位置都不会被访问到. 以 `forEach` 函数为例来说明, 例如:
```js
let array = ['小x' , '小明', '小红'];
array.forEach(function(curElement, curIndex, curArray){
console.log(curIndex + ' 位置是 ' + curElement);
// 遍历到小明的时候向数组里添加新元素
if(curElement === '小明'){
array.push('小新');
array.push('小新新');
}
});
/* 查看输出的结果 并没有遍历到新加入的元素
0 位置是 小x
1 位置是 小明
2 位置是 小红
*/
// 将数组打印出来观察, 发现其中确实增加了新的元素
console.log(array);
// Array(5) ["小x", "小明", "小红", "小新", "小新新"]
```
所以, 不要尝试在遍历的过程中向数组**后面**添加元素并期待能访问到它们.
3. 数组元素减少
会遍历到数组元素减少后的数组的最后一个位置. 例如数组本来有 n 个元素, 遍历过程中元素个数变成了 n - 1, 则只会遍历到 n - 2 这个位置了.
复制代码
这 7 个函数接受的参数都是相同的: 回调函数和用来指定this值的参数. 但是他们的回调函数接受的参数数量可能会不同, 比如 map
的回调函数可以不指定第二、三个参数.
简单总结上述 7 个方法
forEach
: 无返回值. 对每个元素执行回调函数.map
: 返回一个数组. 每个元素执行回调函数, 返回所有由回调函数结果组成的数组.every
: 返回一个布尔值. 用回调函数提供的条件判断每个元素, 如果所有的元素都满足, 则返回 true, 否则 false.some
: 返回一个布尔值. 从头到尾用回调函数提供的条件判断每个元素, 如果有元素满足条件, 就停止循环, 返回 true, 如果数组中的元素都不满足条件, 返回 false.filter
: 返回一个数组. 数组中是满足条件的元素.find
: 返回一个值. 遍历数组, 返回第一个满足条件的元素值. 如果都不满足, 则返回 undefined.findIndex
: 和find
函数相似, 不过返回的是满足条件元素的索引. 否则返回 -1.
数组的归并方法, reduce()
和 reduceRight()
这两个函数都会遍历数组并返回由回调函数计算出来的值, 不同点仅在于这两个函数遍历数组的方向不同, 前者从数组的第一项遍历到最后一项, 后者从最后一项遍历到第一项. 所以, 这里只讨论 reduce
.
reduce()
reduce()
函数可以指定两个参数, 回调函数 callback
和一个作为初始值的 initValue
.
回调函数接受四个参数, 分别是:
- 当前的累加值: count
- 当前元素: curElement
- 当前索引: curIndex
- 当前的数组: curArray
所以这个函数的完整语法可以写作如下:
reduce(function(count, curElement, curIndex, curArray){}, initValue);
复制代码
其中回调函数的第一个参数( count )的值是在访问上一个元素时的回调函数的返回值. 换句话说, 当前的回调函数的返回值会被赋下一次执行的回调函数的参数 count.
这里可能有 2 个疑惑:
- 在遍历开始之前, count 的值是什么?
- 初始值
initValue
是用来做什么的? 下面的内容会讨论这两个问题.
第二个参数 initValue
对回调函数的影响
reduce 函数中的第二个参数 initValue
是可以省略的. 但省略与否会影响回调函数中前三个参数的初始值. 具体要结合实验来说明如下:
- 不指定参数
initValue
的值时, 回调函数的参数 curIndex = 1, curElement 值为 array[1], count 为 arr[0]:
let arr = [2, 3]; // 创建有两个元素的数组以便观察
arr.reduce(function(count, curElement, curIndex, curArr){
console.log(count, curElement, curIndex); // 2 3 1
});
复制代码
可以看到 count === arr[0], curIndex === 1, curElement === arr[1]. 即从数组的第二个元素开始向后遍历.
- 指定参数
initValue
的值时, 回调函数的参数 curIndex = 0, curElement 值为a[0], count 值为 initValue:
let arr = [2]; // 创建有一个元素的数组以便观察
arr.reduce(function(count, curElement, curIndex, curArr){
console.log(count, curElement, curIndex); // 100 2 0
}, 100); // 指定 initValue 为 100
复制代码
可以看到 count === initValue, curIndex === 0, curElement === arr[0]. 这时才是从数组的第一个元素开始向后遍历.
reduce
函数使用案例
上面的内容提到“ 回调函数的第一个参数( count )的值是在访问上一个元素时的回调函数的返回值. 换句话说, 当前的回调函数的返回值会被赋下一次执行的回调函数的参数 count ”. 这个特点正好可以用来求一个数组所有元素的和, 可以把 count 看做之前所有元素的总和, 把当前的值和 count 加起来就是数组到现在位置的和.
let arr = [1, 2, 3, 4, 5];
let sum = arr.reduce(function(count, curElement, curIndex, curArr){
console.log('数组前 ' + curIndex + ' 个元素的和是: ' + count );
console.log('当前是数组第 ' + (curIndex + 1) + ' 个元素, 值是: ' + curElement);
console.log('加上当前元素后的值是: ' + (count + curElement));
console.log('-------------------------------');
return count + curElement; // 前面所有的和 + 当前的元素值
});
console.log(sum);
/* 输出
数组前 1 个元素的和是: 1
当前是数组第 2 个元素, 值是: 2
加上当前元素后的值是: 3
-------------------------------
数组前 2 个元素的和是: 3
当前是数组第 3 个元素, 值是: 3
加上当前元素后的值是: 6
-------------------------------
数组前 3 个元素的和是: 6
当前是数组第 4 个元素, 值是: 4
加上当前元素后的值是: 10
-------------------------------
数组前 4 个元素的和是: 10
当前是数组第 5 个元素, 值是: 5
加上当前元素后的值是: 15
-------------------------------
数组的和是: 15
*/
复制代码
可以看到回调函数的返回值被赋给了 count 以供下次使用, 最后一个 count 就是整个数组的和.
#完.