Polyfill array methods
Write spec-aware JavaScript polyfills for map, filter, reduce, and flat, including sparse arrays, thisArg, array-likes, and safe prototype demos.
- 01Rebuild the main array methodsWrite your own
myMap,myFilter,myReduce, andmyFlatas plain functions, without changingArray.prototype. - 02Handle the tricky casesSkip or keep empty slots correctly, pass
thisArg, read the length once, accept array-like objects, and throw the samereduceerror. - 03Test 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:
mapchanges every item.filterkeeps some of the items.reducecombines all the items into one value.flattakes 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.
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.
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 FUNCTIONSEach 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:
- Turn the input into an object. This throws an error for
nullandundefined. - Read the length once, before the loop.
- Throw an error if the callback is not a function, before the loop starts.
The code below holds three small helpers for these steps.
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.nullandundefinedthrow aTypeError, just like the real methods. - Line 8,
toLength, turns the length into a whole number.+valueturns it into a number first. Anything that is not a positive number becomes 0. - Line 14,
toDepth, does the same for the depth offlat, but keepsInfinity.
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.
myMapfunction 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 objectis 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.
myFilterfunction 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.
myReducefunction 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
reduceif 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 THROUGHAn 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.
map skips a hole and keeps itconst 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.
undefinedconst 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
undefinedat index 1. - Line 4 records each index the callback runs for.
- Line 5 returns
"present"when the value isundefined. - 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. Watch what the callback gets, the hole check, and what this is.
script
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);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.
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. The hole doesn't call the callback, and kept values are added to the end with no gaps.
script
const kept = myFilter(sparse, function (value, index) { return value >= 2;});console.log(kept);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 functionsconst 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.
myMapcalls it withcallback.call(thisArg, ...), sothisiscontextandthis.bonusis 10. Line 14 prints[ 11 ]. - Line 10 passes an arrow function. An arrow function has no
thisof its own. It uses thethisof the code around it. Here that isrunner, becauserun()was called asrunner.run(). Sothis.bonusis 100, and line 15 prints[ 101 ].
reduce and its starting value
EXACT ERRORreduce 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, , 12]. With a start value, the running total begins there. Without one, it begins with the first item that exists. Both end at 17.Pick how the total starts, then step through.
script
const total = myReduce(prices, (acc, price, index) => { return acc + price;}, 0);console.log(total);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
undefinedon purpose. That still counts as a starting value, so it printsundefined. - Line 9 passes
0and prints0. - Line 10 has no starting value, so
myReducestarts with the first item that exists, 5. Nothing comes after it, so it prints5.
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 STACKflat 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. Infinityopens every level, however deep.
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
flattenIntoagain 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.
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 depthsconst 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.
2 comes out, but [3, [4]] stays an array. The empty slot is not copied.Compare the version that calls itself with the list-of-jobs version, and change the depth to see what Infinity does.
script
const result = myFlat(nested, 1);console.log(result);Matching the spec
ECMA-262The 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, andPk: that index as a property name.A: the result array.
map works, shortened from the specText1. 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:
- Turn the array into an object. In our code, this is
toObject. - Read its length once. In our code, this is
toLength(object.length). - If the callback is not a function, throw a
TypeError. - Make the result array,
A, with that length. - Go through each index, from 0 up to the length minus 1.
- If the index exists, read its value and call the callback. In our code, this is
index in object. - 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.
`ToObject(this value)``LengthOfArrayLike(O)``IsCallable(callbackfn)``HasProperty(O, Pk)``Get(O, Pk)``CreateDataPropertyOrThrow(A, Pk, mappedValue)`- Add to the end of the result
Put each step where it happens: in the checks before the loop, in the work for each index, or when the result is written.
The next example shows step 2 at work: the length is read once.
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);valuesstarts with two items.- At index 0, line 3 pushes
99, sovaluesnow has three items. - Line 12 still prints
[ 2, 4 ], because the loop stops after index 1. - Line 13 shows that
valuesreally changed:[ 1, 2, 99 ]. - Lines 7 to 10 try the same with
myMap. Line 8 adds99at index 2. Line 14 prints[ 2, 4 ]too.
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 ].mapconst 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' ].
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 alengthand numbered keys. - Lines 3 to 6 borrow the real
mapwith.call(), sothisinsidemapisletters. - Line 7 passes
letterstomyMap. - 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.
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' ].
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.
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.
(() => { 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
myMapalready exists. Line 8 adds it only if it doesn't. - Lines 12 to 14 set the property's settings.
enumerable: falsehides 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 realmapandflatwere 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 TABLEYou 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.
const input = [1, , 3];console.log(input.map(value => value * 2));console.log(myMap(input, value => value * 2));- 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.
The callback is skipped for the hole, and the result keeps the empty slot.
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.
| Case | What the real method does | What your version must do |
|---|---|---|
Holes in map | It 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 filter | It 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 reduce | It 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 loop | It ignores items added past the starting length. | Read length once, before the loop. |
| Array-like object | It works with any object that has length and numbered keys. | Use toObject and read each item by its number. |
| Callback that is not a function | It 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,
thisArgdid nothing,- an empty
reducecrashed, - a DOM list can borrow an array method.
You will also write better tests for helper functions that accept array-like inputs.
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.
| Method | What it does | What it gives back | What your version must get right |
|---|---|---|---|
map | It 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. |
filter | It 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. |
reduce | It 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. |
flat | It 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.
| Way | What it does | Good for | The problem |
|---|---|---|---|
A quick forEach loop | It 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 spec | It 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.prototype | It 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 EXERCISESmyMap handles holesRun the program. Type the two printed lines, with a | between them.
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);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);The first line prints 0,2: the indexes the callback ran for. The second line prints true false true: index 1 is still empty in the result.
myFilter to keep even numbersWrite the callback that keeps even numbers. What text prints?
function onlyEven(values) {
return myFilter(values, value => value % 2 === 0);
}
console.log(onlyEven([1, 2, 3, 4]).join(","));function onlyEven(values) {
return myFilter(values, value => value % 2 === 0);
}
console.log(onlyEven([1, 2, 3, 4]).join(","));myFilter keeps only the values whose callback returns a truthy value. So it prints the even values, 2,4.
This sum passes no starting value. With a correct myReduce, what does it print?
// 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]));function sum(values) {
return myReduce(values, (total, value) => total + value);
}
console.log(sum([4, 5]));myReduce uses 4 as the first accumulator. Then it calls the callback once, with 4 and 5. So it prints 9.
myFlatMap from your functionsUse your finished functions to build one more array method.
function myFlatMap(arrayLike, callback, thisArg) {
// Use myMap, then flatten one level.
return [];
}
console.log(myFlatMap(["js", "css"], word => word.split("")).join(","));function myFlatMap(arrayLike, callback, thisArg) {
return myFlat(myMap(arrayLike, callback, thisArg), 1);
}
console.log(myFlatMap(["js", "css"], word => word.split("")).join(","));myMap turns each word into an array of letters. myFlat(..., 1) opens one level, so it prints j,s,c,s,s.
Run the demo. Type the four printed lines, with a | between them.
(() => {
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"));
})();(() => {
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"));
})();It prints four lines: the mapped array [ 3, 6 ], the three settings false true true, true true because the real methods were not touched, and true because the method is gone.
Quiz: check your understanding
8 QUESTIONSWork each answer out from the steps, not from memory. Each explanation names the rule the answer depends on.
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.
Question 2 of 8What does this
mapover an array with a hole print?Read the code, then predictconst calls = []; [1, , 3].map((value, index) => calls.push(index)); console.log(calls.join(","));Choose an answer to see the explanation.
Question 3 of 8What does this empty
reduceprint?Read the code, then predicttry { [].reduce((acc, value) => acc + value); } catch (error) { console.log(error.message); }Choose an answer to see the explanation.
Question 4 of 8Which callback can use the
thisArgpassed tomyMap(values, callback, thisArg)?Choose an answer to see the explanation.
Question 5 of 8What does this
mapprint when the callback pushes during the loop?Read the code, then predictconst 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.
Question 6 of 8What does
flat(Infinity)do thatflat()does not?Choose an answer to see the explanation.
Question 7 of 8Why does
Array.prototype.map.call({ length: 2, 0: 'a', 1: 'b' }, fn)work?Choose an answer to see the explanation.
Question 8 of 8Which
Object.definePropertysettings belong in a demo that adds a method toArray.prototype?Choose an answer to see the explanation.
Key takeaways
- Write plain functions first. Adding methods to
Array.prototypeis a separate, risky step. mapkeeps holes in its result.filter,reduce, andflatskip them and leave no gaps.- The callback gets the value, the index, and the array. A normal function can also use
thisArgasthis. reducewith no starting value begins with the first item that exists. On an empty array, it throws aTypeError.- 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.