InterviewsVector
intermediateCommon6 min read · Updated Aug 24, 2026

Implement Function.prototype.call and apply from Scratch

Implement call and apply in JavaScript: the Symbol temp-key trick, correct null/undefined handling, primitive boxing, try/finally cleanup, and apply's array-like argument handling.


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 thisArg — fn.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 objects — 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.
  • 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).

apply: same trick, array-like arguments

apply is call with the arguments pre-packed into an array-like. The this-binding machinery is identical — the only new work is turning argsArray into individual arguments correctly:

Function.prototype.myApply = function (thisArg, argsArray) {
  if (typeof this !== "function") {
    throw new TypeError(`${this} is not a function`);
  }
  // apply treats null/undefined argsArray as "no arguments"
  if (argsArray == null) {
    argsArray = [];
  } else if (typeof argsArray !== "object" && typeof argsArray !== "function") {
    // Real apply rejects primitives here (spec: CreateListFromArrayLike)
    throw new TypeError("CreateListFromArrayLike called on non-object");
  }
  const context = thisArg == null ? globalThis : Object(thisArg);
  const key = Symbol("myApply");
  Object.defineProperty(context, key, {
    value: this,
    configurable: true,
    enumerable: false,
  });
  try {
    // Array.from handles array-LIKES ({ length: 2, 0: 'a', 1: 'b' }) that
    // spread alone rejects (spread needs Symbol.iterator).
    return context[key](...Array.from(argsArray));
  } finally {
    delete context[key];
  }
};

The detail most candidates miss is array-likes. fn.apply(obj, { length: 2, 0: "x", 1: "y" }) works natively because the spec operation behind apply (CreateListFromArrayLike) reads length and indexes — it predates iterables. A naive context[key](...argsArray) throws on a plain array-like because spread needs Symbol.iterator; Array.from is the closest userland equivalent. And once you have myApply, call is a one-liner: function myCall(t, ...a) { return this.myApply(t, a); } — showing you see the relationship. (apply's modern replacements: spread Math.max(...nums) for the common case, Reflect.apply(fn, thisArg, args) in meta-programming.)

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 (above) 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.

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

  • Object.assign

    intermediate

    Copy enumerable own properties across sources, with getter evaluation and null-target errors.