completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Polyfill array methods

Write spec-aware JavaScript polyfills for map, filter, reduce, and flat, including sparse arrays, thisArg, array-likes, and safe prototype demos.

By the end, you can
  • 01
    Rebuild the main array methodsWrite your own myMap, myFilter, myReduce, and myFlat as plain functions, without changing Array.prototype.
  • 02
    Handle the tricky casesSkip or keep empty slots correctly, pass thisArg, read the length once, accept array-like objects, and throw the same reduce error.
  • 03
    Test like a polyfill authorRun the real methods and your versions side by side on empty slots, changes during the loop, bad callbacks, and deep nesting.

Why rebuild built-ins?

A built-in is a method that JavaScript already gives you, such as map. A polyfill is code that adds a standard feature to a browser (or another place that runs JavaScript) that doesn't have it yet.

In this lesson, you write your own versions of four array methods:

  • map changes every item.
  • filter keeps some of the items.
  • reduce combines all the items into one value.
  • flat takes items out of the arrays inside an array.

You write each one as a plain function, such as myMap. Writing it yourself shows the small rules that the real method follows without telling you.

This is the first lesson in Build it from scratch. The goal is to understand the real methods, not to replace them. Along the way you will meet these rules:

  • what the callback (the function you pass in) receives,
  • what happens with empty slots in an array,
  • which errors the methods throw,
  • how the official rule book for JavaScript, the spec, writes each step.

Job interviews often ask you to write these functions. One small missing rule shows whether someone knows how the method really behaves.

Use the real methods in your apps

Don't ship these lesson functions in a real app. Use the built-in methods. If you must support old browsers, use a build tool and a tested polyfill package such as core-js. The Transpilers and polyfills lesson shows how that works.

Real-life analogyRebuilding a built-in is like taking apart a bicycle

You understand a bicycle better after you take off the wheel, clean the chain, and adjust the brakes. You see the small parts you use on every ride without noticing.

In real life: Take off the wheel and look at the hub
In JavaScript: Write the loop and look at each callback argument
In real life: See how the brake cable runs
In JavaScript: Handle empty slots, thisArg, errors, and the length
In real life: Put the bicycle back together and ride it
In JavaScript: Run the real method and your version side by side

Read this lesson after map, filter & reduce and Higher-order functions. It also links to Array basics, Iterating arrays, Recursion, Property descriptors, Native prototypes, and Manuals and specs at the places where those ideas come up.

Start with plain functions

REAL FUNCTIONS

Each version is a plain function that takes the array as its first argument. It is not a method on every array. A plain function is easy to test, and it doesn't change anything that other code shares.

Each function starts with the same three steps:

  1. Turn the input into an object. This throws an error for null and undefined.
  2. Read the length once, before the loop.
  3. Throw an error if the callback is not a function, before the loop starts.

The code below holds three small helpers for these steps.

Three helpers that all four functions usePop out in the code editor (opens in a new tab)JavaScript
function toObject(value) {  if (value == null) {    throw new TypeError("Cannot convert undefined or null to object");  }  return Object(value);} function toLength(value) {  const number = +value;  if (!Number.isFinite(number) || number <= 0) return 0;  return Math.min(Math.floor(number), Number.MAX_SAFE_INTEGER);} function toDepth(value) {  const number = +value;  if (number === Infinity) return Infinity;  if (!Number.isFinite(number) || number <= 0) return 0;  return Math.floor(number);}
  • Line 1, toObject, turns the input into an object. A string becomes an object you can read by index. null and undefined throw a TypeError, just like the real methods.
  • Line 8, toLength, turns the length into a whole number. +value turns it into a number first. Anything that is not a positive number becomes 0.
  • Line 14, toDepth, does the same for the depth of flat, but keeps Infinity.

Because of toObject, array methods also work on strings and on array-like objects. An array-like object has a length and numbered keys, like { length: 2, 0: "a", 1: "b" }. Because the length is read once, adding items during the loop doesn't make the loop longer.

Your own map: myMapPop out in the code editor (opens in a new tab)JavaScript
function myMap(arrayLike, callback, thisArg) {  const object = toObject(arrayLike);  const length = toLength(object.length);  if (typeof callback !== "function") {    throw new TypeError(String(callback) + " is not a function");  }  const result = new Array(length);  for (let index = 0; index < length; index += 1) {    if (index in object) {      result[index] = callback.call(thisArg, object[index], index, object);    }  }  return result;}
  • Lines 2 to 5 are the three steps above.
  • Line 7 makes the result array, with the same length as the input.
  • Line 8 goes through each index, from 0 up to the length.
  • Line 9 checks that the index exists. index in object is false for an empty slot, so the callback is skipped there.
  • Line 10 calls the callback with three things: the value, the index, and the whole object. It stores the answer at the same index.

Line 10 uses callback.call(thisArg, ...). The .call() method runs a function and sets what this means inside it. Here this becomes thisArg, an optional last argument you can pass to myMap.

Your own filter: myFilterPop out in the code editor (opens in a new tab)JavaScript
function myFilter(arrayLike, callback, thisArg) {  const object = toObject(arrayLike);  const length = toLength(object.length);  if (typeof callback !== "function") {    throw new TypeError(String(callback) + " is not a function");  }  const result = [];  for (let index = 0; index < length; index += 1) {    if (index in object) {      const value = object[index];      if (callback.call(thisArg, value, index, object)) {        result.push(value);      }    }  }  return result;}

myFilter uses the same checks. Line 7 starts with an empty result. The difference is on lines 11 and 12. When the callback returns a truthy value (a value that counts as true in an if), push adds the original value to the end of the result. So the result has no gaps, and a kept value can end up at a new index.

Your own reduce: myReducePop out in the code editor (opens in a new tab)JavaScript
function myReduce(arrayLike, callback, initialValue) {  const object = toObject(arrayLike);  const length = toLength(object.length);  if (typeof callback !== "function") {    throw new TypeError(String(callback) + " is not a function");  }   let index = 0;  let accumulator = initialValue;  if (arguments.length < 3) {    while (index < length && !(index in object)) index += 1;    if (index >= length) {      throw new TypeError("Reduce of empty array with no initial value");    }    accumulator = object[index];    index += 1;  }   for (; index < length; index += 1) {    if (index in object) {      accumulator = callback(accumulator, object[index], index, object);    }  }  return accumulator;}

reduce keeps a running result, called the accumulator. Its starting value is optional. Lines 10 to 17 handle the case where you don't pass one:

  • Line 10 counts the arguments. Fewer than 3 means there is no starting value.
  • Line 11 skips empty slots until it finds an item that exists.
  • Lines 12 and 13 throw the same error as the real reduce if there is no such item.
  • Lines 15 and 16 use that first item as the accumulator and move the loop past it.
  • Line 21 calls the callback for each item left and keeps its answer as the new accumulator.

map, filter, empty slots, and thisArg

STEP THROUGH

An array can have an empty slot, also called a hole. In [1, , 3], the two commas side by side leave index 1 with nothing at all. An array with holes is called sparse.

The real map skips a hole. It doesn't call the callback there, and it leaves the same hole in the result. A hole is not the same as a slot that holds undefined. The method first checks whether the index exists, and only then reads the value.

The real map skips a hole and keeps itPop out in the code editor (opens in a new tab)JavaScript
const sparse = [1, , 3];const calls = [];const mapped = sparse.map((value, index) => {  calls.push(index);  return value * 2;}); console.log(calls.join(","));console.log(0 in mapped, 1 in mapped, 2 in mapped);console.log(mapped);
  • Line 8 prints 0,2. The callback ran for index 0 and index 2, but not for index 1.
  • Line 9 prints true false true. The result has nothing at index 1.
  • Line 10 prints [ 2, <1 empty item>, 6 ]. The console shows the hole as an empty item.
A hole is not the same as undefinedPop out in the code editor (opens in a new tab)JavaScript
const values = [, undefined];const calls = [];const mapped = myMap(values, (value, index) => {  calls.push(index);  return value === undefined ? "present" : value;}); console.log(calls.join(","));console.log(0 in mapped, 1 in mapped);
  • Line 1 makes an array with a hole at index 0 and the value undefined at index 1.
  • Line 4 records each index the callback runs for.
  • Line 5 returns "present" when the value is undefined.
  • Line 8 prints 1. Only index 1 ran the callback.
  • Line 9 prints false true. Only index 1 exists in the result.
Step through myMap on an array with a hole
Step 0 of 13Ready
Your turn: follow the blue line

Step through myMap. Watch what the callback gets, the hole check, and what this is.

Running in
  1. script
Next: line 1
const context = { bonus: 10 };const result = myMap(sparse, function (value, index, array) {  return value + index + this.bonus;}, context);console.log(0 in result, 1 in result, result);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

filter skips holes too. But it puts the kept values next to each other in a new array, with no gaps. That is why map keeps the same length, while filter often gives back a shorter array.

Left: map on 1, empty, 3 gives 2, empty, 6. The empty slot is skipped and stays empty. Right: filter on 0, empty, 2, 3 keeps 2 and 3 and packs them into a new array with no gaps.map: value * 21empty3skipped2empty6the gap staysfilter: value >= 20empty23left out23no gaps
Both skip the empty slot. map keeps every slot in place, so the result still has a gap. filter puts only the kept values into a new array, one after another.
Step through myFilter keeping values with no gaps
Step 0 of 11Ready
Your turn: follow the blue line

Step through myFilter. The hole doesn't call the callback, and kept values are added to the end with no gaps.

Running in
  1. script
Next: line 1
const kept = myFilter(sparse, function (value, index) {  return value >= 2;});console.log(kept);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Each method also takes an optional last argument, thisArg. It sets what this means inside the callback. The next example passes it to a normal function and to an arrow function.

thisArg works with normal functions, not arrow functionsPop out in the code editor (opens in a new tab)JavaScript
const context = { bonus: 10 }; const withFunction = myMap([1], function (value) {  return value + this.bonus;}, context); const runner = {  bonus: 100,  run() {    return myMap([1], value => value + this.bonus, context);  },}; console.log(withFunction);console.log(runner.run());
  • Lines 3 to 5 pass a normal function. myMap calls it with callback.call(thisArg, ...), so this is context and this.bonus is 10. Line 14 prints [ 11 ].
  • Line 10 passes an arrow function. An arrow function has no this of its own. It uses the this of the code around it. Here that is runner, because run() was called as runner.run(). So this.bonus is 100, and line 15 prints [ 101 ].

reduce and its starting value

EXACT ERROR

reduce has the trickiest start. It depends on whether you pass a starting value:

  • With a starting value, the accumulator starts there. The callback runs for every item that exists.
  • Without one, the first item that exists becomes the accumulator. The callback starts at the next item.

In both cases, holes are skipped.

prices is 5, empty, 12. Left: with a start value of 0, the acc box starts with 0, then 0 + 5 = 5, then 5 + 12 = 17. Right: with no start value, the acc box starts with the first item, 5, then 5 + 12 = 17.myReduce(prices, …, 0)0acc0 + 5 = 55 + 12 = 17myReduce(prices, …)5acc5 is the first item5 + 12 = 17
Here prices is [5, , 12]. With a start value, the running total begins there. Without one, it begins with the first item that exists. Both end at 17.
Step through where the myReduce total starts
Step 0 of 11Ready
Your turn: follow the blue line

Pick how the total starts, then step through.

Running in
  1. script
Next: line 1
const total = myReduce(prices, (acc, price, index) => {  return acc + price;}, 0);console.log(total);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how the total starts

Changing the choice starts a new run. Guess where the total starts before you step.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Leaving out the starting value vs passing onePop out in the code editor (opens in a new tab)JavaScript
const add = (total, value) => total + value; try {  myReduce([], add);} catch (error) {  console.log(error.name);}console.log(myReduce([], add, undefined));console.log(myReduce([], add, 0));console.log(myReduce([, 5], add));
  • Line 4 leaves out the third argument. The array is empty, so there is nothing to start with. Line 6 prints TypeError.
  • Line 8 passes undefined on purpose. That still counts as a starting value, so it prints undefined.
  • Line 9 passes 0 and prints 0.
  • Line 10 has no starting value, so myReduce starts with the first item that exists, 5. Nothing comes after it, so it prints 5.
The real empty-reduce error and oursPop out in the code editor (opens in a new tab)JavaScript
try {  [].reduce((acc, value) => acc + value);} catch (error) {  console.log(error.name + ": " + error.message);} try {  myReduce([], (acc, value) => acc + value);} catch (error) {  console.log(error.name + ": " + error.message);}

Line 2 uses the real reduce, and line 8 uses myReduce. Lines 4 and 10 both print TypeError: Reduce of empty array with no initial value. Matching the exact message is useful in tests. It shows your version took the same path as the real one.

flat, depth, and recursion

RECURSION VS STACK

flat copies values into a new array with no gaps. When it finds an array inside the array, it can open it and copy its values too. The depth says how many levels to open:

  • The default depth is 1: open one level.
  • Infinity opens every level, however deep.
Your own flat, using recursionPop out in the code editor (opens in a new tab)JavaScript
function myFlat(arrayLike, depth = 1) {  const result = [];  flattenInto(result, toObject(arrayLike), toDepth(depth));  return result;} function flattenInto(target, source, depth) {  const length = toLength(source.length);  for (let index = 0; index < length; index += 1) {    if (!(index in source)) continue;    const value = source[index];    if (Array.isArray(value) && depth > 0) {      flattenInto(target, value, depth === Infinity ? Infinity : depth - 1);    } else {      target.push(value);    }  }}

Recursion means a function calls itself. Here flattenInto calls itself for each inner array.

  • Line 3 starts the work with the result, the input, and the depth.
  • Line 8 reads the length once.
  • Line 10 skips holes.
  • Line 12 checks for an inner array while the depth is above zero.
  • Line 13 calls flattenInto again for that inner array, with one less depth.
  • Line 15 copies any other value into the result.

JavaScript keeps track of every call that hasn't finished yet on the call stack. The call stack has a size limit. A very deeply nested input makes many calls at once and can go past that limit. A version that keeps its own list of work avoids this.

Your own flat, using a list of work to doPop out in the code editor (opens in a new tab)JavaScript
function myFlatWithStack(arrayLike, depth = 1) {  const root = toObject(arrayLike);  const result = [];  const stack = [{    source: root,    index: 0,    length: toLength(root.length),    depth: toDepth(depth),  }];   while (stack.length > 0) {    const frame = stack.pop();    if (frame.index >= frame.length) continue;    const index = frame.index;    frame.index += 1;    stack.push(frame);    if (!(index in frame.source)) continue;    const value = frame.source[index];    if (Array.isArray(value) && frame.depth > 0) {      stack.push({        source: value,        index: 0,        length: toLength(value.length),        depth: frame.depth === Infinity ? Infinity : frame.depth - 1,      });    } else {      result.push(value);    }  }  return result;}

Here the stack is a plain array of jobs still to do. Each job remembers four things: an array, the next index to read, its length, and its depth.

  • Line 4 starts the stack with one job: the whole input.
  • Line 11 keeps going while jobs are left.
  • Line 12 takes the last job off the stack.
  • Lines 19 and 20 find an inner array and add a new job for it, instead of calling a function.
  • Line 27 copies a plain value into the result.
flat with different depthsPop out in the code editor (opens in a new tab)JavaScript
const nested = [1, [2, [3, [4]]], , 5]; console.log(myFlat(nested));console.log(myFlat(nested, 2));console.log(myFlat(nested, Infinity));console.log(myFlatWithStack(nested, Infinity));
  • Line 3 uses depth 1 and prints [ 1, 2, [ 3, [ 4 ] ], 5 ].
  • Line 4 uses depth 2 and prints [ 1, 2, 3, [ 4 ], 5 ].
  • Lines 5 and 6 use Infinity, with both versions. Both print [ 1, 2, 3, 4, 5 ].

The hole at index 2 of nested never shows up in any result.

myFlat(nested) with depth 1. Top: 1, [2, [3, [4]]], empty, 5. Bottom: 1, 2, [3, [4]], 5. The inner array is opened once, so [3, [4]] stays nested, and the empty slot is dropped.myFlat(nested)1[2, [3, [4]]]empty5dropped12[3, [4]]5still nested
Depth 1 opens one level only: 2 comes out, but [3, [4]] stays an array. The empty slot is not copied.
Step through myFlat with different depths
Step 0 of 7Ready
Your turn: follow the blue line

Compare the version that calls itself with the list-of-jobs version, and change the depth to see what Infinity does.

Running in
  1. script
Next: line 1
const result = myFlat(nested, 1);console.log(result);
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose a version and a depth

The list-of-jobs version doesn't call itself, but it follows the same depth and hole rules.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Matching the spec

ECMA-262

The spec is the official rule book for JavaScript. Its name is ECMA-262. It describes Array.prototype.map as a list of exact steps, which is more than "loop and push". Below is a short version of those steps.

Spec steps read like a recipe. Words like ToObject and HasProperty are the names of smaller recipes in the spec. The short names stand for:

  • O: the array, turned into an object.
  • len: its length.
  • k: the current index, and Pk: that index as a property name.
  • A: the result array.
How map works, shortened from the specText
1. ToObject(this value)2. LengthOfArrayLike(O)3. If IsCallable(callbackfn) is false, throw a TypeError4. ArraySpeciesCreate(O, len)5. For each index k from 0 to len - 1:6.   If HasProperty(O, Pk), Get(O, Pk) and call callbackfn7.   CreateDataPropertyOrThrow(A, Pk, mappedValue)

In plain words, line by line:

  1. Turn the array into an object. In our code, this is toObject.
  2. Read its length once. In our code, this is toLength(object.length).
  3. If the callback is not a function, throw a TypeError.
  4. Make the result array, A, with that length.
  5. Go through each index, from 0 up to the length minus 1.
  6. If the index exists, read its value and call the callback. In our code, this is index in object.
  7. Store the callback's answer in A, at the same index.

Step 4 says ArraySpeciesCreate. It lets a subclass of Array, made with class MyList extends Array, choose what kind of array the result is. Our functions always return a normal array. It still helps to know the step is there when you read Manuals and specs.

Sort the spec steps
  • `ToObject(this value)`
  • `LengthOfArrayLike(O)`
  • `IsCallable(callbackfn)`
  • `HasProperty(O, Pk)`
  • `Get(O, Pk)`
  • `CreateDataPropertyOrThrow(A, Pk, mappedValue)`
  • Add to the end of the result
Try it yourself
0 of 7 correct

Put each step where it happens: in the checks before the loop, in the work for each index, or when the result is written.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

The next example shows step 2 at work: the length is read once.

The length is read oncePop out in the code editor (opens in a new tab)JavaScript
const values = [1, 2];const native = values.map((value, index, array) => {  if (index === 0) array.push(99);  return value * 2;}); const polyfilled = myMap([1, 2], (value, index, array) => {  if (index === 0) array[2] = 99;  return value * 2;}); console.log(native);console.log(values);console.log(polyfilled);
  • values starts with two items.
  • At index 0, line 3 pushes 99, so values now has three items.
  • Line 12 still prints [ 2, 4 ], because the loop stops after index 1.
  • Line 13 shows that values really changed: [ 1, 2, 99 ].
  • Lines 7 to 10 try the same with myMap. Line 8 adds 99 at index 2. Line 14 prints [ 2, 4 ] too.
values.map: the length is read once as 2. The callback pushes 99 during the loop, but only 1 and 2 are mapped, to 2 and 4. 99 is not visited.length: 2read once,before the loopvalues.map(…)1299added later,not visited24result
map reads the length, 2, before the loop starts. The 99 pushed during the loop lands in values, but the result is still [ 2, 4 ].
Changing later items during mapPop out in the code editor (opens in a new tab)JavaScript
const values = ["tea", "cake", "juice"];const visited = [];const result = myMap(values, (value, index, array) => {  visited.push(value);  if (index === 0) {    delete array[1];    array[2] = "coffee";    array.push("water");  }  return value.toUpperCase();}); console.log(visited.join(","));console.log(result);console.log(values);

At index 0, the callback changes the array in three ways:

  • Line 6 deletes index 1.
  • Line 7 changes index 2 to "coffee".
  • Line 8 adds "water" at the end.

The loop still stops at the length it read at the start, 3. Each index is checked only when the loop gets there:

  • Index 1 no longer exists, so it is skipped.
  • Index 2 now holds "coffee", so that new value is read.
  • "water" is at index 3, past the end, so it is never visited.

Line 13 prints tea,coffee. Line 14 prints [ 'TEA', <1 empty item>, 'COFFEE' ]. Line 15 prints [ 'tea', <1 empty item>, 'coffee', 'water' ].

Array methods work on array-like objectsPop out in the code editor (opens in a new tab)JavaScript
const letters = { length: 2, 0: "a", 1: "b" }; const native = Array.prototype.map.call(  letters,  (value, index) => value + index,);const polyfilled = myMap(letters, (value, index) => value + index); console.log(native);console.log(polyfilled);
  • Line 1 makes letters. It is not an array. It is an object with a length and numbered keys.
  • Lines 3 to 6 borrow the real map with .call(), so this inside map is letters.
  • Line 7 passes letters to myMap.
  • Lines 9 and 10 both print [ 'a0', 'b1' ].

You meet array-like objects in DOM lists, in the arguments object, and in older code that borrows array methods with call.

An index on the prototype counts tooPop out in the code editor (opens in a new tab)JavaScript
const inheritedValues = Object.create({ 0: "tea" });inheritedValues.length = 1; console.log(myMap(inheritedValues, value => value.toUpperCase()));

A prototype is the object JavaScript checks next when a property is missing. Line 1 makes an object whose prototype holds index 0. So index 0 is not on inheritedValues itself. Line 2 sets the length to 1. The in check looks at the prototype too, so myMap visits index 0. Line 4 prints [ 'TEA' ].

A BigInt length or depth throws an errorPop out in the code editor (opens in a new tab)JavaScript
for (const run of [  () => myMap({ length: 1n, 0: "tea" }, value => value),  () => myFlat(["tea"], 1n),]) {  try {    run();  } catch (error) {    console.log(error.name);  }}

A BigInt is a whole number written with an n at the end, like 1n. Line 2 tries a length of 1n. Line 3 tries a flat depth of 1n. Line 6 runs each one, and line 8 prints TypeError twice. The +value in our helpers throws for a BigInt, and so do the real methods. Only Number(1n), written out, turns a BigInt into a normal number.

Adding a method to every array

Every array gets its methods from one shared object, Array.prototype. Real polyfills sometimes add a method there, so that every array has it. Doing that in app code is risky:

  • A future version of JavaScript may add a method with the same name.
  • Libraries that loop over every property, including shared ones, may see your method.
  • Tests can pass or fail depending on which file ran first.

Leave this job to build tools. And always check first whether the method already exists.

If you add a method in a demo, remove it after

The code below adds a method to Array.prototype only inside a function that runs once, right away. It hides the method from loops using property descriptors, the settings that control a property. Before the function ends, it puts back the old method or deletes the new one. The lesson's own code never changes the real prototypes.

A safe demo that cleans up after itselfPop out in the code editor (opens in a new tab)JavaScript
(() => {  const nativeMap = Array.prototype.map;  const nativeFlat = Array.prototype.flat;  const hadOwn = Object.prototype.hasOwnProperty.call(Array.prototype, "myMap");  const previous = Array.prototype.myMap;   if (typeof Array.prototype.myMap !== "function") {    Object.defineProperty(Array.prototype, "myMap", {      value(callback, thisArg) {        return myMap(this, callback, thisArg);      },      enumerable: false,      writable: true,      configurable: true,    });  }   console.log([1, 2].myMap(value => value * 3));  const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "myMap");  console.log(descriptor.enumerable, descriptor.writable, descriptor.configurable);  console.log(Array.prototype.map === nativeMap, Array.prototype.flat === nativeFlat);   if (hadOwn) {    Object.defineProperty(Array.prototype, "myMap", {      value: previous,      enumerable: false,      writable: true,      configurable: true,    });  } else {    delete Array.prototype.myMap;  }  console.log(hadOwn    ? Array.prototype.myMap === previous    : !Object.prototype.hasOwnProperty.call(Array.prototype, "myMap"));})();
  • Lines 2 to 5 save the real methods and any old myMap, so they can be checked and put back.
  • Line 7 checks whether myMap already exists. Line 8 adds it only if it doesn't.
  • Lines 12 to 14 set the property's settings. enumerable: false hides it from loops.
  • Lines 23 to 32 put back the old method, or delete the new one.

The demo prints four lines:

  • Line 18 prints [ 3, 6 ]. The new method works.
  • Line 20 prints false true true. The method is hidden from loops (not enumerable), but it can still be changed (writable) and removed (configurable).
  • Line 21 prints true true. The real map and flat were not touched.
  • Line 33 prints true. The temporary method is gone.

The Native prototypes lesson explains how prototypes work. In normal app code, don't change them.

Lab: the real method vs your version

TEST TABLE

You can only trust a polyfill when it matches the real method on tricky inputs. The lab below runs each case twice: once with the real method, and once with the lesson function.

The real method vs your version
Code for this casePop out in the code editor (opens in a new tab)JavaScript
const input = [1, , 3];console.log(input.map(value => value * 2));console.log(myMap(input, value => value * 2));
map keeps holessame
  1. real method[2, <hole>, 6]
    your version[2, <hole>, 6]
    same behaviortrue

Use this lab whenever you change one of your functions. A good test compares more than one simple array: it checks the result, the holes, the errors, and what happens when the array changes during the loop.

Try it yourself

The callback is skipped for the hole, and the result keeps the empty slot.

Each case runs the real JavaScript method and the lesson function side by side. For the error cases, both must throw a TypeError. The empty reduce message must match exactly.

The table lists each case in the lab. Read each row as: in this case, the real method does this, so your version must do that.

What each lab case checks
CaseWhat the real method doesWhat your version must do
Holes in mapIt skips the callback for a hole. The result keeps the hole.Check index in object first. Write the answer at the same index.
Holes in filterIt skips the callback for a hole. The result has no gaps.Use the same check. Add each kept value to the end with push.
Empty reduceIt throws a TypeError with an exact message.Look for the first item that exists. Throw before the loop if there is none.
Changing the array during the loopIt ignores items added past the starting length.Read length once, before the loop.
Array-like objectIt works with any object that has length and numbered keys.Use toObject and read each item by its number.
Callback that is not a functionIt throws a TypeError before the loop.Check typeof callback === 'function' first.

Practical use and misconceptions

Rebuilding these methods helps you fix bugs faster. You will know why:

  • a callback did not run,
  • thisArg did nothing,
  • an empty reduce crashed,
  • a DOM list can borrow an array method.

You will also write better tests for helper functions that accept array-like inputs.

Using the functions in app codePop out in the code editor (opens in a new tab)JavaScript
const rows = [  { name: "Ada", skills: ["math", "js"] },  { name: "Lin", skills: [] },]; const skillLabels = myFlat(  myMap(rows, row => myMap(row.skills, skill => row.name + ": " + skill)),); console.log(skillLabels);
  • Line 7 turns each row into a list of labels, like "Ada: math". That gives an array of arrays. Lin has no skills, so Lin's list is empty.
  • Line 6 flattens that one level with myFlat.
  • Line 10 prints [ 'Ada: math', 'Ada: js' ].

The first table compares the four methods. Read each row as: this method does this job, and gives back this.

map, filter, reduce, and flat side by side
MethodWhat it doesWhat it gives backWhat your version must get right
mapIt changes each item that exists.A new array of the same length. Holes stay holes.The callback gets the value, the index, and the array. thisArg is optional.
filterIt keeps each item whose callback returns a truthy value.A new array with no gaps.Holes never call the callback and never show up in the result.
reduceIt combines the items that exist into one running result.Any kind of value.With no starting value, the first item that exists starts the running result.
flatIt copies values and opens inner arrays.A new array with no gaps.The default depth is 1. Infinity opens every level.

The second table compares three ways to write a polyfill.

Three ways to write a polyfill
WayWhat it doesGood forThe problem
A quick forEach loopIt loops over the values, or just calls the real method.Showing the simple case.It misses holes, array-like objects, reading the length once, thisArg, and the errors.
A plain function that follows the specIt does each spec step: make an object, read the length, check the callback, check each index exists, and store the answer.Learning, and safe testing.It is still simpler than a real engine. For example, it always returns a normal array.
A method added to Array.prototypeIt gives every array a new method.Real polyfill packages, added by a build tool.It can show up in loops and clash with other code, unless it checks first and is hidden from loops.

"A polyfill is just a loop with forEach."

A quick loop shows the simple case. A real polyfill must also handle holes, read the length once, throw the right errors, accept array-like objects, and pass thisArg.

"A hole is the same as undefined."

A slot that holds undefined exists, so the callback runs for it. A hole doesn't exist, so the index in object check skips it.

"Arrow functions can use thisArg."

An arrow function takes this from the code around it and ignores thisArg. Use a normal function when you pass a thisArg.

"Adding to a prototype is harmless if the name is unique."

A name that is unique today may become a real JavaScript method later, or another library may use it. Only add to a prototype in a short demo that cleans up, or through a tested polyfill package.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upShow that myMap handles holes

Run the program. Type the two printed lines, with a | between them.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const sparse = [1, , 3];
const calls = [];
const doubled = myMap(sparse, (value, index) => {
  calls.push(index);
  return value * 2;
});
console.log(calls.join(","));
console.log(0 in doubled, 1 in doubled, 2 in doubled);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeUse myFilter to keep even numbers

    Write the callback that keeps even numbers. What text prints?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function onlyEven(values) {
      return myFilter(values, value => value % 2 === 0);
    }
    console.log(onlyEven([1, 2, 3, 4]).join(","));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeSum without a starting value

      This sum passes no starting value. With a correct myReduce, what does it print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      // A broken myReduce would start the total at undefined and print NaN.
      function sum(values) {
        return myReduce(values, (total, value) => total + value);
      }
      console.log(sum([4, 5]));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeBuild myFlatMap from your functions

        Use your finished functions to build one more array method.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function myFlatMap(arrayLike, callback, thisArg) {
          // Use myMap, then flatten one level.
          return [];
        }
        console.log(myFlatMap(["js", "css"], word => word.split("")).join(","));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeAdd a method safely, then clean up

          Run the demo. Type the four printed lines, with a | between them.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          (() => {
            const nativeMap = Array.prototype.map;
            const nativeFlat = Array.prototype.flat;
            const hadOwn = Object.prototype.hasOwnProperty.call(Array.prototype, "myMap");
            const previous = Array.prototype.myMap;
          
            if (typeof Array.prototype.myMap !== "function") {
              Object.defineProperty(Array.prototype, "myMap", {
                value(callback, thisArg) {
                  return myMap(this, callback, thisArg);
                },
                enumerable: false,
                writable: true,
                configurable: true,
              });
            }
          
            console.log([1, 2].myMap(value => value * 3));
            const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "myMap");
            console.log(descriptor.enumerable, descriptor.writable, descriptor.configurable);
            console.log(Array.prototype.map === nativeMap, Array.prototype.flat === nativeFlat);
          
            if (hadOwn) {
              Object.defineProperty(Array.prototype, "myMap", {
                value: previous,
                enumerable: false,
                writable: true,
                configurable: true,
              });
            } else {
              delete Array.prototype.myMap;
            }
            console.log(hadOwn
              ? Array.prototype.myMap === previous
              : !Object.prototype.hasOwnProperty.call(Array.prototype, "myMap"));
          })();

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            8 QUESTIONS

            Work each answer out from the steps, not from memory. Each explanation names the rule the answer depends on.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Why does this lesson rebuild built-in methods, but tell you not to use your versions in real apps?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this map over an array with a hole print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const calls = [];
              [1, , 3].map((value, index) => calls.push(index));
              console.log(calls.join(","));

              Choose an answer to see the explanation.

            3. Question 3 of 8What does this empty reduce print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              try {
                [].reduce((acc, value) => acc + value);
              } catch (error) {
                console.log(error.message);
              }

              Choose an answer to see the explanation.

            4. Question 4 of 8Which callback can use the thisArg passed to myMap(values, callback, thisArg)?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does this map print when the callback pushes during the loop?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const values = [1, 2];
              const result = values.map((value, index, array) => {
                if (index === 0) array.push(99);
                return value * 2;
              });
              console.log(result);

              Choose an answer to see the explanation.

            6. Question 6 of 8What does flat(Infinity) do that flat() does not?

              Choose an answer to see the explanation.

            7. Question 7 of 8Why does Array.prototype.map.call({ length: 2, 0: 'a', 1: 'b' }, fn) work?

              Choose an answer to see the explanation.

            8. Question 8 of 8Which Object.defineProperty settings belong in a demo that adds a method to Array.prototype?

              Choose an answer to see the explanation.

            Key takeaways

            • Write plain functions first. Adding methods to Array.prototype is a separate, risky step.
            • map keeps holes in its result. filter, reduce, and flat skip them and leave no gaps.
            • The callback gets the value, the index, and the array. A normal function can also use thisArg as this.
            • reduce with no starting value begins with the first item that exists. On an empty array, it throws a TypeError.
            • Test your version against the real method on tricky inputs: holes, array-like objects, changes during the loop, bad callbacks, and deep nesting.

            One-line summary.
            A good array-method polyfill follows a short list of steps: make an object, read the length once, check the callback, check each index exists, read the value, and build the result.

            Up next in this module: rebuilding call, apply, and bind. There, you control what this means for any function yourself.

            CompleteFrontend Clear concepts. Working examples.