Skip to main content

Command Palette

Search for a command to run...

The Magic of this, call(), apply(), and bind() in JavaScript

Learn about this keyword and learn how to control function context like a pro.

Published
7 min read
H
CS Graduate | Technical Writing | Software development | 20K+ impressions

If you ask any JavaScript developer what the most confusing part of the language is, there is a very high chance they will say the this keyword.

It behaves differently depending on where you use it, how you call it, and whether or not you are in strict mode. But once you understand the simple mental model behind it, the mystery disappears. And once you master the tools to manipulate it—call(), apply(), and bind()—you unlock a whole new level of flexibility in your code.

Here is your straightforward, jargon-free guide to understanding context in JavaScript.


What does this actually mean?

Instead of diving into deep compiler internals and execution contexts, let’s use a simple golden rule:

this simply refers to "who is calling the function right now."

Imagine a function is a phone call. If you pick up the phone and ask, "Who is this?", the answer depends entirely on who dialed your number.

In JavaScript, this is a dynamic reference to the object that invoked (called) the function.

1. this Inside Objects

When a function is stored inside an object, it is called a method. When you call that method, this points directly to the object that holds it.

const user = {
  name: "Alice",
  age: 25,
  introduce: function() {
    // "this" refers to the 'user' object because 'user' called the function
    console.log(`Hi, I am \({this.name} and I am \){this.age} years old.`);
  }
};

user.introduce(); 
// Output: Hi, I am Alice and I am 25 years old.

Who is calling the function? The user object. Therefore, this.name translates to user.name.

2. this Inside Normal (Standalone) Functions

When you call a function that is not a property of an object—just a plain function call—this depends on strict mode:

  • In non-strict mode, this falls back to the global object (in browsers, window or globalThis).

  • In strict mode, this becomes undefined.

Examples:

function show() {
  console.log(this);
}

show(); // non-strict: window (in browser)
        // strict: undefined (if 'use strict' at top)

Why that matters: if you expect this to refer to some object but you call the function without that object as the caller, you get the global object or undefined instead—leading to bugs (e.g., accidentally creating globals, or TypeErrors when accessing properties on undefined).

Common gotcha: extracting a method out of its object:

const user = {
  name: 'Ada',
  greet() { console.log(this.name); }
};

const greet = user.greet;
greet(); // undefined (strict) or '' (global.name) — you lost the user binding

To preserve the intended this, use explicit binding (next section), call it as a method (user.greet()), or use an arrow function bound in the right scope.

3. this with Constructors and new

When you call a function with new, JavaScript:

  1. Creates a fresh object,

  2. Sets this inside the function to that new object,

  3. Returns the object (unless the function explicitly returns another object).

Example:

function Person(name) {
  this.name = name;
}
const p = new Person('Lin');
console.log(p.name); // "Lin"

Important precedence rule (short): "new" binding takes priority over implicit/explicit binding. If you try to force a this with call/apply on a constructor, new will still create the instance and use it as this instead.

4. this in Arrow Functions (lexical this)

Arrow functions do not have their own this. Instead they inherit this from the surrounding (lexical) scope — like closures for this.

const obj = {
  value: 42,
  regular() { console.log(this.value); },      // 42
  arrow: () => console.log(this.value)         // likely undefined or outer scope value
};

obj.regular(); // 42
obj.arrow();   // not 42 — arrow's `this` is set where the function was defined

Use arrow functions when you want to preserve the outer this (commonly used in callbacks), but do NOT use them as object methods or constructors.

5. Explicit Binding: call(), apply(), bind()

These are the explicit tools to set this for a function.

  • fn.call(thisArg, ...args) — call fn immediately with thisArg and a list of args.

  • fn.apply(thisArg, argsArray) — call fn immediately with thisArg and arguments as an array.

  • fn.bind(thisArg, ...boundArgs) — return a NEW function permanently bound to thisArg (and optionally pre-filled args).

Examples and typical use cases:

Borrowing a method:

const obj = { name: 'Ada' };
function say(greeting) {
  console.log(greeting + ', ' + this.name);
}

say.call(obj, 'Hello');           // "Hello, Ada"
say.apply(obj, ['Hi']);           // "Hi, Ada"

Using apply with arrays (common pre-spread era):

const nums = [3, 1, 4];
Math.max.apply(null, nums);       // 4
// Modern alternative: Math.max(...nums)

Partial application with bind:

function multiply(a, b) { return a * b; }
const double = multiply.bind(null, 2);
console.log(double(5)); // 10

Keeping method this when passing as callback:

class Button {
  constructor() {
    this.count = 0;
    this.onClick = this.onClick.bind(this); // ensure `this` inside onClick is the instance
  }
  onClick() {
    this.count++;
    console.log(this.count);
  }
}
const btn = new Button();
setTimeout(btn.onClick, 100); // Works because bound

Notes:

  • call vs apply: same effect; apply accepts arguments as an array.

  • bind returns a new function; use it when you need a stable this for future calls (event handlers, callbacks).

  • Historically, null/undefined passed to call/apply default to global object in non-strict mode; in strict mode they remain null/undefined.

Precedence summary (most to least):

  1. new

  2. call/apply/bind (explicit)

  3. method-call (implicit)

  4. global/default

Edge detail: using bind and then new — the bound thisArg is ignored when constructing; the new instance becomes this, but bound arguments remain prepended.

6. Practical Patterns & Real-World Examples

  • Method borrowing:

    • Use Array.prototype.slice.call(arguments) (older pattern) to convert arguments to array. Now prefer Array.from(arguments) or rest params.
  • Event handlers in classes:

    • Bind handlers in constructor, or use class fields with arrow functions:

      handleClick = () => { console.log(this); }
      
  • Partial application:

    • bind is a simple built-in way to pre-fill arguments.
  • Function utilities:

    • call/apply let you reuse generic functions on different objects.

7. Common Pitfalls & How to Avoid Them

  • Losing this when passing methods as callbacks — use bind, arrow functions, or wrap in another function.

  • Incorrect assumptions about this inside arrow functions — remember arrow functions get this lexically.

  • Accidentally relying on global this in non-strict mode — always use strict mode or prefer explicit bindings.

  • Overusing bind for performance-critical hot paths — binds create new functions each time; cache the bound function if reused.

8. Quick Cheat Sheet

  • Method call: obj.fn()this is obj.

  • Function call: fn() → non-strict: global; strict: undefined.

  • Constructor: new Fn()this is the new instance.

  • Arrow function: () => {}this is inherited from outer scope.

  • Explicit: fn.call(obj, ...), fn.apply(obj, [...]), fn.bind(obj) → force this.

Conclusion

Understanding this becomes simple if you apply the golden rule: think about "who is calling the function right now" and remember the four primary binding ways (default, implicit, explicit, and new), plus the special lexical behavior of arrow functions. Once you can identify which binding is in effect, call, apply, and bind give you the tools to control or preserve that binding when you need to.