JavaScript The this Keyword

this is one of the most misunderstood keywords in JavaScript because, unlike most languages, its value is not fixed by where a function is written — it is decided by how the function is called. Every time a function runs, JavaScript sets up a fresh execution context, and part of that context is a binding for this. Understanding the rules that decide that binding is essential for writing classes, methods, event handlers, and callbacks that behave the way you expect.

Overview: What Is this?

this is a special identifier that every (non-arrow) function gets automatically, holding a reference to the object the function is currently “acting on behalf of.” It is not a variable you declare — it is set by the JavaScript engine as part of creating the function’s execution context, and its value can change every single time the same function is invoked, depending on the call-site (the exact expression used to call it), not on where the function was defined.

This is the single biggest source of confusion for people coming from other languages: in many languages, this/self is lexically tied to the class instance a method belongs to. In JavaScript, a method is just a function stored as a property. If you pass that same function reference somewhere else and call it differently, this changes with it. Arrow functions are the one major exception — they do not receive their own this binding at all; instead they capture this lexically from the nearest enclosing non-arrow function (or the top-level scope), exactly like they capture any other outer variable.

Syntax and the Four Binding Rules

this has no declaration syntax — you simply use it inside a function body. What matters is which of four binding rules applies at the call-site. When more than one rule could apply, JavaScript picks the highest-priority one from this table:

Rule Triggered by Value of this
1. New binding new Foo() The newly created object
2. Explicit binding fn.call(obj), fn.apply(obj), fn.bind(obj) The object passed in
3. Implicit binding obj.method() The object left of the dot at the call-site
4. Default binding plainFunction() undefined in strict mode / classes; the global object in non-strict sloppy mode

Arrow functions ignore all four rules — they never have their own this, so none of these bindings apply to them; they simply reuse whatever this was in scope when the arrow function was created.

Examples

Example 1: Default Binding vs. Implicit Binding

"use strict";

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

const person = {
  name: "Maya",
  greet() {
    console.log(`Hello, my name is ${this.name}`);
  }
};

showThis();       // default binding, strict mode
person.greet();   // implicit binding
Output:
undefined
Hello, my name is Maya

Calling showThis() as a bare function triggers default binding. Because the file runs in strict mode, this is undefined rather than falling back to the global object. Calling person.greet() triggers implicit binding: since the call-site is person.greet(), JavaScript sets this to person for the duration of that call.

Example 2: New Binding and Explicit Binding

class Car {
  constructor(brand) {
    this.brand = brand;
  }

  describe() {
    return `This car is a ${this.brand}`;
  }
}

const car1 = new Car("Toyota");
console.log(car1.describe());

function introduce(greeting) {
  return `${greeting}, I am ${this.name}`;
}

const user = { name: "Alex" };

console.log(introduce.call(user, "Hi"));
console.log(introduce.apply(user, ["Hello"]));

const boundIntroduce = introduce.bind(user);
console.log(boundIntroduce("Hey"));
Output:
This car is a Toyota
Hi, I am Alex
Hello, I am Alex
Hey, I am Alex

Calling new Car("Toyota") creates a brand-new object, sets this inside the constructor to that object, and returns it — this is new binding, and it is how every constructor function and class works. The introduce function then demonstrates explicit binding: call and apply invoke the function immediately with a chosen this (they only differ in how they pass arguments — a list for call, an array for apply), while bind returns a brand-new function permanently locked to that this, which you can call later.

Example 3: Losing this When a Method Is Detached

class Counter {
  count = 0;

  incrementMethod() {
    this.count++;
    return this.count;
  }

  incrementArrow = () => {
    this.count++;
    return this.count;
  };
}

const counter = new Counter();
const detachedMethod = counter.incrementMethod;
const detachedArrow = counter.incrementArrow;

console.log(detachedArrow());

try {
  detachedMethod();
} catch (error) {
  console.log("Error:", error.message);
}
Output:
1
Error: Cannot read properties of undefined (reading 'count')

incrementArrow is a class field initialized to an arrow function, so it captures this from the constructor’s scope — the instance — once, permanently, at creation time. Detaching it into detachedArrow and calling it standalone still works. incrementMethod, on the other hand, is a normal method on the prototype; once detached and called as a bare function, default binding kicks in, and because class bodies are always strict mode, this is undefined, so this.count++ throws.

Under the Hood: How the Engine Resolves this

When the JavaScript engine is about to run a function, it creates a new execution context and, as part of that context, determines a this binding by inspecting the call-site — the exact syntax used to invoke the function, read left to right:

  • Was the function called with new? If so, use new binding (rule 1) — a fresh object is created, linked to the function’s prototype, and becomes this.
  • Was the function called via .call(), .apply(), or a function previously produced by .bind()? If so, use explicit binding (rule 2) — the object argument becomes this, no exceptions.
  • Was the function called as a property access, like obj.method()? If so, use implicit binding (rule 3) — the object immediately to the left of the final dot becomes this. Note it is only the last object in a chain that matters: a.b.c.method() binds this to c, not a or b.
  • Otherwise, default binding applies (rule 4): this is undefined in strict-mode code (all ES modules and class bodies are strict by default), or the global object in old-style sloppy-mode scripts.

Arrow functions short-circuit this entire lookup. When the engine creates an arrow function, it does not allocate a this binding for it at all — reading this inside an arrow function is treated exactly like reading any other free variable: the engine walks up the surrounding scope chain to the nearest enclosing function (or the top level) and uses whatever this is bound there. That is also why .call(), .apply(), and .bind() cannot change an arrow function’s this — there is no local binding to overwrite.

Common Mistakes

Mistake 1: Passing a Method as a Callback Loses this

"use strict";

class Button {
  constructor(label) {
    this.label = label;
  }

  handleClick() {
    console.log(`${this.label} was clicked`);
  }
}

function registerHandler(handler) {
  handler();
}

const button = new Button("Submit");

try {
  registerHandler(button.handleClick);
} catch (error) {
  console.log("Error:", error.message);
}
Output:
Error: Cannot read properties of undefined (reading 'label')

Passing button.handleClick hands over the bare function, stripped of the button. part — the connection to the object is gone. When registerHandler later calls handler(), that is a plain function call, so default binding applies and this is undefined. Fix it by locking this with .bind() (commonly done in the constructor):

"use strict";

class Button {
  constructor(label) {
    this.label = label;
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    console.log(`${this.label} was clicked`);
  }
}

function registerHandler(handler) {
  handler();
}

const button = new Button("Submit");
registerHandler(button.handleClick);
Output:
Submit was clicked

Mistake 2: Using an Arrow Function for an Object Method

const wallet = {
  balance: 200,
  showBalance: () => {
    console.log(this === wallet);
  }
};

wallet.showBalance();
Output:
false

It’s tempting to use the shorter arrow syntax everywhere, but an arrow function assigned as an object property never binds this to that object — it captures this from the surrounding scope where the object literal itself was written (here, the top level), which is never the wallet object. Use a regular method (shorthand syntax) instead, which participates in implicit binding correctly:

const wallet = {
  balance: 200,
  showBalance() {
    console.log(this === wallet);
  }
};

wallet.showBalance();
Output:
true

Best Practices

  • Use regular method shorthand (method() {}) for object and class methods that need this to refer to the instance they’re called on.
  • Use arrow functions for callbacks and nested helper functions defined inside a method, where you want them to inherit the surrounding this automatically (e.g. inside array.map(() => ...) within a class method).
  • Prefer arrow-function class fields (handleClick = () => {}) over binding in the constructor when a method will routinely be passed around as a callback (event handlers, timers, promise callbacks).
  • If you must bind in a constructor, do it once (this.method = this.method.bind(this)) rather than creating a new bound function on every render or call.
  • Never rely on default binding falling back to the global object — always write strict-mode code (modules and classes already are) so mistakes surface as clear undefined errors instead of silently mutating global state.
  • Avoid arrow functions for object literal methods and for anything you plan to call with new — arrow functions cannot be used as constructors and will throw a TypeError if you try.
  • When in doubt about what this will be, look only at the call-site — the exact text immediately before the parentheses where the function is invoked — not at where the function was defined.

Practice Exercises

  • Write an object stopwatch with a seconds property and two methods: a regular method tick() that increments seconds and logs it, and a plain function logSeconds defined outside the object that logs this.seconds. Call stopwatch.tick() normally, then try calling logSeconds.call(stopwatch) and predict the output before running it.
  • Create a class Toggle with a boolean field on and a method flip() that inverts it and logs the new value. Detach flip from an instance into a standalone variable and call it directly — explain in your own words why it throws, then fix it two different ways (constructor bind, and an arrow class field).
  • Given const obj = { value: 10, getValue: () => this.value };, predict what obj.getValue() returns without running it, then verify your answer and explain why using the binding rules from this lesson.

Summary

  • this is determined by the call-site (how a function is invoked), not by where the function is defined.
  • Four binding rules apply in priority order: new binding, explicit binding (call/apply/bind), implicit binding (obj.method()), and default binding (plain function call).
  • In strict-mode code, functions, classes, and modules, default binding gives this === undefined rather than the global object.
  • Arrow functions have no this of their own — they lexically inherit this from their enclosing scope, and call/apply/bind cannot override that.
  • Detaching a regular method from its object (passing it as a callback) loses its this binding — fix with .bind() or an arrow-function class field.
  • Use regular methods when you need dynamic this tied to the caller, and arrow functions when you want to freeze this to the enclosing context.