Array.forEach Polyfill
Implement Array.prototype.forEach: visit present elements, skip holes, ignore the callback return, and honor thisArg without using native forEach.
- 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
lengthonce. Indexes at or past that length are not visited, even if the callbackpushes.- Call the callback only when
k in O. Holes skip. Presentundefineddoes not.- Ignore the callback return.
forEachitself returnsundefined.- Callback args are
(value, index, array), withthisArgviacall.findis a different walk: it usesGet, so holes run asundefined. 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 on this site
- Array.map polyfill
- Array.find polyfill
- Array.filter polyfill
- Array map, filter, reduce
- Functional array patterns
- Polyfill questions playbook
- JavaScript interview learning path
Related guides
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.
- Array.includes PolyfillImplement Array.prototype.includes with SameValueZero: NaN is found, +0 equals -0, and fromIndex can walk backwards from the end.
- Array.map PolyfillImplement Array.prototype.map without cheating: thisArg, sparse holes, callback args, and why indexOf-based loops fail on duplicates and NaN.