Function.prototype.call and apply Polyfill
Polyfill call and apply: set this for one invocation, spread an argument list, and explain why apply(null, array) is not the same as a spread.
- javascript
- this
- polyfill
- interview
The first call polyfill almost everyone writes is thisArg.fn = target; thisArg.fn(a, b); delete thisArg.fn. It greets 'fn' in thisArg forever when delete fails, it clobbers an existing fn, and the target can see that extra own property while it runs. Native Function.prototype.call never installs a visible key on the receiver. The sibling trap is treating apply as fn(...list). apply takes an array-like. Spread takes an iterable. fn.apply(null, aSet) and fn(...aSet) are not the same call.
Write standalone myCall and myApply. Assigning onto Function.prototype is interview convention and mutates a shared builtin.
Key takeaways
callsetsthisfor one invocation and takes a discrete argument list.applyis the same invocation, with arguments read from an array-like (lengthplus indexes).- Do not leave a leftover own property on the receiver. A unique key plus
deleteinfinallyis the whiteboard fix.- Primitive
thisArgis boxed in sloppy mode. Strict functions keep the primitive. Nullish becomesglobalThisonly in sloppy mode.apply(null, x)is not a spread. Strings throw. Sets contribute no args. Array-likes without an iterator work.
call sets this, apply spreads the list
Internal Call(F, thisArg, argList) is the whole feature. call builds argList from the rest parameters. apply builds it with CreateListFromArrayLike: the second argument must be an object, or null / undefined (meaning no args). A string primitive is not an object, so fn.apply(null, 'ab') throws. 'ab' is iterable, so fn(...'ab') passes 'a' and 'b'.
function myCall(fn, thisArg, ...args) {
if (typeof fn !== 'function') {
throw new TypeError('myCall called on a non-function');
}
if (thisArg == null) {
// Ordinary call: sloppy callees see globalThis, strict callees see undefined.
// Native call(null) on a strict function yields null, not undefined.
return fn(...args);
}
const context = Object(thisArg);
const key = Symbol('myCall');
context[key] = fn;
try {
return context[key](...args);
} finally {
delete context[key];
}
}
function myApply(fn, thisArg, argArray) {
if (typeof fn !== 'function') {
throw new TypeError('myApply called on a non-function');
}
let args = [];
if (argArray != null) {
if (typeof argArray !== 'object' && typeof argArray !== 'function') {
throw new TypeError('CreateListFromArrayLike called on non-object');
}
const len = argArray.length >>> 0;
args = new Array(len);
for (let i = 0; i < len; i++) {
args[i] = argArray[i];
}
}
return myCall(fn, thisArg, ...args);
}
function tag(prefix, suffix) {
return prefix + this.id + suffix;
}
const node = { id: 7 };
myCall(tag, node, '<', '>'); // '<7>'
myApply(tag, node, ['#', '']); // '#7'
Symbol as the install key avoids colliding with 'fn' or 'call'. finally runs on throw, so a target that blows up does not leave the method behind. The property is still visible to Object.getOwnPropertySymbols(this) during the call. Native call does not have that leak. Say it; do not pretend the polyfill is exotic.
Object(thisArg) is the sloppy-mode box. tag.call(1) in sloppy code sees a Number object. In strict code (modules included) this stays the primitive 1. A property-attach polyfill cannot represent a primitive this without the real Call. Same gap for this === null on a strict function: attaching to null is impossible, and fn(...args) passes undefined, not null.
apply is array-like, spread is iterable
| Value passed as the list | myApply(fn, thisArg, value) |
fn(...value) |
|---|---|---|
[1, 2] |
two args | two args |
{ 0: 1, 1: 2, length: 2 } |
two args | TypeError unless it is iterable |
new Set([1, 2]) |
zero args (length is undefined) |
two args |
'ab' (primitive) |
TypeError |
'a', 'b' |
null / undefined |
zero args | TypeError |
arguments object |
works (array-like) | works in ES2015+ (iterable) |
That table is the answer to “why is apply(null, array) not a spread?” Null as thisArg is a separate issue: it is sloppy-global / strict-null, while fn(...array) is an ordinary call (undefined / global). The argument-list rule is the one people skip.
function argc() {
return arguments.length;
}
myApply(argc, null, { 0: 'a', 1: 'b', length: 2 }); // 2
myApply(argc, null, new Set([1, 2, 3])); // 0
// argc(...new Set([1, 2, 3])) // 3
Footguns in call and apply polyfills
A string key 'fn' overwrites thisArg.fn. Restoring the old value still means the target observed the wrong fn while it ran. Unique key, then delete.
Frozen or sealed receivers throw when you add the key. Native call works on frozen objects because it never adds one. Reflect.apply(fn, thisArg, args) is the engine primitive; using it is correct and, in a strict reading, cheating if the prompt banned call / apply / Reflect.
Huge lists: apply and spread both hit argument-count limits on old engines. Math.max.apply(null, giant) is a history question, not a reason to copy the array with a loop in user code today.
apply with a missing second argument is apply(thisArg, undefined): no args, no throw. Passing a number, a boolean, or a string throws. Do not coerce those with Object(argArray) or you accept Object(3) and read length off a Number wrapper (undefined → zero args) instead of throwing.
The bind polyfill is allowed to use apply. This page is the one that may not. Prototype assignment (Function.prototype.myCall = function (...) { return myCall(this, ...); }) mutates a builtin.
Interview answer (out loud)
“call invokes the function once with an explicit this and a rest list. apply does the same, but the arguments come from an array-like: I read length and indexes, or I take no args when the list is null or undefined. A non-object list throws. I would not assign thisArg.fn = target, because that leaves or clobbers an own property; a Symbol key and delete in finally is the usual polyfill. Primitive this is boxed in sloppy mode. apply is not spread: Sets and strings prove it.”
Further reading
- Function.prototype.call: MDN
- Function.prototype.apply: MDN
- Function.prototype.call: ECMA-262
- Function.prototype.apply: ECMA-262
Related on this site
- Function.prototype.bind polyfill
- call, apply, and bind
- this binding rules
- Rest and spread syntax
- Strict mode
- Debounce implementation
- Polyfill questions playbook
Related guides
- Function.prototype.bind PolyfillImplement Function.prototype.bind: bound this, prepended arguments, and new Bound() that still uses the original function prototype.
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.
- Array.forEach PolyfillImplement Array.prototype.forEach: visit present elements, skip holes, ignore the callback return, and honor thisArg without using native forEach.