cf.completefrontendCode editorOpen lab
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 core array methodsImplement real standalone myMap, myFilter, myReduce, and myFlat functions before discussing prototype methods.
  • 02
    Handle spec edge casesPreserve or skip holes correctly, bind thisArg, capture length once, support array-likes, and throw the same reduce error.
  • 03
    Test like a polyfill authorRun native methods and your versions side-by-side on sparse arrays, mutations, bad callbacks, and deep flattening.

Why rebuild built-ins?

A polyfill is code that provides a standard feature when an environment does not have it yet. In this lesson we rebuild map, filter, reduce, and flat as standalone functions so you can see the rules that native methods hide.

This is the first lesson in Build it from scratch. The point is understanding: rebuilding a familiar feature makes callback arguments, sparse arrays, errors, and spec language concrete. Interviewers also like these questions because small omissions reveal whether someone knows the real behavior or only the happy path.

Production advice

Do not ship lesson polyfills instead of the platform. In production, prefer native built-ins, targeted transpilation, and vetted packages such as core-js through a build setup. The Transpilers and polyfills lesson explains that toolchain.

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

You understand a bicycle differently after you remove the wheel, clean the chain, and adjust the brakes. Polyfilling does the same for built-ins: it exposes the small rules you rely on every day.

In real life: Remove the wheel and inspect the hub
In JavaScript: Write the loop and inspect each callback argument
In real life: Notice the brake cable routing
In JavaScript: Handle holes, thisArg, errors, and length capture
In real life: Put the bicycle back together
In JavaScript: Run native and polyfill behavior side-by-side

Where the analogy stops: Taking apart a bicycle teaches the mechanism. It does not mean your hand-built parts are safer than factory-tested parts on a real road.

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

Standalone polyfills first

REAL FUNCTIONS

Start with standalone functions. They are easy to test, they do not mutate global prototypes, and they make the receiver explicit. Each function begins with the same three habits: convert the input with a ToObject-style helper, capture an array-like length once, and reject a non-function callback before iterating.

Shared helpers for the lesson polyfillsPop 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 = 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 = Number(value);  if (number === Infinity) return Infinity;  if (!Number.isFinite(number) || number <= 0) return 0;  return Math.floor(number);}

ToObject is why array methods can work on strings and array-like objects, but reject null and undefined. LengthOfArrayLike is why appending during iteration does not extend the loop.

A spec-aware standalone 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;}

Line 7 creates the output at the captured length. Line 9 is the sparse-array rule: only present properties call the callback. Line 10 passes value, index, and the array-like object, while binding the optional thisArg.

A standalone 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;}

filter does not preserve indexes. When the callback returns a truthy value, the original value is appended to the next dense result index.

A standalone 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 is different because it may not have an initial accumulator. If no initial value is passed, the first present element becomes the accumulator. If there is no present element, the native error message must be thrown.

map, filter, holes, and thisArg

STEP THROUGH

Native map preserves holes and skips callbacks for them. A sparse slot is not the same as a slot containing undefined. The spec checks whether the property exists before reading it.

Native map skips holes and preserves themPop 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);

The first log is 0,2, proving index 1 never called the callback. The second log is true false true, proving the mapped result still has a hole at index 1.

Step through myMap on a sparse array
Step 0 of 13Ready
Your turn: follow the blue line

Step through the standalone myMap. Watch the callback arguments, the hole check, and the thisArg binding.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
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);
CallStoreChangeResultRun = next line. 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 also skips holes, but it compacts passing values into a dense result. That difference is why map can keep the same length while filter often returns a shorter array.

Step through myFilter compacting values
Step 0 of 11Ready
Your turn: follow the blue line

Step through myFilter. Holes do not call the callback, and passing values compact into a dense result.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const kept = myFilter(sparse, function (value, index) {  return value >= 2;});console.log(kept);
CallStoreChangeResultRun = next line. 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.
thisArg works with regular functions, not arrowsPop 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());

A regular callback gets this from callback.call(thisArg, ...), so the first log is [11]. The arrow ignores that call binding and captures the surrounding receiver as this, so the second log is [101].

reduce and the first accumulator

EXACT ERROR

reduce has the trickiest start. With an initial value, every present item reaches the callback. Without one, the first present item becomes the accumulator and callbacks begin after it. Holes are skipped in both paths.

Step through myReduce accumulator choices
Step 0 of 11Ready
Your turn: follow the blue line

Switch reduce modes and step through how the accumulator starts.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const total = myReduce(prices, (acc, price, index) => {  return acc + price;}, 0);console.log(total);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose how the accumulator starts

Changing the mode starts a fresh recorded run. Predict the first accumulator before stepping.

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.
Prove the native empty-reduce messagePop 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);}

Both lines print TypeError: Reduce of empty array with no initial value. Matching that exact message is useful in tests because it proves the polyfill took the same no-accumulator path.

flat depth and recursion

RECURSION VS STACK

flat copies values into a new dense array. When it sees an array and the depth is above zero, it opens that array and continues. The default depth is 1; Infinity keeps descending.

Recursive myFlatPop 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 reads like the definition: flatten this source, and when a nested array appears, flatten that source too with one less depth. For very deep input, an explicit stack avoids using the JavaScript call stack.

Explicit-stack myFlatWithStackPop 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;}
Depth examples for flatPop 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));
Step through myFlat depth handling
Step 0 of 7Ready
Your turn: follow the blue line

Compare recursive flattening with an explicit stack, and change the depth to see why Infinity matters.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const result = myFlat(nested, 1);console.log(result);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Choose flattening strategy

The explicit stack avoids recursive calls but 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 ECMA-262 algorithm for Array.prototype.map is more precise than “loop and push.” Read the spec names as a checklist. The exact document can look formal, but the steps map cleanly onto the code you just wrote.

Array.prototype.map algorithm checklistText
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)

ArraySpeciesCreate is why subclassed arrays can choose the constructor used for the result. These standalone polyfills return normal arrays, but knowing the step exists keeps you honest when reading Manuals and specs.

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

Place each step by when it happens: setup, per-index iteration, or result writing.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Length is captured 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);

Both native and polyfill versions map only the original two indexes. Appending during iteration mutates the source, but it does not extend the loop.

Array methods are generic over array-likesPop 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);

An object with a length and numeric properties can be a receiver. That matters when designing APIs, working with DOM lists, or reading older JavaScript that borrows array methods with call.

Prototype safety

Real polyfills sometimes install a method on Array.prototype. That is dangerous in application code: you can collide with future standards, break libraries that loop over inherited properties, or make tests order-dependent. Keep prototype patches inside controlled build tools and always feature check first.

If you demonstrate a patch, restore it

The runnable snippet below patches Array.prototype only inside an immediately invoked function, makes the method non-enumerable with property descriptors, and restores or deletes the method before it exits. The lesson TypeScript modules never patch native prototypes.

Safe isolated prototype demonstrationPop out in the code editor (opens in a new tab)JavaScript
(() => {  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));  console.log(Object.keys(Array.prototype).includes("myMap"));   if (hadOwn) {    Object.defineProperty(Array.prototype, "myMap", {      value: previous,      enumerable: false,      writable: true,      configurable: true,    });  } else {    delete Array.prototype.myMap;  }})();

The first log proves the method worked. The second log is false, proving myMap is not enumerable. Learn the prototype rules in Native prototypes, but avoid patching them in normal app code.

Native vs polyfill lab

TEST TABLE

Polyfills are only believable when they match native behavior on uncomfortable inputs. The lab below runs each case twice: once with the native method and once with the lesson function.

Native vs polyfill conformance table
Conformance case sourcePop 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 preserves holesmatch
  1. native[2, <hole>, 6]
    polyfill[2, <hole>, 6]
    same behaviortrue

Use this table when you change a polyfill. A good test compares shapes, holes, errors, and mutation timing rather than only one happy-path array.

Try it yourself

HasProperty skips the callback but the result keeps the empty slot.

Every row runs native JavaScript and the lesson polyfill side-by-side. TypeError rows match by error type; reduce's empty-array message matches exactly.
What the conformance table is checking
CaseNative behaviorPolyfill rule
Sparse mapCallback skips holes; output keeps holesUse index in object before Get, then write at the same index
Sparse filterCallback skips holes; output is denseUse the same hole check, then append passing values
Empty reduceThrows the exact TypeError messageFind the first present element or throw before looping
Mutation during iterationNew indexes past captured length are ignoredStore length before the loop
Array-like receiverWorks with length and numeric keysUse ToObject and numeric property access
Non-callable callbackThrows TypeError before iteratingCheck typeof callback === 'function' early

Practical use and misconceptions

Rebuilding methods makes you a sharper debugger. You will recognize why a callback did not run, why thisArg did nothing, why an empty reducer crashed, and why a NodeList can borrow an array method. You will also write better tests for utilities that accept array-like inputs.

Using the standalone functions in app-shaped 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);
Map, filter, reduce, and flat compared
MethodJobResult shapePolyfill detail
mapTransforms each present itemPreserves length and holesCallback receives value, index, and array; optional thisArg
filterKeeps present items whose callback returns truthyReturns a dense arrayHoles never call the callback and never appear in the result
reduceFolds present items into one accumulatorReturns any typeWithout an initial value, first present item becomes the accumulator
flatCopies values while opening nested arraysReturns a dense arrayDefault depth is 1; Infinity keeps descending
Quick demo vs spec-faithful polyfill
ApproachWhat it doesUseful forProblem
Quick forEach wrapperCalls the native method or loops over valuesGreat for explaining a happy pathMisses holes, array-likes, length capture, thisArg, and TypeErrors
Spec-faithful standalone functionUses ToObject, LengthOfArrayLike, IsCallable, HasProperty, and explicit writesBest learning target and safest to testStill not a full engine: species constructors and realm details are simplified
Prototype patchAdds a method to Array.prototypeOnly needed for real polyfills in controlled buildsCan break enumeration and other libraries if not feature-checked and non-enumerable

“A polyfill is just a wrapper around forEach.”

A wrapper can teach the happy path, but a real polyfill must handle holes, length capture, TypeErrors, array-likes, and callback binding.

“A hole is the same as undefined.”

A slot containing undefined is present and calls the callback. A hole is absent and is skipped by HasProperty.

“Arrow callbacks can use thisArg.”

Arrows capture this lexically. Use a regular function when the array method receives a thisArg.

“Prototype patches are harmless if the name is unique.”

A unique name today can become tomorrow's standard or collide with another library. Patch only in isolated demos or vetted polyfill packages.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upProve myMap handles holes

Run the program and type the two printed facts separated by a pipe.

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 · PracticeWrite myFilter usage

    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 · PracticeFix a no-initial reduce polyfill

      Explain what the fixed reducer should print when no initial value is supplied.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      // A broken reducer would start accumulator as undefined here.
      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 · ChallengeWrite myFlatMap from your pieces

        Use the completed polyfills 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 · ChallengePatch safely, then clean up

          Run the isolated patch demo. What does it prove?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          (() => {
            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));
            console.log(Object.keys(Array.prototype).includes("myMap"));
          
            if (hadOwn) {
              Object.defineProperty(Array.prototype, "myMap", {
                value: previous,
                enumerable: false,
                writable: true,
                configurable: true,
              });
            } else {
              delete Array.prototype.myMap;
            }
          })();

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

            Quiz: check your understanding

            8 QUESTIONS

            Predict from the algorithm, not from memory. The explanations name the spec rule each answer depends on.

            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Why does this module rebuild built-ins instead of recommending custom replacements in production?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does sparse map example print first?

              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 exact message does empty reduce without an initial value print here?

              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 the length-capture example print first?

              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 prototype demonstration?

              Choose an answer to see the explanation.

            Key takeaways

            • Build standalone functions first; patching prototypes is a separate, risky demonstration.
            • map preserves holes, while filter, reduce, and flat skip them.
            • Array callbacks receive value, index, and array; regular functions can use thisArg.
            • reduce without an initial value uses the first present element or throws the native TypeError on empty arrays.
            • Spec-faithful polyfills test array-likes, mutation during iteration, non-callable callbacks, holes, and deep flattening.

            One-line summary.
            A good array-method polyfill is a small spec checklist: object, length, callable callback, property check, value read, and a carefully shaped result.

            Up next in this module: rebuilding call, apply, and bind, where the focus shifts from array receivers to function receivers and explicit this control.

            CompleteFrontend Clear concepts. Working examples.