ESC

Type to search the knowledge base.

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.

intermediate5 min read
  • 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). No thisArg.
  • undefined as initial value is present. Use arguments.length, not initialValue === 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

  1. Reject nullish receiver and non-function callback.
  2. O = Object(arrayLike), len = ToUint32(length).
  3. If an initial value was passed, acc = initialValue, k = 0.
  4. Else advance k to the first k in O. If none, TypeError. Else acc = O[k++].
  5. While k < len: if present, acc = callback(acc, O[k], k, O); always k++.
  6. 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 guides