DEV Community

Derrick Odhiambo
Derrick Odhiambo

Posted on

How reduce() works under the hood

reduce() is an Array method that executes a reducer callback on each element in order and returns a single value.

reducer(callbackFn);
reducer(callbackFn, initialValue);

const array = [1, 2, 3, 4];

// Example: 0 + 1 + 2 + 3 + 4
const initialValue = 0;

const sumWithInitial = array.reduce(
  (accumulator, currentValue) => accumulator + currentValue,
  initialValue,
);

console.log(sumWithInitial);
// Expected output: 10
Enter fullscreen mode Exit fullscreen mode

reduce() calls your function with (accumulator, currentValue, currentIndex, array) on every element. If you don't pass an initialValue, the array's first element becomes the starting accumulator, and the loop begins from index 1 instead of 0 — otherwise, it starts from initialValue at index 0.

On the first call, there's no previous result to build on yet, so accumulator starts as whatever you provided as initialValue (or the array's first element, if you didn't), and callbackFn combines that starting value with currentValue to produce the first real result.

What actually happens under the hood

There are many ways to implement a reduce method, but we'll use the ECMAScript specification as our guide. It breaks the behavior of reduce down into clear, well-defined steps, giving us a solid foundation for implementing it ourselves.

First, a few edge cases worth knowing.

  • An empty array, with or without the initialValue argument.

  • A single-value array, with and without the initialValue argument.

  • Sparse arrays with interior holes, e.g. [ , , , 2, 3]. Empty slots are skipped while traversing.

  • Sparse arrays with no initialValue.

  • All-hole array with no initialValue, e.g., [ , , , ].

  • Index properties inherited through the array's prototype.

  • undefined passed explicitly as an initialValue

Array.prototype.myReducer = function (callbackFn, initialValue) {
  // Reduce requires a real function to call on each element — reject
  // anything else immediately, before doing any work on the array.
  if (typeof callbackFn !== "function") {
    throw new TypeError("This is not a function");
  }

  // Object(this) guarantees O is always a proper object to operate on,
  // even in edge cases where `this` isn't already a genuine array —
  // this is what lets myReducer be "borrowed" and called on array-like
  // objects (e.g. via Array.prototype.myReducer.call(arrayLike, fn)).
  const O = Object(this);

  // `>>> 0` coerces O.length into a sane, non-negative 32-bit unsigned
  // integer, so the loop below always has a safe bound to run against,
  // even if O.length is missing, negative, or not a number at all.
  const len = O.length >>> 0;

  // arguments.length, not initialValue itself, is what tells us whether
  // a second argument was passed. Checking `initialValue !== undefined`
  // instead would incorrectly treat myReduce(fn, undefined) — a
  // deliberate, if unusual, call — as if no initial value were given.
  const noInitialValue = arguments.length < 2;

  // Reducing an empty array with nothing to start from is genuinely
  // undefined behaviour — there's no way to produce a meaningful result,
  // so this matches native reduce() in throwing rather than guessing.
  if (len === 0 && noInitialValue) {
    throw new TypeError("Reduce of empty array with no initial value");
  }

  let k = 0; // current index into the source array
  let accumulator = undefined;

  if (!noInitialValue) {
    // An initial value was supplied — use it as-is, and the main loop
    // below will start from index 0.
    accumulator = initialValue;
  } else {
    // No initial value: the accumulator has to start as the array's
    // first *present* element — not just index 0, since sparse arrays
    // can have holes at the start. We scan forward until we find one.
    let kPresent = false;

    while (kPresent === false && k < len) {
      let propertyKey = String(k);

      // `in` checks whether the property genuinely exists — including
      // via the prototype chain — which correctly distinguishes a real
      // hole (nothing assigned) from an index explicitly set to
      // undefined. A plain O[k] !== undefined check would get this wrong.
      if (propertyKey in O) {
        kPresent = true;
        accumulator = O[k];
      }
      k = k + 1; // advance regardless, whether or not this index was present
    }

    // If we scanned the whole array and never found a present element,
    // every index was a hole — functionally the same as an empty array
    // for reduce's purposes, so the same error applies.
    if (kPresent === false) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
  }

  // Main reduction loop. This runs unconditionally, regardless of which
  // branch above set the accumulator — whether it came from
  // initialValue or from scanning for the first present element, every
  // remaining element still needs to be folded in.
  while (k < len) {
    let propertyKey = String(k);

    // Same hole-check as above: skip indexes that were never assigned,
    // rather than passing `undefined` into the callback for them.
    if (propertyKey in O) {
      let kValue = O[propertyKey];

      // Argument order matters here: (accumulator, currentValue,
      // currentIndex, array) — matching the real callback signature.
      // accumulator is always the FIRST argument; getting this order
      // wrong (e.g., passing kValue first) silently produces incorrect
      // results without throwing any error.
      accumulator = callbackFn(accumulator, kValue, k, O);
    }
    k = k + 1;
  }

  return accumulator;
};
Enter fullscreen mode Exit fullscreen mode

myReduce reproduces the native reduce() with the same argument handling and the same edge cases around missing initial values and sparse arrays.
ECMAScript specification guide

Top comments (0)