forEach (javascript) you don't know

Posted by winmastergames on Mon, 03 Jan 2022 10:33:10 +0100

Thinking series aims to convey a landing programming idea or solution in 10 minutes.

Array.prototype.forEach ( callbackfn [ , thisArg ] )

Specification address (the following references are derived from this specification): https://tc39.es/ecma262/#sec-array.prototype.foreach

Skip non-existent elements

callbackfn only calls the elements that actually exist in the array; This function is not called when an element is missing from the array.

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}`)
})

// Output results:
execute 3
execute undefined
execute 2
execute 1

The element value is undefined, and callback FN is executed normally; Element value is missing, callbackfn skips directly.

Newly added elements in callback FN will not be processed

After the forEach call starts, the elements appended to the array will not be accessed by 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

// Output results:
execute 1
execute 2

It was executed twice, but ary eventually became [1, 2, 2, 3].

Change element in callbackfn

① If the existing elements in the array are changed, the value they pass to callbackfn will be the value when forEach accesses them.

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
})

// Output results:
execute 1
execute 3

The execution output result is 1 3, and the value obtained by callback FN is the value of real-time access (modified value).

② Elements deleted after the start of the call to forEach and before access are not accessed.

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]
})

// Output results:
execute 1

The execution output is 1, and the element deleted in callback FN is no longer accessed.

Termination of execution

Using return in forEach will not return, and the function will continue to execute.

ary = [1, 2];
ary.forEach(i => {
  console.log(`execute ${i}`)
  return;//invalid
})

// Output results:
execute 1
execute 2

return does not stop performing subsequent operations.

Reason: if you look closely, you will find that return ends with the current callbackfn, not the forEach function itself.

Solution:

① Use try to monitor code blocks and throw exceptions where interrupts are needed;

② The official recommended method (replacement method) uses every and some to replace the forEach function - every stops the loop when it encounters return false; some stops the loop when it encounters return true.

ary = [1, 2];
try {
  ary.forEach(i => {
    if (i === 2) throw new Error('Termination of execution')
    console.log(`execute ${i}`)
  })
} catch (e) {}
// or
ary.every(i => {
  if (i === 2) return false
   console.log(`execute ${i}`)
})

// Output results:
execute 1

[key] asynchronous execution

Analog asynchronous function

function asyncFn (num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(num), 1000*num)
  })
}

There is an array const ary = [3,2,1], expecting to output 3,2,1 in order.

ary.forEach(async num => {
  let res = await asyncFn(num)
  console.log(res)
})
// Output result: 1 2 3

ECMA262 specification:

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

The execution of callback FN cannot guarantee the order (asynchrony), so it will lead to the above problems.

Solution: use for of

for(let num of ary) {
  let res = await asyncFn(num)
  console.log(res)
}
// Output result: 3 2 1

for...of is not a simple traversal, but an iterator to traverse the array prototype[Symbol.iterator]().

Specification address: https://tc39.es/ecma262/#sec-for-in-and-for-of-statements

let iterators = ary[Symbol.iterator]()
iterators.next()	// {value: 3, done: false}

for... Implementation of

let iterators = ary[Symbol.iterator]()
let res = iterators.next()
while (!res.done) {
  let value = res.value
  await asyncFn(value)
  res = iterators.next()
}

The next will not be called until the current value is executed.

By extension, the generator yield is also an iterator

Realize Fibonacci

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