几个关于数组遍历的所谓的高阶函数 map等

727 阅读14分钟

数组是出现频率最高的数据结构之一, 而遍历是对数组做的最多的操作. 可以用来对进行遍历的函数有很多, 而且每个函数都有各自的适用情景, 要做根据不同的需求中选择最合理的函数, 必须先对这些函数各自的特点有所了解.

本文涉及的数组遍历相关的函数

首先将文中所讨论的所有方法列出, 以便有整体印象. 这些方法可以分为两种, 分别是:

  • 数组的遍历方法, 共 7 个:

    1. forEach()
    2. map()
    3. every()
    4. some()
    5. filter()
    6. find()
    7. findIndex()
  • 数组的归并方法, 共 2 个:

    1. reduce()
    2. reduceRight()

下面依次讨论并比较所列出方法各自的功能和特点.

数组的遍历方法

上面提到的 7 种数组的遍历方法都接受 2 个参数 (callback, thisArg):

  1. 第一个是回调函数 callback , 会对数组的每一个元素都执行这个函数. 回调函数接受三个参数: 正在处理的当前元素( currentElement )、正在处理的当前元素的索引( currentIndex )、当前正在被操作的数组( currentArray ).

  2. 第二个参数是 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

几个需要注意的情况

  • 当数组的某个位置没有值时, 则这个位置会被跳过. 但值是 undefinednull 的位置不会被当做空, 例如:
// 设置数组的第二个元素为空, 第 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)中的主角都有哪些, 从后向前看:

  1. parseInt 函数: 这个函数的作用是解析出来一个字符串中的整数, 它接受两个参数: 要解析的字符串和基数, 这个基数表示想要以哪个进制来解析这个字符串. 基数如果不指定, 则默认为 10, 即按照 十进制 来解析字符串中的整数. 在上面的代码中 parseInt 作为 map 的回调函数.

  2. map() 函数, 它会向回调函数传入三个参数, 即使在我们只指定一个参数名的时候, 另外的两个参数也会隐式的向回调函数中传入.

  3. ["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 个方法

  1. forEach: 无返回值. 对每个元素执行回调函数.
  2. map: 返回一个数组. 每个元素执行回调函数, 返回所有由回调函数结果组成的数组.
  3. every: 返回一个布尔值. 用回调函数提供的条件判断每个元素, 如果所有的元素都满足, 则返回 true, 否则 false.
  4. some: 返回一个布尔值. 从头到尾用回调函数提供的条件判断每个元素, 如果有元素满足条件, 就停止循环, 返回 true, 如果数组中的元素都不满足条件, 返回 false.
  5. filter: 返回一个数组. 数组中是满足条件的元素.
  6. find: 返回一个值. 遍历数组, 返回第一个满足条件的元素值. 如果都不满足, 则返回 undefined.
  7. findIndex: 和 find 函数相似, 不过返回的是满足条件元素的索引. 否则返回 -1.

数组的归并方法, reduce()reduceRight()

这两个函数都会遍历数组并返回由回调函数计算出来的值, 不同点仅在于这两个函数遍历数组的方向不同, 前者从数组的第一项遍历到最后一项, 后者从最后一项遍历到第一项. 所以, 这里只讨论 reduce.

reduce()

reduce() 函数可以指定两个参数, 回调函数 callback 和一个作为初始值的 initValue.

回调函数接受四个参数, 分别是:

  1. 当前的累加值: count
  2. 当前元素: curElement
  3. 当前索引: curIndex
  4. 当前的数组: curArray

所以这个函数的完整语法可以写作如下:

reduce(function(count, curElement, curIndex, curArray){}, initValue);

其中回调函数的第一个参数( count )的值是在访问上一个元素时的回调函数的返回值. 换句话说, 当前的回调函数的返回值会被赋下一次执行的回调函数的参数 count.

这里可能有 2 个疑惑:

  1. 在遍历开始之前, count 的值是什么?
  2. 初始值 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 就是整个数组的和.

#完.