数组的遍历及归并方法简要总结 reduce() reduceRight() forEach() map() filter() every() some()等...

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

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

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

  • 数组的遍历方法, 共 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 就是整个数组的和.

#完.

转载于:https://juejin.im/post/5ca4c4da6fb9a05e4c0e6d57

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值