JavaScript - Classes (OOP)

Overview

Estimated time: 35–45 minutes

Classes provide a familiar syntax over JavaScript's prototype-based inheritance. Learn class fields, methods, getters/setters, static and private fields, and how to extend classes with extends/super.

Learning Objectives

  • Declare classes with constructors, instance fields, and methods.
  • Use getters/setters for computed or validated properties.
  • Define static members and private fields with #.
  • Extend classes using extends and call super correctly.

Prerequisites

Class basics

class Person {
  // public field (per-instance)
  role = 'user';

  constructor(name) {
    this.name = name; // creates a property on the instance
  }

  // prototype method (shared across instances)
  greet() { return `Hi, I'm ${this.name}`; }

  // getter/setter around a backing field
  get upperName() { return String(this.name).toUpperCase(); }
  set upperName(v) { this.name = String(v).toLowerCase(); }

  // static method (on the class, not instances)
  static isPerson(x) { return x instanceof Person; }
}

const p = new Person('Ada');
p.greet();           // "Hi, I'm Ada"
Person.isPerson(p);  // true

Private fields and methods

Private identifiers start with # and are only accessible within the class body.

class Counter {
  #count = 0;              // private field
  #bump() { this.#count++; } // private method

  inc() { this.#bump(); }
  value() { return this.#count; }
}

const c = new Counter();
c.inc();
c.value(); // 1
// c.#count; // SyntaxError: private field is not accessible

Inheritance with extends and super

class Employee extends Person {
  constructor(name, id) {
    super(name);       // call base constructor first
    this.id = id;
  }

  // override method
  greet() { return `${super.greet()} (Employee #${this.id})`; }
}

new Employee('Bob', 7).greet();
// "Hi, I'm Bob (Employee #7)"

this binding and class fields

Extracted methods lose their this binding. Use Function.prototype.bind or a public field arrow function to capture this per instance.

class Button {
  label = 'Click';

  handleClick() { console.log('clicked', this.label); }
  handleClickBound = () => { console.log('clicked', this.label); } // captures this
}

const b = new Button();
const f1 = b.handleClick;
const f2 = b.handleClickBound;

try { f1(); } catch (e) { /* this is undefined in strict mode */ }
f2(); // works; logs 'clicked Click'

Common Pitfalls

  • this binding: Passing methods as callbacks drops this. Use .bind(this) or arrow function fields.
  • Initialization order: Field initializers run after super() in derived classes.
  • Privacy: #private is lexical and not accessible outside the class—even in tests or subclasses.
  • Class hoisting: Class declarations are not hoisted like functions; you must declare before use.

Try it

Run to exercise fields, private members, inheritance, and this-binding: