⚑New: We've launched a brand new Coding Challenges section! Check out these interactive, real-world exercises to level up your skills.Explore Challengesβ†’
FrontendPrep
javascriptMedium

JavaScript call, apply, and bind: The Ultimate Frontend Interview Guide

Loading...

Master explicit function context binding in JavaScript. Learn the differences between call, apply, and bind with visual analogies, production examples, and advanced custom polyfills (handling constructors and currying).

Arvind M
Arvind Mβ€’LinkedIn

JavaScript call, apply, and bind: The Ultimate Frontend Interview Guide

One of the most foundational concepts in JavaScript is execution context and how the this keyword is evaluated. In coding interviews, you will frequently face questions like:

"What is the difference between call, apply, and bind? When would you use each, and how do you write custom polyfills for them from scratch?"

While junior candidates can list the syntax, senior candidates are expected to write robust polyfills that account for edge cases (such as function currying and preserving the prototype chain when instantiated as a constructor) and explain their impact on performance and framework architectures like React.


1. The Core Concepts

In JavaScript, the value of this is dynamicβ€”it is not determined by where a function is declared, but by how it is called.

The methods call, apply, and bind are built-in utilities on Function.prototype that allow you to override this default behavior and explicitly declare what object this should point to.

The Rental Car Analogy πŸš—

Imagine you are a driver (a function). You know how to execute actions (drive, steer, brake), but you don't own a car (the execution context, this). To drive, you must rent a car:

  • call(): You rent a Sports Car and drive it immediately. You invite passengers along, listing them one-by-one (call(sportsCar, passengerA, passengerB)).
  • apply(): You rent an SUV and drive it immediately. You pack all your passengers together in a single luggage bag (an array) (apply(suv, [passengerA, passengerB])).
  • bind(): You reserve a Luxury Sedan for the weekend. The agency doesn't make you drive it immediately; instead, they hand you a key (a new bound function). You can use this key to drive the sedan at any point in the future. You can lock in some passengers now, or add them later when you actually drive.

2. Deep Dive: Syntax & Production Use Cases

Let's explore each method's syntax and see how they are applied in real-world scenarios.

A. call()

Invokes the function immediately, binding its this context to the first argument. Additional parameters are passed individually as a comma-separated list.

Syntax

func.call(thisArg, arg1, arg2, ...);

Basic Example

function introduce(hobby, city) {
  console.log(`Hi, I'm ${this.name}. I love ${hobby} and live in ${city}.`);
}
 
const person = { name: "Sarah" };
introduce.call(person, "hiking", "Seattle");
// Outputs: "Hi, I'm Sarah. I love hiking and live in Seattle."

Real-World Use Case: Method Borrowing

Method borrowing allows an object to use a method from another object without copying it. A classic use case is invoking Object.prototype methods safely on objects that might not inherit them (e.g., objects created with Object.create(null)).

const user = Object.create(null); // No prototype chain
user.role = "admin";
 
// ❌ Calling directly will crash because user.hasOwnProperty is undefined
// user.hasOwnProperty("role");
 
// βœ… Safe borrowing from Object.prototype
const hasRole = Object.prototype.hasOwnProperty.call(user, "role");
console.log(hasRole); // true

B. apply()

Invokes the function immediately, binding its this context to the first argument. Additional parameters are passed as a single array (or array-like object).

Syntax

func.apply(thisArg, [argsArray]);

Basic Example

introduce.apply(person, ["painting", "Boston"]);
// Outputs: "Hi, I'm Sarah. I love painting and live in Boston."

Real-World Use Case: Handing Arrays to Variadic Functions

Before the introduction of the ES6 spread operator (...), apply was the standard way to pass an array of numbers to functions like Math.max or Math.min, which expect individual arguments.

const temperatures = [42, 102, 56, 88, 91];
 
// Math.max expects Math.max(num1, num2, ...)
// null is passed as context because Math.max does not use `this`
const hottest = Math.max.apply(null, temperatures);
console.log(hottest); // 102
 
// Modern ES6 equivalent: Math.max(...temperatures)

C. bind()

Does not invoke the function immediately. Instead, it returns a new copy of the function with the this context permanently locked to the first argument. Any arguments passed to bind are prepended to the arguments passed to the returned function when it is eventually invoked (partial application).

Syntax

const boundFunc = func.bind(thisArg, arg1, arg2, ...);

Basic Example

const boundIntroduce = introduce.bind(person, "photography");
// The first argument "photography" is now locked in.
// We pass the remaining arguments when calling the bound function.
boundIntroduce("Chicago");
// Outputs: "Hi, I'm Sarah. I love photography and live in Chicago."

Real-World Use Case: Preserving Context in Asynchronous Callbacks

When passing object methods as callbacks (e.g., to event listeners or timers), the context is lost because the function is executed by another runner. bind is used to lock the context.

class Timer {
  constructor() {
    this.seconds = 0;
  }
 
  start() {
    // ❌ BUG: 'this' inside the callback will point to the global/window object
    // setInterval(function() { this.seconds++ }, 1000);
 
    // βœ… Context locked to the current Timer instance
    setInterval(
      function () {
        this.seconds++;
        console.log(this.seconds);
      }.bind(this),
      1000,
    );
  }
}

3. Side-by-Side Comparison Matrix

Featurecall()apply()bind()
InvocationImmediateImmediateDeferred (Returns a wrapper function)
ArgumentsList (arg1, arg2, ...)Array ([arg1, arg2, ...])List (arg1, arg2, ...)
Return ValueResult of the functionResult of the functionA new bound copy of the function
Can Re-bind?Yes, on each callYes, on each callNo, context is locked once bound
Main Use CaseMethod borrowingArray parameters (pre-ES6)Async callbacks, partial application

4. Writing Custom Polyfills (From First Principles)

Writing custom versions of these prototypes is a classic frontend interview challenge. It demonstrates your mastery of closures, prototypes, and how functions interact with objects.

A. Polyfill for call (myCall)

The core concept is to temporarily attach the function as a property on the context object, execute it from there (so this implicitly refers to that object), and then clean up the property.

Function.prototype.myCall = function (context, ...args) {
  // 1. Handle null/undefined context or box primitive types
  if (context === null || context === undefined) {
    context = typeof window !== "undefined" ? window : globalThis;
  } else {
    context = Object(context);
  }
 
  // 2. Create a unique symbol key to avoid overwriting existing properties
  const uniqueKey = Symbol("tempFn");
 
  // 3. Temporarily attach the function (represented by 'this') to the context
  context[uniqueKey] = this;
 
  // 4. Invoke the function and store the result
  const result = context[uniqueKey](...args);
 
  // 5. Clean up the added property
  delete context[uniqueKey];
 
  // 6. Return the execution result
  return result;
};

B. Polyfill for apply (myApply)

This follows the exact same logic as myCall, but handles arguments passed as a single array.

Function.prototype.myApply = function (context, argsArray) {
  if (context === null || context === undefined) {
    context = typeof window !== "undefined" ? window : globalThis;
  } else {
    context = Object(context);
  }
  const uniqueKey = Symbol("tempFn");
 
  context[uniqueKey] = this;
 
  // Ensure argsArray is array-like or empty
  const safeArgs = Array.isArray(argsArray) ? argsArray : [];
 
  const result = context[uniqueKey](...safeArgs);
 
  delete context[uniqueKey];
  return result;
};

C. Polyfill for bind (myBind)

An interviewer will usually check if your bind polyfill supports:

  1. Partial Application / Currying: Merging arguments supplied at binding time with arguments supplied at invocation time.
  2. Constructor Support (Senior Requirement): If the bound function is instantiated with the new keyword, the locked context must be ignored, and this should point to the new object being created, while retaining the prototype chain.
Function.prototype.myBind = function (context, ...bindArgs) {
  // 1. Safety check
  if (typeof this !== "function") {
    throw new TypeError(
      "Function.prototype.myBind must be called on a function",
    );
  }
 
  const targetFn = this;
 
  // 2. Return a new wrapper function
  const boundFn = function (...callArgs) {
    // Check if the function is being used as a constructor (with 'new')
    // If 'this' is an instance of boundFn, it means it's called as 'new boundFn()'
    const isConstructor = this instanceof boundFn;
 
    // If called as constructor, use 'this' (the new instance) as context, otherwise 'context'
    return targetFn.apply(isConstructor ? this : context, [
      ...bindArgs,
      ...callArgs,
    ]);
  };
 
  // 3. Preserve the prototype chain of the target function
  if (targetFn.prototype) {
    boundFn.prototype = Object.create(targetFn.prototype);
  }
 
  return boundFn;
};

Verification of Constructor Support:

function Car(make, model) {
  this.make = make;
  this.model = model;
}
 
const dummyContext = { make: "Dummy" };
const BoundCar = Car.myBind(dummyContext, "Tesla");
 
// 1. Standard call uses context:
// (Doesn't modify context here directly since Car returns nothing, but uses correct binds)
 
// 2. Instantiated with 'new':
const myCar = new BoundCar("Model 3");
console.log(myCar.make); // "Tesla" (bound argument preserved)
console.log(myCar.model); // "Model 3" (invocation argument combined)
console.log(myCar instanceof Car); // true (prototype chain preserved!)

5. React Integration & Modern Pitfalls

❌ The Pitfall: Re-binding inside Render

In React, functions passed to child components as props must maintain stable references. Binding functions or using inline arrow functions inside render() (or in the JSX of a functional component) creates a brand new function reference on every single render cycle.

// ⚠️ POOR PERFORMANCE IMPLEMENTATION
function List({ items }) {
  const handleClick = (id) => {
    console.log("Selected item:", id);
  };
 
  return (
    <ul>
      {items.map((item) => (
        // ❌ Creates a new function instance on every single render cycle
        <ListItem key={item.id} onClick={handleClick.bind(null, item.id)} />
      ))}
    </ul>
  );
}

Why does this cause issues?

If ListItem is wrapped in React.memo for performance optimization, it will still re-render on every render of the parent List because handleClick.bind(...) generates a new function reference every time, defeating the memoization.

βœ… The Solution: Stable Handlers & Child Component Scopes

Pass static identifiers down to the child component and let the child component pass them back up, or use a memoized callback.

// Parent component passes static handler
function List({ items }) {
  const handleClick = useCallback((id) => {
    console.log("Selected item:", id);
  }, []);
 
  return (
    <ul>
      {items.map((item) => (
        <ListItem key={item.id} id={item.id} onClick={handleClick} />
      ))}
    </ul>
  );
}
 
// Child component invokes handler with its own ID
const ListItem = React.memo(({ id, onClick }) => {
  return <li onClick={() => onClick(id)}>Item {id}</li>;
});

6. Crucial Gotchas & Edge Cases

Gotcha 1: Arrow Functions Ignore Explicit Binding

Arrow functions do not have their own this binding. They lexically resolve this from their outer scope at creation time. Calling call, apply, or bind on an arrow function will simply execute the function, ignoring the passed context argument.

const obj = { name: "Dave" };
const greet = () => console.log(`Hello, ${this.name}`);
 
greet.call(obj); // Hello, undefined (or global name)

Gotcha 2: The Double Binding Trap

What happens if you bind a function twice?

function print() {
  console.log(this.value);
}
 
const obj1 = { value: 1 };
const obj2 = { value: 2 };
 
const bound1 = print.bind(obj1);
const bound2 = bound1.bind(obj2);
 
bound2(); // What prints? 1 or 2?

Output: 1. Why? Once a function is bound, this is permanently locked. Internally, bound1 is a wrapper that executes print.apply(obj1). Calling bound2 changes the this context of bound1's wrapper, but when bound1 runs, it still executes print using apply(obj1). You cannot override a bound function's context.

Gotcha 3: Non-strict vs. Strict Mode Box-out

In non-strict mode, passing null or undefined as the context to call, apply, or bind causes it to default to the global object (window or global). In addition, primitives (like numbers or strings) are boxed into objects (e.g. 2 becomes Number(2)).

In strict mode ("use strict"), the context remains exactly what was passed.

function checkContext() {
  "use strict";
  console.log(this);
}
 
checkContext.call(null); // null
checkContext.call(42); // 42 (not Number object)

Senior-Level Interview Answer

"In JavaScript, the execution context this is dynamically evaluated at runtime. call, apply, and bind are methods on Function.prototype used to explicitly control this binding. Both call and apply invoke the function immediately, differing only in argument delivery: call accepts a comma-separated list, whereas apply expects an array. Conversely, bind returns a new wrapper function with the context and optional arguments bound to its lexical scope for deferred execution.

In interviews, when asked to build custom implementations, a naive approach handles basic context application using objects. A production-ready polyfill, however, must leverage unique symbols to prevent context pollution, support functional currying, handle strict-mode differences, and override context binding when the function is instantiated as a constructor using the new operator. In frameworks like React, we avoid in-render binds to keep function references stable, preventing child components from performing unnecessary updates."


Common Interview Mistakes Checklist

  • Forgot to return the execution result: In myCall and myApply, ensure you return the value of the function invocation, otherwise they will return undefined.
  • Assumed bind is always called normally: Ensure your custom bind polyfill handles constructor calls (this instanceof boundFn) so it doesn't break when instantiated with new.
  • Lacked prototype inheritance: Forgot to copy or inherit the target function's prototype (boundFn.prototype = Object.create(targetFn.prototype)) in the bind polyfill, breaking prototype chain checks.
  • Polluted the context object: Adding a plain string key like context.tempFn = this inside the polyfill instead of using a Symbol can overwrite existing properties on the target context.

Finished practicing this challenge?

Mark it as completed to track your progress, or bookmark it to review later.

Loading...

Share this Resource

Help other developers level up by sharing this study guide.

⚑ Weekly newsletter

Crack Your Next Frontend Interview.

Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.

  • Senior level React, JS, and CSS interview blueprints
  • System Design & performance optimization deep-dives
  • 100% free, zero spam, unsubscribe with one click

Join the Study Track

We value your privacy. Unsubscribe at any time.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.