Implement Function.prototype.call and apply from scratch
The Short Answer
call and apply both invoke a function immediately while letting you choose what this points to. The only difference is how they receive arguments: call takes them one by one (comma-separated), while apply takes them as a single array. Everything else about the two is identical.
Reimplementing them is a classic interview warm-up because the trick is delightfully simple: assign the function as a temporary property on the context object, call it through that property so this resolves naturally, then clean up. The snippet below shows the native behaviour our polyfills must match.
const user = { name: "Grace" };
function introduce(role: string, team: string) {
return `${this.name} is a ${role} on ${team}`;
}
// call — arguments listed individually
introduce.call(user, "engineer", "Platform");
// apply — arguments as an array
introduce.apply(user, ["engineer", "Platform"]);
The Core Trick
The whole technique rests on one rule of JavaScript: when you call a function as a method of an object (obj.fn()), this inside that function becomes obj. So if we temporarily attach the target function to the context and call it from there, this is set for free — no special machinery required. These are the steps our implementation follows.
How it works
- Take the context object (default to globalThis if none is given)
- Add the function as a temporary property on that context
- Invoke it through the property so `this` resolves to the context
- Capture the return value
- Delete the temporary property so we leave the object untouched
Implementing call
Here this inside myCall is the function we want to invoke. We use a unique Symbol as the temporary key so we never clobber an existing property on the context. After invoking, we delete the key and return the result. Watch how the rest parameters collect the comma-separated arguments into an array we can spread.
Function.prototype.myCall = function (context, ...args) {
// null/undefined context falls back to the global object
context = context ?? globalThis;
// Symbol key avoids overwriting any real property
const fnKey = Symbol("fn");
context[fnKey] = this; // `this` is the function myCall was called on
const result = context[fnKey](...args); // `this` inside === context
delete context[fnKey]; // clean up after ourselves
return result;
};
// usage
console.log(introduce.myCall(user, "engineer", "Platform"));
// "Grace is a engineer on Platform"
Why a Symbol key
If we used a string like context.fn, we might overwrite an existing fn property on the object and corrupt it. A fresh Symbol is guaranteed unique, so the temporary assignment can never collide with real data.
Implementing apply
apply is almost identical — the only change is the signature. Instead of rest parameters it accepts a single args array, which we spread into the call. We also guard against a missing array so calling with no arguments still works exactly like the native version.
Function.prototype.myApply = function (context, args) {
context = context ?? globalThis;
const fnKey = Symbol("fn");
context[fnKey] = this;
// args may be undefined — spread an empty array in that case
const result = context[fnKey](...(args ?? []));
delete context[fnKey];
return result;
};
// usage
console.log(introduce.myApply(user, ["engineer", "Platform"]));
// "Grace is a engineer on Platform"
call vs apply at a Glance
Since the two differ only in argument shape, choosing between them is purely about which form your data already has. This table sums up the practical distinction interviewers expect you to state crisply.
| Aspect | call | apply |
|---|---|---|
| Argument form | Listed individually | Single array |
| Invokes immediately | Yes | Yes |
| Sets this | Yes | Yes |
| Best when | You know the args upfront | Args are already in an array |
Common Mistakes
Forgetting to delete the temporary property
Leaving the function attached to the context object pollutes it with an unexpected key that can leak into loops, serialization, or later lookups.
✅Always `delete context[fnKey]` before returning, and prefer a Symbol key so cleanup is collision-free.
Ignoring null or undefined context
Native call/apply fall back to the global object (non-strict) when context is null or undefined. Skipping this throws a 'cannot set property of null' error.
✅Default the context with `context = context ?? globalThis` before using it.
Why Interviewers Ask This
This question checks whether you understand the single most important rule about this: it is decided by how a function is called, not where it is defined. Candidates who reach for the temporary-property trick demonstrate they grasp method invocation at a fundamental level, and handling the Symbol key and null-context edge cases shows production-grade attention to detail.
Quick Revision Cheat Sheet
Both: Invoke the function immediately with a chosen `this`
call: Arguments passed individually: fn.call(ctx, a, b)
apply: Arguments passed as an array: fn.apply(ctx, [a, b])
Core trick: Attach fn to the context, call it, then delete it
Safety: Use a Symbol key and default null context to globalThis