Thinking|你不知道的 forEach(javascript)

Thinking系列,旨在利用10分钟的时间传达一种可落地的编程思想或解决方案。
Array.prototype.forEach ( callbackfn [ , thisArg ] )

规范地址(下述引用文,均源自该规范):https://tc39.es/ecma262/#sec-array.prototype.foreach
跳过不存在的元素
callbackfn 只对数组中实际存在的元素调用;数组中缺少元素时不调用该函数。
forEach calls callbackfn once for each element present in the array, in ascending order. callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.
let ary = [3, undefined, 2, , 1] console.log(ary.length) // 5 ary.forEach(i => {console.log(`execute ${ i}`) })// 输出结果: execute 3 execute undefined execute 2 execute 1

元素值为 undefined,callbackfn 正常执行;元素值缺失,callbackfn 直接跳过。
callbackfn 中新增加的元素不会被处理
在 forEach 调用开始后,追加到数组中的元素将不会被 callbackfn 访问。
Elements which are appended to the array after the call to forEach begins will not be visited by callbackfn.
ary = [1, 2] ary.forEach(i => {console.log(`execute ${ i}`) ary.push(ary.length) }) console.log(ary.length) // 4// 输出结果: execute 1 execute 2

执行了2次,但 ary 最终变成了 [1, 2, 2, 3]
callbackfn 中变更元素
① 如果数组中已有的元素被改变了,它们传递给 callbackfn 的值将是 forEach 访问它们时的值。
If existing elements of the array are changed, their value as passed to callbackfn will be the value at the time forEach visits them;
ary = [1, 2] ary.forEach(i => {console.log(`execute ${ i}`) ary[1] = 3 })// 输出结果: execute 1 execute 3

执行输出结果为 1 3callbackfn 获取的值为实时访问的值(修改后的值)。
② 在开始调用 forEach 之后和访问之前被删除的元素不会被访问。
elements that are deleted after the call to forEach begins and before being visited are not visited.
ary = [1, 2] ary.forEach(i => {console.log(`execute ${ i}`) delete ary[1] })// 输出结果: execute 1

执行输出结果为 1callbackfn 中删除的元素不再被访问。
终止执行
【Thinking|你不知道的 forEach(javascript)】forEach 中用 return 不会返回,函数会继续执行。
ary = [1, 2]; ary.forEach(i => {console.log(`execute ${ i}`) return; //无效 })// 输出结果: execute 1 execute 2

return 并不会停止执行后续的操作。
原因: 仔细查看就会发现,return 结束的是当前 callbackfn ,并不是 forEach 函数本身。
解决方案:
① 使用 try 监视代码块,在需要中断的地方抛出异常;
② 官方推荐方法(替换方法),用 everysome 替代 forEach函数 – every 在碰到 return false 的时候,中止循环;some在碰到 return true 的时候,中止循环。
ary = [1, 2]; try {ary.forEach(i => {if (i === 2) throw new Error('终止执行') console.log(`execute ${ i}`) }) } catch (e) { } // or ary.every(i => {if (i === 2) return false console.log(`execute ${ i}`) })// 输出结果: execute 1

【重点】异步执行
模拟异步函数
function asyncFn (num) {return new Promise((resolve, reject) => {setTimeout(() => resolve(num), 1000*num) }) }

存在数组 const ary = [3, 2, 1] ,期望按照顺序输出 3 2 1
ary.forEach(async num => {let res = await asyncFn(num) console.log(res) }) // 输出结果:1 2 3

ECMA262规范:
Thinking|你不知道的 forEach(javascript)
文章图片

for (let k = 0, len = ary.length; k < len; k++) {if (k in ary) {let ele = ary[k] asyncFn(k) // callbackfn(ele, k, ary) } }

callbackfn 的执行无法保证顺序(异步),所以会导致上述问题。
解决方案: 使用 for...of
for(let num of ary) {let res = await asyncFn(num) console.log(res) } // 输出结果:3 2 1

for...of 并不是简单的遍历执行,而是通过迭代器去遍历 Array.prototype[Symbol.iterator]()
规范地址:https://tc39.es/ecma262/#sec-for-in-and-for-of-statements
let iterators = ary[Symbol.iterator]() iterators.next() // {value: 3, done: false}

for...of 的实现
let iterators = ary[Symbol.iterator]() let res = iterators.next() while (!res.done) {let value = https://www.it610.com/article/res.value await asyncFn(value) res = iterators.next() }

执行完成当前值,才会调用下一个 next
再延伸一下,生成器 yield 也是迭代器
实现斐波那契
function* fibonacci(){let [prev, cur] = [0, 1] while (true) {[prev, cur] = [cur, prev + cur] yield cur } }for (let i of fibonacci()) {if (i > 50) break console.log(i) }

  • https://juejin.cn/post/6844903986479251464#heading-5
  • https://juejin.cn/post/6844904004007247880#heading-70

    推荐阅读