Skip to the code
cf.

Array.prototype.reduce

MediumPolyfills

Write Array.prototype.myReduce(callback, initialValue), which works like the built-in reduce without calling it. It calls callback(accumulator, element, index, array) for each element, left to right; each call's return value becomes the next accumulator, and the last one is the result.

With an initial value, the first accumulator is that value and the loop starts at index 0. Without one, the first element is the first accumulator and the loop starts after it. Passing undefined counts as passing an initial value: what matters is how many arguments myReduce got.

Holes are skipped, including at the start, when you're looking for the first element. When there's nothing to call the callback on, it isn't called: an empty array returns the initial value, and a single element with no initial value is returned as it is.

Throw a TypeError if callback isn't a function, or if there's no initial value and no element to start from (an empty array, or only holes).

[1, 2, 3].myReduce((sum, n) => sum + n);     // 6
[1, 2, 3].myReduce((sum, n) => sum + n, 10); // 16
[, , 5].myReduce((sum, n) => sum + n);       // 5, without calling back
[].myReduce((sum, n) => sum + n);            // TypeError

What the tests check

  1. reduces left to right from the initial value
    expect(["a", "b", "c"].myReduce((text, letter) => text + letter, ">")).toBe(">abc");
    
  2. passes the accumulator, element, index, and array
    const fn = spy((sum, n) => sum + n);
    const numbers = [5, 6];
    numbers.myReduce(fn, 0);
    expect(fn).toHaveBeenCalledTimes(2);
    expect(fn).toHaveBeenCalledWith(0, 5, 0, numbers);
    expect(fn).toHaveBeenCalledWith(5, 6, 1, numbers);
    expect(fn.calls[0].args[3]).toBe(numbers);
    
  3. starts from the first element when there's no initial value
    const fn = spy((sum, n) => sum + n);
    expect([1, 2, 3].myReduce(fn)).toBe(6);
    expect(fn).toHaveBeenCalledTimes(2);
    expect(fn.calls[0].args.slice(0, 3)).toEqual([1, 2, 1]);
    
  4. treats an initial value of undefined as given
    const fn = spy((accumulator, n) => n);
    [1, 2].myReduce(fn, undefined);
    expect(fn).toHaveBeenCalledTimes(2);
    expect(fn.calls[0].args.slice(0, 3)).toEqual([undefined, 1, 0]);
    
  5. skips holes, even before the first element
    const fromFirst = spy((sum, n) => sum + n);
    expect([, 1, , 2].myReduce(fromFirst)).toBe(3);
    expect(fromFirst).toHaveBeenCalledTimes(1);
    expect(fromFirst.calls[0].args.slice(0, 3)).toEqual([1, 2, 3]);
    const fromInitial = spy((sum, n) => sum + n);
    expect([1, , 3].myReduce(fromInitial, 0)).toBe(4);
    expect(fromInitial).toHaveBeenCalledTimes(2);
    
  6. returns a lone value without calling the callback
    const fn = spy();
    expect([].myReduce(fn, 42)).toBe(42);
    expect([7].myReduce(fn)).toBe(7);
    expect([, 7, ,].myReduce(fn)).toBe(7);
    expect(fn).not.toHaveBeenCalled();
    
  7. throws a TypeError with no initial value and no element
    const add = (a, b) => a + b;
    expect(() => [].myReduce(add)).toThrow(TypeError);
    expect(() => [, ,].myReduce(add)).toThrow(TypeError);
    
  8. throws a TypeError when callback isn't a function, even for an empty array
    expect(() => [1, 2].myReduce("add", 0)).toThrow(TypeError);
    expect(() => [].myReduce(null, 0)).toThrow(TypeError);
    
  9. works without the built-in reduce
    const builtIn = Array.prototype.reduce;
    Array.prototype.reduce = function () {
      throw new Error("myReduce called the built-in reduce");
    };
    let result;
    try {
      result = [1, 2, 3].myReduce((sum, n) => sum + n);
    } finally {
      Array.prototype.reduce = builtIn;
    }
    expect(result).toBe(6);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).