JavaScript by Example

Classes and `this`

The class syntax, prototype-based inheritance under the hood, and the this-rebinding bug that breaks methods the moment they are passed as callbacks.

class is syntactic sugar over JavaScript's prototype system. Each instance gets methods shared on the prototype, and this inside a method refers to whatever object the method was called on - not where the method was defined. That distinction is the source of most class-related bugs.

A class groups related state and behavior. The constructor runs when you call new and sets up instance properties. Methods are defined in the class body and are available on every instance through the shared prototype.

class Counter {
  constructor(start = 0) {
    this.count = start;
  }
 
  increment() {
    this.count += 1;
  }
 
  decrement() {
    this.count -= 1;
  }
 
  value() {
    return this.count;
  }
}
 
const c = new Counter();
c.increment();
c.increment();
c.increment();
c.decrement();
console.log(c.value()); // 2
 
const c2 = new Counter(10);
c2.increment();
console.log(c2.value()); // 11

this is bound at the call site. Pulling a method off an instance and calling it as a plain function loses the binding. Classes always run in strict mode, so this becomes undefined (not window) and the call throws.

class Counter {
  constructor() {
    this.count = 0;
  }
 
  increment() {
    this.count += 1; // 'this' depends on how this function is called
  }
}
 
const c = new Counter();
c.increment();        // works - this === c
console.log(c.count); // 1
 
// Detach the method from its object
const inc = c.increment;
inc(); // TypeError: Cannot set properties of undefined
 
// The same problem arises when passing a method as a callback
setTimeout(c.increment, 100); // this is undefined inside increment

Three patterns fix the lost binding. Each has a different trade-off: binding in the constructor is compatible everywhere; class-field arrows are the modern default; wrapping at the call site is a one-off fix when you don't control the class.

// Fix 1: bind in the constructor (works in all environments)
class Counter1 {
  constructor() {
    this.count = 0;
    this.increment = this.increment.bind(this);
  }
  increment() { this.count += 1; }
}
 
// Fix 2: class field arrow (modern - arrow captures this at construction)
class Counter2 {
  count = 0;
  increment = () => { this.count += 1; };
}
 
// Fix 3: wrap at the call site (one-off, doesn't fix the class itself)
class Counter3 {
  constructor() { this.count = 0; }
  increment() { this.count += 1; }
}
 
const c1 = new Counter1();
const inc1 = c1.increment;
inc1(); // works
console.log(c1.count); // 1
 
const c2 = new Counter2();
const inc2 = c2.increment;
inc2(); // works
console.log(c2.count); // 1
 
const c3 = new Counter3();
setTimeout(() => c3.increment(), 100); // works - arrow at call site

In production

this is bound at the call site, not the definition site. The moment a method is passed as a callback the binding is gone. The class-field arrow (increment = () => { ... }) is the modern fix and requires no extra thought at every call site. Use .bind(this) in the constructor for environments or codebases that avoid class fields. Wrapping in an arrow at the call site patches the symptom without fixing the class.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free