InterviewsVector
intermediateCommon5 min read · Updated Jul 18, 2026

Implement Function.prototype.call from Scratch

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.


The problem

Without using the built-in call, apply, or bind, implement Function.prototype.myCall(thisArg, ...args) so that fn.myCall(obj, a, b) behaves exactly like fn.call(obj, a, b).

Interviewers ask this because the solution forces you to demonstrate three things at once:

  • How 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.
  • Symbols — you must attach the function to the target object without colliding with, or overwriting, its existing properties.
  • Side-effect hygiene — you're mutating an object the caller owns; a correct answer leaves no trace, even when the function throws.

Where you see it in production

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 → arrays

Being 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.

The key insight

obj.method() sets this to obj. So: temporarily make the function a property of thisArg, invoke it as a method, then remove the property.

Naive attempt — and why it fails

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;
};
  1. 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.
  2. A string key can collide. If context already has an fn property, you shadow it during the call and then delete the user's data on the way out.
  3. No cleanup on throw. If the function throws, delete never runs and the temporary property leaks onto the caller's object.

Correct implementation

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
  }
};

Dry run

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 started

Edge cases interviewers probe

  • Falsy-but-valid thisArgfn.myCall(0) must bind to Object(0) (a Number wrapper), not globalThis. This is the #1 planted bug in this question.
  • A function that throws — the try/finally is what separates a demo from a correct answer. Without it, thisArg keeps a stray Symbol property forever.
  • Frozen objectsObject.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.
  • Strict mode differences — real 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."

Common mistakes

  • context || globalThis instead of context == null (rebinds 0, '', false, NaN).
  • Skipping Object(thisArg) — assigning a property to a primitive silently no-ops in sloppy mode, then context[key] is undefined and the invocation throws.
  • Using a string key, or an enumerable Symbol property, both of which are observable by the called function.
  • delete outside finally.
  • Forgetting the return value entirely (surprisingly common under interview pressure).

call vs apply vs bind

Invokes immediatelyArgumentsReturns
call(thisArg, a, b)yeslisted individuallythe function's return value
apply(thisArg, [a, b])yesone array-likethe function's return value
bind(thisArg, a)nopartially applied, rest at call timea 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.

Follow-up questions

  • "Why a Symbol and not a string key?" — uniqueness is guaranteed; no collision, no shadowing, and with enumerable: false the called function can't accidentally observe it.
  • "What if 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.
  • "How would the behavior differ in strict mode?" — strict functions receive null/primitives as-is; sloppy functions get globalThis/boxed values. The polyfill reproduces sloppy semantics only.
  • "Can you do this without mutating the object?"Reflect.apply, or Function.prototype.bind (which you'd then be asked to implement).
  • "Where does 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.

  • The new Operator

    intermediate

    Implement myNew(Constructor, ...args): prototype linking, this binding, and the return-object override rule.

  • Custom instanceof

    intermediate

    Walk the prototype chain by hand, then meet Symbol.hasInstance — and learn where instanceof lies to you.