Array.reduce Polyfill
Implement reduce with and without an initial value. Empty array TypeError, holes, left-fold order, and why skip-first-present-element is easy to get wrong.
- javascript
- arrays
- polyfill
- interview
[].reduce((a, b) => a + b) throws TypeError. [].reduce((a, b) => a + b, 0) is 0. A polyfill that seeds with arr[0] without asking “was there an initial value?” explodes on empty, and a polyfill that always seeds with arr[0] skips the first present element when the caller did pass 0. Array.reduce is a left fold with two entry points. Holes are skipped in both.
The subtle miss is “no initial value means arr[0].” No. It means the first present element. On [ , , 5, 7] the seed is 5 at index 2, and the callback then runs once with 7 at index 3.
Standalone myReduce(arrayLike, callback, initialValue). Detect the third argument with arguments.length (or a rest flag). reduce has no thisArg; the callback is called with this as undefined.
Key takeaways
- Empty array-like plus no initial value is
TypeError. Empty plus initial value returns the initial value.- No initial value: skip leading holes, take the first present element as the seed, continue after it.
- With initial value: start at index 0, still skip holes, never skip a present first element.
- Callback is
(acc, value, index, array). NothisArg.undefinedas initial value is present. Usearguments.length, notinitialValue === undefined.
No initial value means the first present element is the seed
function myReduce(arrayLike, callback, initialValue) {
if (arrayLike == null) {
throw new TypeError('myReduce 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;
const hasInit = arguments.length >= 3;
let k = 0;
let acc;
if (hasInit) {
acc = initialValue;
} else {
while (k < len && !(k in O)) k += 1;
if (k >= len) {
throw new TypeError('Reduce of empty array with no initial value');
}
acc = O[k];
k += 1;
}
while (k < len) {
if (k in O) {
acc = callback(acc, O[k], k, O);
}
k += 1;
}
return acc;
}
The while that skips holes before seeding is the whole interview. if (!hasInit) { acc = O[0]; k = 1; } is the wrong shortcut: it treats a leading hole as undefined seed, or it throws only when length === 0, missing [ , , ] (length 3, no present elements), which must also throw.
hasInit is arguments.length >= 3, not a default parameter. myReduce(arr, fn, undefined) must use undefined as the seed and still visit index 0.
myReduce([1, 2, 3], (a, b) => a + b); // 6, seed 1, visits 2 then 3
myReduce([1, 2, 3], (a, b) => a + b, 0); // 6, seed 0, visits 1, 2, 3
myReduce([1, 2, 3], (a, b) => a + b, undefined); // NaN if you add, seed undefined
myReduce([], (a, b) => a + b, 0); // 0
// myReduce([], (a, b) => a + b); // TypeError
myReduce([, , 5, 7], (a, b) => a + b); // 12
myReduce([, , 5, 7], (a, b) => a + b, 0); // 12, callback also sees 5
On [ , , 5, 7] without init, the callback’s first call is (5, 7, 3, array). Index 3, not 1. If you start k at 1 after taking O[0], you invent a seed of undefined and you run the callback on 5 and 7 with the wrong accumulator.
With init 0 on the same array, the callback runs for 5 and for 7. The holes still never invoke the callback. That is the same HasProperty rule as map and filter.
Procedure
- Reject nullish receiver and non-function callback.
O = Object(arrayLike),len = ToUint32(length).- If an initial value was passed,
acc = initialValue,k = 0. - Else advance
kto the firstk in O. If none,TypeError. Elseacc = O[k++]. - While
k < len: if present,acc = callback(acc, O[k], k, O); alwaysk++. - Return
acc.
Left fold means you do not walk from the end (reduceRight is a different method). Order of present elements is index order.
reduce does not take thisArg. callback.call(undefined, acc, value, k, O) is the spec Call. Writing callback.call(thisArg, ...) is copying map by accident.
Footguns in an Array.reduce polyfill
[undefined].reduce(fn) does not throw. There is a present element. [ , ].reduce(fn) does throw: length 1, hole, no seed.
Passing 0 as init is the correct sum pattern. Forgetting it on a possibly empty list is how production throws after a filter emptied the array. Prefer always passing init in app code; still implement the no-init path for the interview.
Building objects with { ...acc, [k]: v } inside reduce is O(n²) copies. Mention if they ask you to group. The groupBy page is the dedicated helper.
Do not skip the first present element when init is missing and you already used it as acc. Double-visiting O[k] after seeding is the dual bug: you add 5 twice on [5, 7].
for...of plus a boolean first flag treats holes as undefined values and will use undefined as a seed on a leading hole. Indexed walk only.
Prototype assignment mutates a builtin. Prefer myReduce(arr, fn, init).
Interview answer (out loud)
“If the caller passed an initial value, that is the seed and I still skip holes from index 0. If they did not, I skip leading holes and use the first present element as the seed, then continue after it. An array with no present elements and no init throws TypeError. I detect init with arguments.length so undefined is a real seed. reduce has no thisArg. I would not write acc = arr[0]; k = 1 on a sparse list.”
Further reading
Related on this site
- Array.map polyfill
- Array.filter polyfill
- Array map, filter, reduce
- Polyfill questions playbook
- JavaScript interview learning path
- Promises
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.forEach PolyfillImplement Array.prototype.forEach: visit present elements, skip holes, ignore the callback return, and honor thisArg without using native forEach.
- Array.includes PolyfillImplement Array.prototype.includes with SameValueZero: NaN is found, +0 equals -0, and fromIndex can walk backwards from the end.