Skip to content
JavaScript js advanced 4 min read

Mastering this

The this keyword is the single most misunderstood feature in JavaScript, and for good reason: its value is decided at call time, not where the function is defined. Once you internalize the small set of rules that govern how this is bound, the “mysterious” behavior in callbacks, event handlers, and class methods stops being mysterious. This page walks through every binding rule in priority order, the edge cases that trip people up, and the canonical fixes.

How this gets its value

this is not a variable you assign — it is an implicit parameter set when a function is invoked. The crucial question is always: how was this function called? There are four binding rules, and they apply in a strict priority order. When more than one could apply, the one higher in the list wins.

PriorityRuleHow it’s triggeredWhat this becomes
1 (highest)new bindingnew Fn()the freshly created object
2Explicit bindingfn.call(obj), fn.apply(obj), fn.bind(obj)the passed object
3Implicit bindingobj.fn()the object left of the dot
4 (lowest)Default bindingfn() (standalone)undefined (strict) or globalThis (sloppy)

Arrow functions sit outside this table entirely — they have no own this and ignore all four rules (covered below).

Default and implicit binding

A plain function call uses default binding. In strict mode (which all modules and classes are), this is undefined; in sloppy mode it silently falls back to the global object.

'use strict';
function whoAmI() {
  return this;
}
console.log(whoAmI()); // undefined (strict)

const user = {
  name: 'Ada',
  greet() {
    return `Hi, ${this.name}`;
  },
};
console.log(user.greet()); // implicit binding -> user

Output:

undefined
Hi, Ada

Only the final property access matters. a.b.c.method() binds this to a.b.c, not a.

Explicit binding with call, apply, and bind

call and apply invoke immediately with an explicit this; they differ only in how arguments are passed. bind returns a new function permanently locked to a this, no matter how it is later called.

function introduce(greeting, punctuation) {
  return `${greeting}, I'm ${this.name}${punctuation}`;
}
const dev = { name: 'Lin' };

console.log(introduce.call(dev, 'Hello', '!'));      // args listed
console.log(introduce.apply(dev, ['Hey', '.']));     // args as array
const boundIntro = introduce.bind(dev, 'Yo');
console.log(boundIntro('?'));                         // pre-filled greeting

Output:

Hello, I'm Lin!
Hey, I'm Lin.
Yo, I'm Lin?

A bound function cannot be re-bound. Once bind locks this, later .call(other) is ignored. This makes bind the safest fix for passing a method as a callback.

The edge cases that break this

The “I lost my this” bugs almost always come from a method being detached from its object — implicit binding silently degrades to default binding.

Destructured or assigned methods lose the dot:

const counter = {
  count: 0,
  inc() {
    this.count++;
  },
};
const { inc } = counter;
// inc(); // TypeError: cannot read 'count' of undefined

Callbacks passed to other functions are called bare:

class Timer {
  seconds = 0;
  start() {
    // setTimeout calls the callback with no object -> default binding
    setTimeout(function () {
      this.seconds++; // 'this' is NOT the Timer
    }, 1000);
  }
}

DOM event handlers set this to the element when you use a regular function:

button.addEventListener('click', function () {
  console.log(this); // the <button> element, not your component
});

Arrow functions: lexical this

Arrow functions do not get their own this. Instead they close over the this of the enclosing lexical scope at definition time. This is exactly what fixes the setTimeout and event-handler problems, because the arrow simply reuses the surrounding this.

class Timer {
  seconds = 0;
  start() {
    setTimeout(() => {
      this.seconds++; // 'this' is the Timer instance, lexically captured
      console.log(this.seconds);
    }, 0);
  }
}
new Timer().start();

Output:

1

Because arrows ignore the binding rules, call, apply, and bind cannot change their this — they only pass arguments. Never use an arrow as an object method that needs this, and never as a DOM handler that needs this to be the element.

The three canonical fixes

When you must hand a method to something that will call it bare, choose one of these:

// 1. bind in the constructor (class fields era)
class C {
  handle = () => this.value;        // arrow field: auto-lexical
}

// 2. bind explicitly when passing
element.addEventListener('click', this.onClick.bind(this));

// 3. store a reference (older pattern, still valid)
function legacy() {
  const self = this;
  return function () {
    return self.data;
  };
}
const api = {
  base: 'https://devcraftly.com',
  // arrow field-style method keeps 'this' bound to api
  url(path) {
    const build = () => `${this.base}/${path}`;
    return build();
  },
};
console.log(api.url('docs'));

Best Practices

  • Always ask “how is this function called?” — definition location is irrelevant for regular functions.
  • Use arrow functions for callbacks, timers, and array iterators when you want to keep the surrounding this.
  • Prefer class fields with arrow functions (onClick = () => {}) for React-style handlers to avoid manual bind calls.
  • Never use an arrow function as an object method or prototype method that relies on this.
  • Use bind (not re-bindable) when passing a method where you need a permanent, guaranteed context.
  • Enable strict mode everywhere — silent fallback to globalThis hides bugs that strict mode surfaces as clear TypeErrors.
Last updated June 1, 2026
Was this helpful?