Function.prototype.apply
intermediateSame trick as call but with an arguments array — plus the subtle differences that interviewers probe.
Implement Function.prototype.call in JavaScript with Symbol keys, correct null/undefined handling, primitive boxing, and try/finally cleanup — plus the follow-ups interviewers probe.
Without using the built-in
call,apply, orbind, implementFunction.prototype.myCall(thisArg, ...args)so thatfn.myCall(obj, a, b)behaves exactly likefn.call(obj, a, b).
Interviewers ask this because the solution forces you to demonstrate three things at once:
this is actually bound — the only way to set this without call/apply is method invocation: obj.fn() binds this to obj. The whole trick is manufacturing that call site.You rarely call call on your own functions, but method borrowing is everywhere:
Object.prototype.toString.call(value) // reliable type tags
Object.prototype.hasOwnProperty.call(obj, k) // safe on Object.create(null)
Array.prototype.slice.call(arguments) // pre-ES6 array-likes → arraysBeing able to say why these are borrowed (the receiver doesn't inherit the method, or its own version is untrustworthy) is a senior signal on its own.
obj.method() sets this to obj. So: temporarily make the function a property of thisArg, invoke it as a method, then remove the property.
Function.prototype.myCall = function (context, ...args) {
context = context || globalThis; // ❌ bug 1
context.fn = this; // ❌ bug 2
const result = context.fn(...args);
delete context.fn; // ❌ bug 3
return result;
};context || globalThis rebinds every falsy value. fn.call(0), fn.call(''), and fn.call(false) must bind to (a boxed version of) those primitives — only null and undefined fall back to the global object. The check must be context == null.context already has an fn property, you shadow it during the call and then delete the user's data on the way out.delete never runs and the temporary property leaks onto the caller's object.Function.prototype.myCall = function (thisArg, ...args) {
if (typeof this !== "function") {
throw new TypeError(`${this} is not a function`);
}
// Only null/undefined fall back to globalThis (sloppy-mode semantics).
// Primitives are boxed: fn.call(42) sees this as a Number object.
const context = thisArg == null ? globalThis : Object(thisArg);
// A unique Symbol can never collide with existing properties.
const key = Symbol("myCall");
Object.defineProperty(context, key, {
value: this,
configurable: true,
enumerable: false, // invisible to Object.keys, spreads, JSON.stringify
});
try {
return context[key](...args); // method invocation binds `this`
} finally {
delete context[key]; // runs even if the function throws
}
};function greet(greeting) {
return `${greeting}, ${this.name}`;
}
const alice = { name: "Alice" };
greet.myCall(alice, "Hello");
// 1. thisArg is alice (not null) → context = Object(alice) = alice
// 2. alice[Symbol(myCall)] = greet (non-enumerable, no collision)
// 3. context[key]("Hello") → method call → this === alice → "Hello, Alice"
// 4. finally: delete alice[Symbol(myCall)] → alice is exactly as it startedthisArg — fn.myCall(0) must bind to Object(0) (a Number wrapper), not globalThis. This is the #1 planted bug in this question.try/finally is what separates a demo from a correct answer. Without it, thisArg keeps a stray Symbol property forever.Object.defineProperty on a frozen thisArg throws. Real call works on frozen objects because it never touches the object at all — it sets this at the spec level ([[Call]]). This is the honest limitation of the temp-property trick; naming it unprompted is a strong senior signal.call in a strict-mode function passes null and primitives through unboxed. The temp-property approach can't reproduce that; say so rather than pretending.Reflect.apply(fn, thisArg, args) — the modern, trick-free way to do the same thing. Interviewers often accept "in production I'd use Reflect.apply; the Symbol trick is how you'd polyfill it."context || globalThis instead of context == null (rebinds 0, '', false, NaN).Object(thisArg) — assigning a property to a primitive silently no-ops in sloppy mode, then context[key] is undefined and the invocation throws.delete outside finally.| Invokes immediately | Arguments | Returns | |
|---|---|---|---|
call(thisArg, a, b) | yes | listed individually | the function's return value |
apply(thisArg, [a, b]) | yes | one array-like | the function's return value |
bind(thisArg, a) | no | partially applied, rest at call time | a new function |
The implementations escalate in the same order: apply is call with an array, and bind adds closures and the new-operator edge case.
enumerable: false the called function can't accidentally observe it.thisArg is frozen or sealed?" — the trick fails; native call doesn't. There's no userland fix — that's why [[Call]] is an engine-level operation.null/primitives as-is; sloppy functions get globalThis/boxed values. The polyfill reproduces sloppy semantics only.Reflect.apply, or Function.prototype.bind (which you'd then be asked to implement).this come from in the first place?" — call-site binding rules; see the this keyword.Same trick as call but with an arguments array — plus the subtle differences that interviewers probe.
Implement bind including partial application and the new-operator edge case most candidates miss.
Implement myNew(Constructor, ...args): prototype linking, this binding, and the return-object override rule.
Walk the prototype chain by hand, then meet Symbol.hasInstance — and learn where instanceof lies to you.