InterviewsVector
intermediateCommon4 min read · Updated Jul 18, 2026

Implement Function.prototype.apply from Scratch

Implement Function.prototype.apply in JavaScript: array-like argument handling, null/undefined thisArg semantics, and the differences from call that interviewers probe.


The problem

Implement Function.prototype.myApply(thisArg, argsArray) so it behaves like the built-in apply — including when argsArray is null, undefined, or an array-like rather than a real array.

This is usually asked immediately after implementing call, and the mechanics are 90% identical. The remaining 10% — how apply treats its arguments array — is exactly what the interviewer is watching, so this article focuses on the differences.

Where you see it in production

apply shines when the arguments already live in an array (though rest/spread has replaced most uses):

Math.max.apply(null, numbers);      // pre-ES6; today: Math.max(...numbers)
fn.apply(this, arguments);          // forwarding in old codebases
Reflect.apply(handler, target, args); // the modern equivalent, common in proxies

Knowing that spread made most apply calls obsolete — except when the callee cares about this — is a good aside to offer.

Correct implementation

Same temp-Symbol trick as call; the differences are all in argument handling:

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");
  }
 
  // Only null/undefined fall back to globalThis; primitives are boxed
  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' }),
    // which spread alone would reject (spread needs an iterator).
    return context[key](...Array.from(argsArray));
  } finally {
    delete context[key];
  }
};

The array-like detail most candidates miss

function tag(a, b) {
  return `${this.name}: ${a}, ${b}`;
}
 
const arrayLike = { length: 2, 0: "x", 1: "y" };
 
tag.apply({ name: "ok" }, arrayLike);   // "ok: x, y" — native apply accepts it
tag.myApply({ name: "ok" }, arrayLike); // "ok: x, y" — Array.from makes this work

A naive context[key](...argsArray) throws here, because spread requires Symbol.iterator and plain array-likes don't have one. The spec operation behind apply (CreateListFromArrayLike) reads length and indexes — Array.from is the closest userland equivalent. Mentioning this distinction is usually the moment the interviewer stops asking follow-ups.

Edge cases interviewers probe

  • argsArray of null/undefined → call with no arguments (do not throw).
  • A primitive argsArrayfn.apply(obj, 42) throws a TypeError natively; silently ignoring it is a spec deviation worth naming.
  • Falsy thisArgfn.myApply(0, []) must box 0, not fall back to globalThis; thisArg == null is the only correct check (see the same trap in call).
  • Strings as argsArrayfn.apply(null, "ab") spreads to ('a', 'b'); both Array.from and native behavior agree here since strings are array-like and iterable.
  • A throwing function — cleanup must run; hence try/finally.
  • Frozen thisArg — the temp-property trick fails where native apply doesn't; acknowledge the limitation (details in the call article).

Common mistakes

  • context || globalThis — rebinds falsy primitives.
  • Spreading argsArray directly — breaks on array-likes, the input apply was designed for.
  • Treating null argsArray as an error instead of "no args".
  • Forgetting that apply returns the function's return value.
  • Reaching for arguments-style loops when Array.from states the intent in one call.

call vs apply vs bind

Invokes immediatelyArgumentsReturns
callyesindividuallyfunction's return value
applyyesone array-likefunction's return value
bindnopartial now, rest latera new function

Follow-up questions

  • "Why does apply accept array-likes at all?" — it predates iterables; arguments was the motivating array-like in 1999.
  • "When would you still use apply today?" — when both this and an argument array vary at runtime; otherwise spread reads better. Reflect.apply is the modern choice in meta-programming code.
  • "Implement call using your myApply."Function.prototype.myCall = function (t, ...a) { return this.myApply(t, a); } — a one-liner that shows you see the relationship.
  • "What's the performance story?" — engines optimize both heavily; historically call was marginally faster, but choosing by readability is the correct answer.
  • "How does bind build on this?" — closures over the bound args plus a constructor-call check: implement bind.

  • Implement bind including partial application and the new-operator edge case most candidates miss.

  • Implement call from scratch: temporary method assignment and why Symbol keys avoid collisions.

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