Polyfill call, apply & bind
Build myCall, myApply, and myBind while comparing strict this, array-like apply arguments, partial binding, and new-aware constructors.
- 01Rebuild call and applyUse the temporary Symbol property trick, validate apply's argument bag, and clean up after every call.
- 02Implement bind honestlyReturn a wrapper that locks this, prepends partial arguments, and knows when it was called with new.
- 03Judge polyfill faithfulnessCompare native behavior to the lesson helpers for strict receivers, primitives, metadata, constructors, arrows, and classes.
Rebuild the this controls
The everyday call, apply & bind lesson taught what these methods do. The this keyword and Losing this taught why the call site matters. Now you will rebuild the machinery so the rules stop feeling magical.
A polyfill for call, apply, or bind is a normal JavaScript function that recreates the observable behavior of a built-in function method for older or learning environments. A production polyfill patches carefully; this lesson keeps the helpers standalone.
Imagine borrowing Ada's phone to make one call. call and apply use the chosen phone now; apply takes its extra values from a list. bind saves the chosen phone for later.
- In real life: Use Ada's phone for one call
- In JavaScript:
myCall(fn, receiver, ...args) - In real life: Use a list of contacts for that call
- In JavaScript:
myApply(fn, receiver, argsLike) - In real life: Save Ada on speed dial
- In JavaScript:
myBind(fn, receiver, ...preset)
Where the analogy stops: A real phone does not explain strict mode or constructors. A polyfill must document those JavaScript rules.
We will connect the implementation to Methods & this, Functions are objects, Currying & partial application, Constructors & new, F.prototype & constructors, Strict mode, and Symbols. The neighboring build-it-yourself lessons are Polyfill array methods and Build a promise from scratch.
myCall with a temporary Symbol
STEP THROUGHThe core trick is short: put the function on the target object under a unique Symbol, call it as receiver[key](...args), delete the property, then return the original result. The property call is what makes JavaScript set this to the receiver.
Step through the classic call polyfill trick: attach with a Symbol, invoke as a method, delete, and return the result.
script
if (typeof fn !== "function") throw new TypeError("Target must be callable"); const receiver = thisArg == null ? globalThis : Object(thisArg); const key = Symbol("myCall"); try { Object.defineProperty(receiver, key, { value: fn, configurable: true }); return receiver[key](...args); } finally { delete receiver[key]; }} function label(prefix, punctuation) { return prefix + this.name + punctuation;}const user = { name: "Ada" };console.log(myCall(label, user, "Hi, ", "!"));Read the steps line by line. Line 3 chooses the receiver. Line 4 creates a collision-proof key. Line 6 installs the function for one moment. Line 7 invokes it as a method. Line 9 cleans up even if the original function throws, because the cleanup sits in finally.
A string key like "__fn" might overwrite a real property. A fresh Symbol("myCall") is unique, so it avoids property collisions while still being a valid object key.
thisArg: null, undefined, primitives, and strict mode
REAL OUTPUTNative call passes a receiver into the function call. What the function sees then depends on whether the target function is strict. In a sloppy classic script, null and undefined become globalThis, and primitives are boxed. In a strict function, undefined stays undefined, null stays null, and primitives stay primitive.
function sloppyNull() { return this === globalThis;}function strictUndefined() { "use strict"; return this === undefined;}function strictNull() { "use strict"; return this === null;}function sloppyPrimitive() { return Object.prototype.toString.call(this);}function strictPrimitive() { "use strict"; return typeof this;} console.log(sloppyNull.call(null));console.log(strictUndefined.call(undefined));console.log(strictNull.call(null));console.log(sloppyPrimitive.call(5));console.log(strictPrimitive.call(5));A Symbol-property polyfill can match the common sloppy behavior by using Object(thisArg) and globalThis. It cannot perfectly pass undefined into a strict target, because the trick itself must call receiver[key](). That honesty is part of writing a production-quality lesson: know where the trick stops.
myApply: call plus an argument bag
VALIDATIONapply is call with one packaging difference. Its second argument is either null or undefined for “no arguments,” or an object with length and indexed values. A primitive such as 7 is not an argument bag and should throw a TypeError before the original function runs.
function myCall(fn, thisArg, ...args) { const receiver = thisArg == null ? globalThis : Object(thisArg); const key = Symbol("myCall"); try { Object.defineProperty(receiver, key, { value: fn, configurable: true }); return receiver[key](...args); } finally { delete receiver[key]; }}function myApply(fn, thisArg, argsArray) { if (argsArray == null) return myCall(fn, thisArg); if (typeof argsArray !== "object" && typeof argsArray !== "function") { throw new TypeError("CreateListFromArrayLike called on non-object"); } return myCall(fn, thisArg, ...Array.from(argsArray));}function list(a, b) { return this.prefix + a + " & " + b;}const args = { 0: "Ada", 1: "Grace", length: 2 };console.log(myApply(list, { prefix: "Team: " }, args));Line 12 accepts null and undefined. Lines 13–15 reject non-objects. Line 16 turns an array-like value into real arguments with Array.from. Today, if you only need to unpack an array, spread syntax is clearer:
const scores = [4, 9, 2];console.log(Math.max.apply(null, scores));console.log(Math.max(...scores));myBind: fixed this and preset leading arguments
PARTIAL APPLICATIONbind returns a new function. Normal calls to that wrapper use the original function with the bound this value and a combined argument list: preset values first, later values after them. That is partial application, the same idea explored in the currying lesson.
function multiply(a, b, c) { return a * b * c;}const nativeDouble = multiply.bind(null, 2);console.log(nativeDouble.name);console.log(nativeDouble.length);console.log(nativeDouble(3, 4)); function showName(punctuation) { return this.name + punctuation;}const first = showName.bind({ name: "Ada" });const rebound = first.bind({ name: "Grace" });console.log(rebound("!"));Native bound functions also expose metadata. The name is prefixed with "bound ", and length is the number of remaining parameters after preset arguments. This lesson’s standalone myBind attempts those with Object.defineProperty, but the behavior is best effort because older engines may not let you redefine function metadata.
Binding twice does not replace the first bound this. The second wrapper can add more preset arguments, but the first receiver wins.
bind with new
STEP THROUGHThe constructor case is the part simplified helpers usually miss. When a bound function is called with new, the bound thisArg is ignored. JavaScript creates a fresh instance, prepends the bound arguments, and keeps the original prototype relationship so point instanceof Point is true.
Binding a constructor is like filling in one blank before a new form is made. The new form is still the one being filled in. The old placeholder object is not changed.
- In real life: The form still belongs to Point
- In JavaScript: The original
Point.prototyperemains in the chain - In real life: The first blank already says 1
- In JavaScript: The preset argument
1becomesx - In real life: A new form is filled out
- In JavaScript:
newsupplies the realthis
Where the analogy stops: A paper form cannot return another form. JavaScript constructors can return an object, so a complete helper handles that case.
Step through the constructor branch of myBind: new.target changes which this value is forwarded.
script
function boundFunction(...later) { const usingNew = new.target !== undefined; const receiver = usingNew ? this : thisArg; const result = fn.apply(receiver, [...preset, ...later]); return result; } if (fn.prototype) { boundFunction.prototype = Object.create(fn.prototype); } return boundFunction;} function Point(x, y) { this.x = x; this.y = y;}Point.prototype.label = function () { return this.x + "," + this.y;}; const BoundPoint = myBind(Point, { x: 99, y: 99 }, 1);const point = new BoundPoint(2);console.log(point.label());console.log(point instanceof Point);Modern helpers can use new.target to detect the constructor branch. Older MDN-style polyfills used an empty bridge function and an instanceof check to make the prototype chain work:
function classicBind(fn, thisArg, ...preset) { function Empty() {} function bound(...later) { const receiver = this instanceof Empty ? this : thisArg; return fn.apply(receiver, preset.concat(later)); } if (fn.prototype) { Empty.prototype = fn.prototype; bound.prototype = new Empty(); // Keep Empty.prototype intact because bound uses instanceof Empty. } return bound;} function Point(x, y) { this.x = x; this.y = y;}const BoundPoint = classicBind(Point, { x: 99 }, 1);const point = new BoundPoint(2);console.log(point instanceof Point);Native vs polyfill: know what matches
COMPAREA polyfill is only useful when you can name its faithfulness. The table is the map; the lab below produces the values with real JavaScript. The most important mismatch is strict receiver substitution in myCall.
| Case | Status | Why |
|---|---|---|
Object receiver with call | Matches | Temporary method call supplies the same receiver. |
null or undefined in sloppy code | Matches common polyfill | Both substitute globalThis; classic scripts do this. |
null or undefined in strict code | Intentional mismatch | Native strict functions receive the exact value; the Symbol trick must call through an object. |
| Primitive receiver | Matches sloppy functions | Native sloppy calls box primitives; the helper uses Object(thisArg). Strict primitive receivers are not perfectly emulated. |
apply with null args | Matches | No normal arguments are forwarded. |
apply with a number as args | Matches error kind | Both throw TypeError; the helper checks before Array.from. |
Partial bind | Matches | Preset values are prepended to later arguments. |
| Binding twice | Matches | The first bound this wins; later binds can only add arguments. |
new with a bound constructor | Matches in this helper | new.target ignores bound this and the prototype bridge keeps instanceof Original true. |
name and length | Best effort | Native engines set them; this helper attempts Object.defineProperty. |
Class constructor with call | Matches failure | Classes throw because they require new. |
// All helpers are standalone; no built-in prototype is patched.const cases = [ "normal object receiver", "strict undefined receiver", "primitive receiver", "array-like apply arguments", "partial bind", "bind with new", "name and length", "class constructor call",];for (const label of cases) { console.log(label);}- Native
Hello, Ada- Polyfill
Hello, Ada- Verdict
- same observable result
The Symbol method trick matches native call for ordinary object receivers.
Arrow functions do not have their own this, so call, apply, and bind cannot retarget them. Class constructors are the opposite edge: they require new, so calling them through call throws a TypeError.
const arrow = (function makeArrow() { return () => this.name;}).call({ name: "Ada" });console.log(arrow.call({ name: "Grace" })); class Widget {}try { Widget.call({});} catch (error) { console.log(error.name); console.log(error.message);}Where you’ll use this
SORT ITThese helpers show up in practical code even when you never ship the polyfill. Event handlers use bind or wrappers to preserve a receiver. Libraries borrow array methods for array-like values. Utility code partially applies repeated options such as logging prefixes, tax rates, locales, or sort directions.
- Borrow
Array.prototype.joinfor a string-like object right now - Run a function with arguments already stored in
{ 0: "Ada", length: 1 } - Create a callback that remembers
viewbefore an event fires later - Save the first argument of
priceWithTax(rate, price)as0.08 - Read older code that does
Math.max.apply(null, numbers) - Support
new BoundPoint(2)after presettingxto1
Sort each task by the helper whose behavior it needs.
Modern JavaScript often prefers clearer syntax once you understand the old pattern: Math.max(...numbers) instead of apply, wrappers instead of repeated bind in hot render paths, and Object.hasOwn instead of a borrowed hasOwnProperty when your target environments support it.
Common misconceptions
- “The Symbol trick is the spec.” It teaches the call-site mechanism, but it is not a full engine-level implementation.
- “Strict and sloppy receivers behave the same.” Strict functions keep exact receivers; sloppy functions substitute and box.
- “
applyaccepts any iterable.” Nativeapplywants an object argument list. This lesson usesArray.fromfor array-like values after validation. - “
bindonly fixesthis.” It also presets leading arguments and can be used withnew. - “Binding twice changes
thisagain.” The first bound receiver wins. - “A bound constructor writes into the bound object.”
newcreates a fresh instance and ignores the boundthisArg. - “Classes are just functions for
call.” Class constructors throw when invoked withoutnew.
| Tool | Changes this? | Runs now? | Constructor behavior |
|---|---|---|---|
call | Runs the function immediately | First argument is this; the rest are listed one by one | Can be polyfilled with a temporary Symbol property |
apply | Runs the function immediately | First argument is this; second is null, undefined, or an array-like object | Can delegate to myCall(...Array.from(args)) after validation |
bind | Returns a new function | First argument is locked this; later arguments are preset leading values | Needs a wrapper, metadata best effort, and a constructor branch |
| Arrow functions | Ignore these this controls | call, apply, and bind cannot replace lexical this | No useful this polyfill target |
Practice exercises
5 EXERCISESFill in the helper so the final line prints Hi, Ada!.
function myCall(fn, thisArg, ...args) {
// 1. Pick a receiver: globalThis for null/undefined, otherwise Object(thisArg).
// 2. Store fn under a Symbol, call receiver[key](...args), then delete the key.
}
function greet(punctuation) {
return "Hi, " + this.name + punctuation;
}
console.log(myCall(greet, { name: "Ada" }, "!"));function myCall(fn, thisArg, ...args) {
if (typeof fn !== "function") throw new TypeError("Target must be callable");
const receiver = thisArg == null ? globalThis : Object(thisArg);
const key = Symbol("myCall");
try {
Object.defineProperty(receiver, key, { value: fn, configurable: true });
return receiver[key](...args);
} finally {
delete receiver[key];
}
}
function label(prefix, punctuation) {
return prefix + this.name + punctuation;
}
const user = { name: "Ada" };
console.log(myCall(label, user, "Hi, ", "!"));The temporary property makes the invocation a method call, so JavaScript supplies the receiver as this.
The starter calls Array.from(null). Repair it so no-argument apply calls work.
function brokenApply(fn, thisArg, argsArray) {
return fn.call(thisArg, ...Array.from(argsArray));
}
function who() { return this.name; }
try {
console.log(brokenApply(who, { name: "Ada" }, null));
} catch (error) {
console.log(error.name);
}function myCall(fn, thisArg, ...args) {
const receiver = thisArg == null ? globalThis : Object(thisArg);
const key = Symbol("myCall");
try {
Object.defineProperty(receiver, key, { value: fn, configurable: true });
return receiver[key](...args);
} finally {
delete receiver[key];
}
}
function myApply(fn, thisArg, argsArray) {
if (argsArray == null) return myCall(fn, thisArg);
return myCall(fn, thisArg, ...Array.from(argsArray));
}
function who() { return this.name; }
console.log(myApply(who, { name: "Ada" }, null));The fixed helper handles the no-arguments case first, then only converts real argument objects.
Add the validation branch so myApply(noop, null, 7) prints the error name.
function myApply(fn, thisArg, argsArray) {
if (argsArray == null) return myCall(fn, thisArg);
// Add the same non-object check native apply performs before Array.from.
}
function noop() { return "ok"; }
try {
myApply(noop, null, 7);
} catch (error) {
console.log(error.name);
}function myCall(fn, thisArg, ...args) {
const receiver = thisArg == null ? globalThis : Object(thisArg);
const key = Symbol("myCall");
try {
Object.defineProperty(receiver, key, { value: fn, configurable: true });
return receiver[key](...args);
} finally {
delete receiver[key];
}
}
function myApply(fn, thisArg, argsArray) {
if (argsArray == null) return myCall(fn, thisArg);
if (typeof argsArray !== "object" && typeof argsArray !== "function") {
throw new TypeError("CreateListFromArrayLike called on non-object");
}
return myCall(fn, thisArg, ...Array.from(argsArray));
}
function noop() { return "ok"; }
try {
myApply(noop, null, 7);
} catch (error) {
console.log(error.name);
}The validation matches the important native rule: the second argument can be absent, nullish, or object-like, but not a primitive number.
Implement the normal-call branch of myBind and prove partial application works.
function myBind(fn, thisArg, ...preset) {
return function boundFunction(...later) {
return fn.apply(thisArg, preset.concat(later));
};
}
function multiply(a, b) { return a * b; }
const double = myBind(multiply, null, 2);
console.log(double(6));function myBind(fn, thisArg, ...preset) {
return function boundFunction(...later) {
return fn.apply(thisArg, preset.concat(later));
};
}
function multiply(a, b) { return a * b; }
const double = myBind(multiply, null, 2);
console.log(double(6));The wrapper saves 2 as the first argument. Calling double(6) forwards [2, 6] to multiply.
Finish the constructor branch so new BoundPoint(2) prints 1,2 true.
function myBind(fn, thisArg, ...preset) {
function boundFunction(...later) {
const receiver = new.target ? this : thisArg;
return fn.apply(receiver, preset.concat(later));
}
// Link boundFunction.prototype to fn.prototype here.
return boundFunction;
}
function Point(x, y) { this.x = x; this.y = y; }
Point.prototype.label = function () { return this.x + "," + this.y; };
const BoundPoint = myBind(Point, { x: 99 }, 1);
const point = new BoundPoint(2);
try {
console.log(point.label(), point instanceof Point);
} catch (error) {
console.log(error.name);
}function myBind(fn, thisArg, ...preset) {
function boundFunction(...later) {
const receiver = new.target ? this : thisArg;
const result = fn.apply(receiver, preset.concat(later));
return result;
}
if (fn.prototype) {
boundFunction.prototype = Object.create(fn.prototype);
}
return boundFunction;
}
function Point(x, y) { this.x = x; this.y = y; }
Point.prototype.label = function () { return this.x + "," + this.y; };
const BoundPoint = myBind(Point, { x: 99 }, 1);
const point = new BoundPoint(2);
console.log(point.label(), point instanceof Point);The constructor branch ignores the bound object, forwards preset and fresh arguments to the new instance, and links the wrapper prototype to the original prototype.
Check your understanding
8 QUESTIONSQuestion 1 of 8Why does the
myCallpolyfill store the function under aSymbolkey before invoking it?Choose an answer to see the explanation.
Question 2 of 8What do these strict and sloppy receiver checks print?
Read the code, then predictfunction sloppy() { return this === globalThis; } function strict() { "use strict"; return this === undefined; } console.log(sloppy.call(null)); console.log(strict.call(undefined));Choose an answer to see the explanation.
Question 3 of 8What does native apply print when the argument bag is a number?
Read the code, then predictfunction add(a, b) { return a + b; } try { console.log(add.apply(null, 7)); } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 4 of 8What does the partially bound multiplier print?
Read the code, then predictfunction multiply(a, b) { return a * b; } const double = multiply.bind(null, 2); console.log(double(6));Choose an answer to see the explanation.
Question 5 of 8What does binding the same function twice print?
Read the code, then predictfunction show() { return this.name; } const once = show.bind({ name: "Ada" }); const twice = once.bind({ name: "Grace" }); console.log(twice());Choose an answer to see the explanation.
Question 6 of 8What does this bound constructor snippet print?
Read the code, then predictfunction Point(x, y) { this.x = x; this.y = y; } Point.prototype.label = function () { return this.x + "," + this.y; }; const BoundPoint = Point.bind({ x: 99, y: 99 }, 1); const point = new BoundPoint(2); console.log(point.label()); console.log(point instanceof Point);Choose an answer to see the explanation.
Question 7 of 8What does a class constructor called with
callprint here?Read the code, then predictclass Widget {} try { Widget.call({}); } catch (error) { console.log(error.name); console.log(error.message.includes("cannot be invoked without 'new'")); }Choose an answer to see the explanation.
Question 8 of 8What metadata does native bind report here?
Read the code, then predictfunction multiply(a, b, c) { return a * b * c; } const bound = multiply.bind(null, 2); console.log(bound.name); console.log(bound.length);Choose an answer to see the explanation.
Key takeaways
myCalluses a uniqueSymbolproperty so the original function can be invoked as a method of the receiver.myApplyismyCallplus argument-list validation andArray.fromfor array-like values.- Strict receiver semantics are the main place a pure Symbol trick is not fully spec-faithful.
myBindreturns a wrapper that fixesthis, prepends preset arguments, and treatsnewspecially withnew.target.- Native
bindmetadata, arrows, classes, and constructor return values are the edges that separate a learning helper from a production polyfill.
Remember the one-liner.
Rebuilding these methods is really rebuilding call sites: immediate method calls for call/apply, and a reusable wrapper for bind.
Up next: build a promise from scratch.