cf.completefrontend

Write the codeMedium

Implement Array.prototype.map yourself, as myMap.

Before you start

What's a polyfill?

A polyfill is code that adds a standard feature to an environment that doesn't have it yet, such as Promise.any or Array.prototype.flat in an older browser. It's written in plain JavaScript that the old environment can already run, and it follows the specification, the document that defines how the feature behaves, as closely as it can. Code written for new browsers then works in old ones too. The name comes from Polyfilla, a paste for filling cracks in walls: a polyfill fills the gaps in a browser.

A real polyfill checks for the feature first, as in if (!Promise.any), and defines it only when it's missing, so a browser that has the built-in keeps its own version:

if (!Array.prototype.flat) {
  Array.prototype.flat = function flat(depth) {
    // the steps the specification lists, written in plain JavaScript
  };
}

Every array gets its methods from one shared object, Array.prototype, and every function from Function.prototype. So a method added there appears on every array or function in the program, even ones created before it was added, and inside it this is the array or function it was called on. Careful polyfills add it with Object.defineProperty rather than = as above, so that, like the built-ins, it isn't enumerable and doesn't show up in a for...in loop.

When an interviewer asks you to write a polyfill, they mean: reproduce a built-in's behaviour yourself, without calling it. It shows you know exactly what the built-in does, beyond its everyday use. The details are the point, because code written against the standard relies on them too:

Built-inEveryday useDetails a polyfill must match too
Array.prototype.map[1, 2].map((n) => n * 2) is [2, 4]thisArg, holes, and the TypeError for a callback that isn't a function, thrown even when the array is empty
Function.prototype.bindA function with a fixed thisPreset arguments, and calling the result with new
Promise.allWaits for several promisesPlain values among them, an empty input, and an argument that isn't iterable
Object.createAn object that inherits from the one you passnull as the prototype, and a TypeError for one that's neither an object nor null
JSON.stringifyTurns data into textundefined and functions, toJSON, and a TypeError on cycles

These tasks give your version its own name, such as Array.prototype.myMap, Object.myCreate, or JSON.myStringify, or make it a stand-alone function, such as promiseAll(iterable) or objectIs(a, b). Replacing the real method would change it for every other piece of code on the page, and leave you no built-in to compare yours with.

In production you rarely write polyfills by hand: libraries such as core-js provide them, and Babel can add the ones your target browsers need. A polyfill can only add missing objects, functions, and methods. New syntax, such as ?. or class, can't be added that way: a compiler has to rewrite it as older syntax, which is called transpiling.

What's a hole in an array?

An array's length counts slots, and a slot can be empty: no element at all, not even undefined. That empty slot is called a hole, and an array with holes is called sparse. Reading a hole gives undefined, the value you get for any property that doesn't exist, so it looks like an element holding undefined. Object.keys, which lists the indexes an array really has, tells the two apart:

const withHole = [1, , 3]; // nothing between the commas
const withUndefined = [1, undefined, 3];
 
withHole.length;      // 3
withUndefined.length; // 3
withHole[1];          // undefined
withUndefined[1];     // undefined
Object.keys(withHole);      // ["0", "2"]: there's no element at index 1
Object.keys(withUndefined); // ["0", "1", "2"]: there is one, and its value is undefined

Holes appear in a few ways:

HowExampleResult
A gap in an array literal[1, , 3]A hole at index 1
new Array with one numbernew Array(3)Three holes, and length is 3
delete on an elementdelete arr[1]Index 1 becomes a hole, and length doesn't change
Writing past the endarr[5] = 6 on [1, 2, 3]Holes at indexes 3 and 4, and length is 6
Making length biggerarr.length = 5 on [1, 2, 3]Holes at indexes 3 and 4

Consoles mark holes so you can spot them: Chrome shows [1, , 3] as [1, empty, 3], and Node as [ 1, <1 empty item>, 3 ].

Built-in methods don't agree on what to do with a hole: some skip it, some keep it in their result, and some read it as undefined, or write null in JSON. That's why every task that rebuilds one of them says exactly what to do with holes. Here's [1, , 3] going through a few:

CallResultWhat happened to the hole
[1, , 3].filter(() => true)[1, 3]Skipped: the callback isn't called for it, and the result has no hole
[1, , 3].map((n) => n * 10)[10, , 30]Skipped, but kept: the result has a hole in the same place
[1, , 3].reduce((sum, n) => sum + n)4Skipped: 1 starts the total, and the callback runs once, to add 3
[1, , 3].flat()[1, 3]Dropped
Array.from([1, , 3])[1, undefined, 3]Read as undefined, which becomes a real element
JSON.stringify([1, , 3])"[1,null,3]"Written as null

myMap(callback, thisArg) must behave like the built-in map. It calls callback with (element, index, array), with the optional thisArg as its this, and returns a new array of the results.

Skip holes: don't call callback for them, and keep them as holes in the result. Don't visit elements added to the array during the call.

Throw a TypeError if callback isn't a function.

Write this
Array.prototype.myMap = function (callback, thisArg) {
  // your code
};
 
// [1, 2, 3].myMap((n) => n * 2); // [2, 4, 6]
// [1, , 3].myMap((n) => n * 2);  // [2, , 6]: the hole stays a hole
// [1, 2].myMap(function (n) {
//   return n * this.factor;
// }, { factor: 10 });            // [10, 20]

Solve it in the editor, with tests

The answer

Write it yourself first. Then compare it with a reference solution and the checks it passes.

Checking your sign-in…