Skip to the code
cf.

Array.prototype.map

Write Array.prototype.myMap(callback, thisArg), which works like the built-in map without calling it. It returns a new array of the same length, where each element is callback(element, index, array), called with thisArg as its this. The original array doesn't change.

Match these details of the built-in. Holes (missing indexes, as in [1, , 3]) are skipped: the callback never sees them, and they stay holes in the result. The length is read once, before the loop, so elements pushed while it runs aren't visited. If callback isn't a function, throw a TypeError, even for an empty array.

[1, 2, 3].myMap((n) => n * 2);  // [2, 4, 6]
[1, , 3].myMap((n) => n * 10);  // [10, empty, 30]
["a", "b"].myMap(function (letter, index) {
  return this.prefix + letter + index;
}, { prefix: "#" });            // ["#a0", "#b1"]

What the tests check

  1. returns a new array of the callback's results
    const numbers = [1, 2, 3];
    const doubled = numbers.myMap((n) => n * 2);
    expect(doubled).toEqual([2, 4, 6]);
    expect(doubled).not.toBe(numbers);
    expect(numbers).toEqual([1, 2, 3]);
    
  2. calls the callback with the element, its index, and the array
    const fn = spy();
    const letters = ["a", "b"];
    letters.myMap(fn);
    expect(fn).toHaveBeenCalledTimes(2);
    expect(fn).toHaveBeenCalledWith("a", 0, letters);
    expect(fn).toHaveBeenCalledWith("b", 1, letters);
    expect(fn.calls[1].args[2]).toBe(letters);
    
  3. uses thisArg as the callback's this
    const context = { factor: 3 };
    const result = [1, 2].myMap(function (n) {
      return n * this.factor;
    }, context);
    expect(result).toEqual([3, 6]);
    
  4. skips holes and keeps them in the result
    const fn = spy((n) => n * 10);
    const result = [1, , 3].myMap(fn);
    expect(fn).toHaveBeenCalledTimes(2);
    expect(result).toHaveLength(3);
    expect(1 in result).toBe(false);
    expect(result).toEqual([10, , 30]);
    
  5. doesn't visit elements added during the loop
    const numbers = [1, 2];
    const result = numbers.myMap((n) => {
      if (numbers.length < 5) numbers.push(n * 10);
      return n;
    });
    expect(result).toEqual([1, 2]);
    
  6. throws a TypeError when callback isn't a function, even for an empty array
    expect(() => [1, 2].myMap("double")).toThrow(TypeError);
    expect(() => [].myMap()).toThrow(TypeError);
    
  7. works without the built-in map
    const numbers = [1, 2];
    const builtIn = Array.prototype.map;
    // Only map on numbers throws, so other code, like console.log, still works.
    Array.prototype.map = function (...args) {
      if (this === numbers) throw new Error("myMap called the built-in map");
      return builtIn.apply(this, args);
    };
    let result;
    try {
      result = numbers.myMap((n) => n + 1);
    } finally {
      Array.prototype.map = builtIn;
    }
    expect(result).toEqual([2, 3]);
    

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).