Topics
v6.0.0
On this page · sections

JavaScript Core JS Polyfill

Six famous polyfills: call, apply, bind, map, filter, and reduce.

Level: Medium · Coding Round · Core JS Polyfill

A polyfill is your own version of a built-in. These six show up in almost every JavaScript coding round.

Why These Six Matter

  • call — run a function now and choose this.
  • apply — same as call, but the arguments come in an array.
  • bind — do not run yet. Return a function that already has this.
  • map — new array, same length, one new value per item.
  • filter — new shorter array, only the items that pass.
  • reduce — fold the array into one value. A start of 0 still counts.

map and filter use call on every index. Learn this first, then the three array walks.

Questions

#WriteIn one line
1callRun the function now, and choose this
2applySame as call, but arguments arrive in an array
3bindDo not run yet. Return a function with this already chosen
4mapNew array, same length, one new value per item
5filterNew shorter array, only the items that pass
6reduceFold the array into one value

1. call

Run the function immediately. You pass the object that should be this, then the arguments one by one.

function greet(msg) { return `${msg}, ${this.name}`; } greet.myCall({ name: "Ada" }, "Hello"); // "Hello, Ada" // this === { name: "Ada" }

How

  1. If they passed null or nothing, use globalThis.
  2. If they passed a number or string, turn it into an object.
  3. Hang the function on that object and call it. That is what sets this.
  4. Remove it afterwards, even if the function throws.
Function.prototype.myCall = function (thisArg, ...args) { if (typeof this !== "function") { throw new TypeError("myCall must be called on a function"); } const ctx = thisArg == null ? globalThis : Object(thisArg); const key = Symbol(); ctx[key] = this; try { return ctx[key](...args); } finally { delete ctx[key]; } };

Symbol() is a private property name, so you do not overwrite name or id. Arrow functions ignore the this you pass.

2. apply

Same as call. The only change: arguments come in one array, not as separate parameters.

Math.max.myApply(null, [1, 5, 3]); // 5
Function.prototype.myApply = function (thisArg, argsList) { if (typeof this !== "function") { throw new TypeError("myApply must be called on a function"); } const args = argsList == null ? [] : Array.from(argsList); return this.myCall(thisArg, ...args); };

No list means “call with zero arguments.” Array.from also accepts array-like objects such as arguments.

call(obj, a, b) → separate arguments apply(obj, [a, b]) → one list

3. bind

Do not run the function. Return a new function that already knows its this, and any arguments you want to lock in now.

function greet(greeting, name) { return `${greeting}, ${name} — ${this.role}`; } const hi = greet.myBind({ role: "admin" }, "Hi"); hi("Ada"); // "Hi, Ada — admin" // "Hi" was saved. "Ada" is passed later.

Two ways it can be called

hi("Ada") → this is the object you bound new Admin("x") → this is the new object. Ignore the bound object.
Function.prototype.myBind = function (thisArg, ...preset) { if (typeof this !== "function") { throw new TypeError("myBind must be called on a function"); } const fn = this; function bound(...args) { const ctx = new.target ? this : thisArg; return fn.apply(ctx, [...preset, ...args]); } if (fn.prototype) { bound.prototype = Object.create(fn.prototype); } return bound; };

new.target is set only when someone writes new bound(). Copying prototype keeps instanceof working. Calling bound.call(somethingElse) does not change the bound this.

4. map

Build a new array of the same length. Run fn on each real item. Do not change the original array.

myMap([1, 2, 3], (x) => x * 2); // [2, 4, 6]

A hole is a missing index, not the value undefined.

input: [1, <empty>, 3] map: [2, <empty>, 6] same places
function myMap(array, fn, thisArg) { const out = new Array(array.length); for (let i = 0; i < array.length; i++) { if (!(i in array)) continue; out[i] = fn.call(thisArg, array[i], i, array); } return out; }

new Array(length) starts with empty slots, so skipped indexes stay empty. fn receives (value, index, array). thisArg is the this inside a normal function. An arrow function ignores it.

5. filter

Walk the same way as map, but keep only the items that pass. The result is packed: no empty slots.

myFilter([1, 2, 3, 4], (x) => x % 2 === 0); // [2, 4]
input: [1, <empty>, 2, 4] filter: [2, 4] empty slots are dropped
function myFilter(array, fn, thisArg) { const out = []; for (let i = 0; i < array.length; i++) { if (!(i in array)) continue; if (fn.call(thisArg, array[i], i, array)) { out.push(array[i]); } } return out; }

Check i in array, not array[i] == null. A real stored undefined is a value. A missing index is a hole.

map → keep the same indexes filter → keep only the matches, in order

6. reduce

Turn the whole array into one result. You either pass a starting value, or the first item becomes the start.

myReduce([1, 2, 3], (sum, n) => sum + n, 0); // 0 + 1 + 2 + 3 = 6 myReduce([1, 2, 3], (sum, n) => sum + n); // start at 1, then + 2 + 3 = 6
seed given → start at index 0, seed is the first total no seed → start at index 1, array[0] is the first total empty, no seed → throw

0 is a real starting value. Do not write if (initial) — that treats 0 as “missing.”

function myReduce(array, fn, initial) { let acc; let i = 0; if (arguments.length >= 3) { acc = initial; } else { if (!array.length) { throw new TypeError("Reduce of empty array"); } acc = array[0]; i = 1; } for (; i < array.length; i++) { if (i in array) { acc = fn(acc, array[i], i, array); } } return acc; }

arguments.length >= 3 means “they passed a third argument,” even when that argument is 0, false, null, or undefined.

Say this

“call runs now: I put the function on the object and call it, then delete it. apply is the same with an argument list. bind waits: it returns a function that already has this, unless you use new. map keeps every index. filter keeps only matches. reduce checks whether a start value was passed — 0 still counts.”

Last updated on