分享

精通Javascript: 函数式array.forEach的8个案例

 学掌门 2022-04-12

JavaScript是当今流行语言中对函数式编程支持最好的编程语言。我们继续构建函数式编程的基础,在前文中分解介绍了帮助我们组织思维的四种方法,分别为:

- array.reduce方法 帮你精通JS:神奇的array.reduce方法的10个案例

- array.map方法 帮你精通JS:神奇的array.map的6个案例

- array.flat方法,以及array.flatMap 帮你精通JS: array.flat与flatMap用法指南

以上四种方法的共同点都是对array作转换和变形,而且都不需要陷入到琐碎loop实现细节的 dirty details之中。

接下来,我们将学习更加通用的函数式迭代方法 array.forEach()。

一句话概括区分 forEach 与 map 的区别,pure-function 就用 map,impure-function 则用 forEach。

array.forEach() 语法概述

forEach() 方法对数组的每个元素执行一次给定的函数。

1. const array1 = ['a''b''c']; 

2.  

3. array1.forEach(element => console.log(element)); 

4.  

5. // expected output"a" 

6. // expected output"b" 

7. // expected output"c" 

 参数

callback

为数组中每个元素执行的函数,该函数接收一至三个参数:

- currentValue 数组中正在处理的当前元素。

- index 可选 数组中正在处理的当前元素的索引。

- array 可选 forEach() 方法正在操作的数组。

thisArg 可选 可选参数。当执行回调函数 callback 时,用作 this 的值。

返回值

undefined。

array.forEach() 描述

forEach() 方法按升序为数组中含有效值的每一项执行一次 callback 函数,那些已删除或者未初始化的项将被跳过(例如在稀疏数组上)。

可依次向 callback 函数传入三个参数:

1. 数组当前项的值

2. 数组当前项的索引

3. 数组对象本身

如果 thisArg 参数有值,则每次 callback 函数被调用时,this 都会指向 thisArg 参数。如果省略了 thisArg 参数,或者其值为 null 或 undefined,this 则指向全局对象。按照函数观察到 this 的常用规则,callback 函数最终可观察到 this 值。

forEach() 遍历的范围在第一次调用 callback 前就会确定。调用 forEach 后添加到数组中的项不会被 callback 访问到。如果已经存在的值被改变,则传递给 callback 的值是 forEach() 遍历到他们那一刻的值。已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了(例如使用 shift()),之后的元素将被跳过——参见下面的示例

forEach() 为每个数组元素执行一次 callback 函数;与 map() 或者 reduce() 不同的是,它总是返回 undefined 值,并且不可链式调用。其典型用例是在一个调用链的最后执行副作用(side effects,函数式编程上,指函数进行 返回结果值 以外的操作)。

forEach() 被调用时,不会改变原数组,也就是调用它的数组(尽管 callback 函数在被调用时可能会改变原数组)。(此处说法可能不够明确,具体可参考EMCA语言规范:'forEach does not directly mutate the object on which it is called but the object may be mutated by the calls to callback function.',即 forEach 不会直接改变调用它的对象,但是那个对象可能会被 callback 函数改变。)

注意 除了抛出异常以外,没有办法中止或跳出 forEach() 循环。如果你需要中止或跳出循环,forEach() 方法不是应当使用的工具。

若你需要提前终止循环,你可以使用:

· 一个简单的 for 循环

· for...of / for...in 循环

· Array.prototype.every()

· Array.prototype.some()

· Array.prototype.find()

· Array.prototype.findIndex()

这些数组方法则可以对数组元素判断,以便确定是否需要继续遍历:

· every()

· some()

· find()

· findIndex()

只要条件允许,也可以使用 filter() 提前过滤出需要遍历的部分,再用 forEach() 处理。

案例 01 不对未初始化的值进行任何操作(稀疏数组)

如你所见,3 和 7 之间空缺的数组单元未被 forEach() 调用 callback 函数,或进行任何其他操作。

1. const arraySparse = [1,3,,7]; 

2. let numCallbackRuns = 0; 

3.  

4. arraySparse.forEach(function(element){ 

5.   console.log(element); 

6.   numCallbackRuns++; 

7. }); 

8.  

9. console.log("numCallbackRuns: ", numCallbackRuns); 

10.  

11. // 1 

12. // 3 

13. // 7 

14. // numCallbackRuns: 3 

 案例 02 将 for 循环转换为 forEach

1. const items = ['item1''item2''item3']; 

2. const copy = []; 

3.  

4. // before 

5. for (let i=0; i<items.length; i++) { 

6.   copy.push(items[i]); 

7. 

8.  

9. // after 

10. items.forEach(function(item){ 

11.   copy.push(item); 

12. }); 

 案例 03 打印出数组的内容

注意:为了在控制台中显示数组的内容,你可以使用 console.table() 来展示经过格式化的数组。下面的例子则是另一种使用 forEach() 的格式化的方法。

下面的代码会为每一个数组元素输出一行记录:

1. function logArrayElements(element, index, array) { 

2.   console.log('a[' + index + '] = ' + element); 

3. 

4.  

5. // 注意索引 2 被跳过了,因为在数组的这个位置没有项 

6. [2, 5, , 9].forEach(logArrayElements); 

7. // logs: 

8. // a[0] = 2 

9. // a[1] = 5 

10. // a[3] = 9 

 案例 04 使用 thisArg

举个勉强的例子,按照每个数组中的元素值,更新一个对象的属性:

1. function Counter() { 

2.   this.sum = 0; 

3.   this.count = 0; 

4. 

5. Counter.prototype.add = function(array) { 

6.   array.forEach(function(entry) { 

7.     this.sum += entry; 

8.     ++this.count

9.   }, this); 

10.   // ^---- Note 

11. }; 

12.  

13. const obj = new Counter(); 

14. obj.add([2, 5, 9]); 

15. obj.count

16. // 3 === (1 + 1 + 1) 

17. obj.sum

18. // 16 === (2 + 5 + 9) 

因为 thisArg 参数(this)传给了 forEach(),每次调用时,它都被传给 callback 函数,作为它的 this 值。

注意:如果使用箭头函数表达式来传入函数参数, thisArg 参数会被忽略,因为箭头函数在词法上绑定了 this 值。

案例 05 对象复制器函数

下面的代码会创建一个给定对象的副本。 创建对象的副本有不同的方法,以下是只是一种方法,并解释了 Array.prototype.forEach() 是如何使用 ECMAScript 5 Object.* 元属性(meta property)函数工作的。

1. function copy(obj) { 

2.   const copy = Object.create(Object.getPrototypeOf(obj)); 

3.   const propNames = Object.getOwnPropertyNames(obj); 

4.  

5.   propNames.forEach(function(name) { 

6.     const desc = Object.getOwnPropertyDescriptor(obj, name); 

7.     Object.defineProperty(copy, namedesc); 

8.   }); 

9.  

10.   return copy; 

11. 

12.  

13. const obj1 = { a: 1, b: 2 }; 

14. const obj2 = copy(obj1); // 现在 obj2 看起来和 obj1 一模一样了 

 案例 06 如果数组在迭代时被修改了,则其他元素会被跳过。

下面的例子会输出 "one", "two", "four"。当到达包含值 "two" 的项时,整个数组的第一个项被移除了,这导致所有剩下的项上移一个位置。因为元素 "four" 正位于在数组更前的位置,所以 "three" 会被跳过。 forEach() 不会在迭代之前创建数组的副本。

1. var words = ['one''two''three''four']; 

2. words.forEach(function(word) { 

3.   console.log(word); 

4.   if (word === 'two') { 

5.     words.shift(); 

6.   } 

7. }); 

8. // one 

9. // two 

10. // four 

 案例 07 扁平化数组

下面的示例仅用于学习目的。如果你想使用内置方法来扁平化数组,你可以考虑使用 Array.prototype.flat()(预计将成为 ES2019 的一部分,并且已在主要浏览器中实现)或参考其 polyfill。

1. /** 

2.  * Flattens passed array in one dimensional array 

3.  * 

4.  * @params {array} arr 

5.  * @returns {array} 

6.  */ 

7. function flatten(arr) { 

8.   const result = []; 

9.  

10.   arr.forEach((i) => { 

11.     if (Array.isArray(i)) 

12.       result.push(...flatten(i)); 

13.     else 

14.       result.push(i); 

15.   }) 

16.  

17.   return result; 

18. 

19.  

20. // Usage 

21. const problem = [1, 2, 3, [4, 5, [6, 7], 8, 9]]; 

22.  

23. flatten(problem); // [1, 2, 3, 4, 5, 6, 7, 8, 9] 

 案例08 针对 promise 或 async 函数的使用备注

如果使用 promise 或 async 函数作为 forEach() 等类似方法的 callback 参数,最好对造成的执行顺序影响多加考虑,否则容易出现错误。

1. let ratings = [5, 4, 5]; 

2.  

3. let sum = 0; 

4.  

5. let sumFunction = async function (a, b) { 

6.     return a + b; 

7. 

8.  

9. ratings.forEach(async function(rating) { 

10.     sum = await sumFunction(sum, rating); 

11. }) 

12.  

13. console.log(sum); 

14. // Expected output: 14 

15. // Actual output: 0 

文章来源:网络  版权归原作者所有

上文内容不用于商业目的,如涉及知识产权问题,请权利人联系小编,我们将立即处理

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多