ESC

Type to search the knowledge base.

Array.forEach Polyfill

Implement Array.prototype.forEach: visit present elements, skip holes, ignore the callback return, and honor thisArg without using native forEach.

beginner5 min read
  • javascript
  • arrays
  • polyfill
  • interview

A forEach that for...ofs the array and pushes the callback’s return into a result looks like map. Native Array.forEach does not return that array. It returns undefined. The callback’s return is discarded. The other miss is walking holes: for...of and spread yield undefined for empty slots and you will invoke the callback on a slot native forEach skips. k in O is the presence check, the same HasProperty map uses.

Write a standalone myForEach(arrayLike, callback, thisArg). Putting it on Array.prototype is interview convention and mutates a shared builtin.

Key takeaways

  • Capture length once. Indexes at or past that length are not visited, even if the callback pushes.
  • Call the callback only when k in O. Holes skip. Present undefined does not.
  • Ignore the callback return. forEach itself returns undefined.
  • Callback args are (value, index, array), with thisArg via call.
  • find is a different walk: it uses Get, so holes run as undefined. Do not copy that here.

Visit present elements, skip holes, ignore the return

Spec shape: ToObject, LengthOfArrayLike, then for k from 0 to len - 1, HasProperty then Get then Call. There is no result array. There is no break. A thrown error leaves the loop; a returned value does not.

function myForEach(arrayLike, callback, thisArg) {
  if (arrayLike == null) {
    throw new TypeError('myForEach called on null or undefined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }

  const O = Object(arrayLike);
  const len = O.length >>> 0;

  for (let k = 0; k < len; k++) {
    if (k in O) {
      callback.call(thisArg, O[k], k, O);
    }
  }
}

const seen = [];
myForEach([1, , 3], function (value, index) {
  seen.push(index + ':' + value);
  return value * 100; // discarded
});
seen; // ['0:1', '2:3']

k in O is true for an own index and for a rare inherited index. It is false for a hole. Object.hasOwn(O, k) is own-only. Prefer in if you are matching the spec.

>>> 0 is the old ToUint32 stand-in. Strings and { 0: 'a', 1: 'b', length: 2 } work because you used Object and length, not Array.isArray. for...of would skip those array-likes unless they implemented an iterator.

Length is a snapshot, values are live

const growing = [10, 20];
myForEach(growing, function (value, index, arr) {
  if (index === 0) arr.push(30);
});
// 30 was appended after length was captured. No callback for it.

const shrinking = [10, 20, 30];
myForEach(shrinking, function (value, index, arr) {
  if (index === 0) arr.pop();
});
// original length is 3. Index 2 is now a hole (or gone). HasProperty is false. Skipped.

That is why you store len up front and still Get inside the loop. You do not snapshot the values. You do not re-read O.length each iteration.

thisArg and the discarded return

const tally = { total: 0 };
myForEach(
  [2, , 5],
  function (value) {
    this.total += value;
  },
  tally,
);
tally.total; // 7, hole skipped

If you write callback(O[k], k, O) without call, methods that read this break. Arrows ignore thisArg either way, so a suite that only passes arrows will green-light a wrong polyfill.

The third argument is the same object O, not a copy. A later callback sees live writes. That is defined; it is also how people accidentally skip or double-process indexes. Prefer not to mutate the list you are walking.

There is no myForEach(...).map. The return is undefined. An empty array-like is a successful no-op: zero callback calls, still undefined. A missing callback is a TypeError, not a silent skip. To transform, use map. To keep some values, use filter. To search, use find, which does not skip holes.

Footguns in an Array.forEach polyfill

Situation Native forEach Broken polyfill
[1, , 3] callback at 0 and 2 for...of also calls at the hole
Callback returns false ignored, loop continues treated like every, stops
Callback pushes new tail not visited O.length re-read, extra calls
Present undefined callback runs skipped because == null
Array-like {0,1,length} works for...of throws

delete arr[i] makes a hole. arr[i] = undefined does not. Presence is in, not === undefined.

You cannot break out of forEach. return inside the callback only returns from the callback. Throwing is the only abort, and it is not a control-flow tool you should recommend.

Assigning Array.prototype.myForEach = function (cb, thisArg) { return myForEach(this, cb, thisArg); } mutates a builtin. The standalone function is enough.

This page is the spec walk. For when app code should use forEach versus map versus a for loop, see array map, filter, reduce.

Interview answer (out loud)

“I ToObject the receiver, capture length, and for each index I use in. If the slot is present I call the callback with thisArg, value, index, and the object. I throw away the callback’s return and I return undefined from forEach itself. Holes are skipped. Growing the array during the walk does not visit the new tail because length was captured. I would not use for…of, and I would not confuse this with find, which reads holes as undefined.”

Further reading

Related guides