Back to JavaScript
JavaScript
medium
mid

How do you apply object oriented programming patterns in JavaScript?

Use `class` for encapsulated kinds (component, model, service). Fields for state, methods on prototype, private fields with `#`, `static` for class-level helpers, `extends` + `super` for inheritance. Favor composition + small classes; avoid deep hierarchies. For pure-data shapes use plain objects + TypeScript types, not classes.

5 min read·~10 min to think through

Class syntax

js
class User {
  #id;                                  // private field
  name;                                 // public field
  static defaultRole = "member";        // static field

  constructor(id, name) {
    this.#id = id;
    this.name = name;
  }

  get id() { return this.#id; }         // getter
  greet() { return `Hi, ${this.name}`; } // method (on prototype)
  static fromJSON(json) { return new User(json.id, json.name); }
}

const u = User.fromJSON({ id: 1, name: "Sam" });
u.greet();   // "Hi, Sam"
u.id;        // 1; #id is unreachable from outside

Pieces of OO in JS

FeatureSyntax
Public fieldname = "x"
Private field#secret
Methodgreet() { }
Static methodstatic fromJSON() { }
Getter / setterget x() { } / set x(v) { }
Inheritanceclass B extends A { }
Super callsuper(args); super.method()
Abstract-likeThrow in base method

Inheritance

js
class Animal {
  constructor(name) { this.name = name; }
  speak() { return "?"; }
}

class Dog extends Animal {
  constructor(name, breed) { super(name); this.breed = breed; }
  speak() { return "woof"; }
}

super(...) is required in subclass constructors before using this.

Composition over inheritance

For mixed capabilities, prefer composition:

js
class CartService {
  constructor(deps) {
    this.api = deps.api;
    this.events = deps.events;
  }
  async add(item) {
    await this.api.post("/cart", item);
    this.events.emit("cart:add", item);
  }
}

Inject dependencies via constructor; easy to test (swap in mocks).

Mixins (use sparingly)

js
const Serializable = (Base) => class extends Base {
  toJSON() { return JSON.stringify(this); }
};

class Foo extends Serializable(Object) {}

Function-returning-class pattern for ad hoc mixins. Stack carefully — debugging gets hairy.

When OOP is overkill

For pure data:

ts
type User = { id: number; name: string };
const u: User = { id: 1, name: "Sam" };

No class needed. Use classes when:

  • You have meaningful invariants enforced by the constructor.
  • Methods belong to the data (order.totalCents()).
  • Multiple instances with shared behavior.

Don't use classes when:

  • You'd write a class with only a constructor and getters — just use a plain object.
  • Data flows through reducers / immutable transforms.
  • "Anaemic domain model" — class with only fields and trivial setters.

Modern patterns

  • Service classes for app boundaries (ApiClient, AnalyticsClient).
  • Models with behavior (Order with status transitions).
  • Singletons for cross-cutting (Logger, Config).
  • React class components are now rare — function components + hooks replace them.

Private + encapsulation

#field is hard-private — engine-level, can't be reflected, doesn't show up in Object.keys. Better than the legacy _underscore convention.

Static blocks

js
class C {
  static cache = new Map();
  static {
    // run once at class definition
    C.cache.set("default", new C());
  }
}

Interview framing

"Use class when the abstraction earns its keep — invariants, shared behavior across instances, real methods on data. Public/private fields with #, methods on prototype, statics for factories, getters for derived. Inherit with extends + super, but prefer composition: inject collaborators via constructor for testability. Plain objects + TypeScript types are fine for pure data — don't reach for class on every shape. React has mostly retired class components; for app code, OO shines for service boundaries and domain models with behavior."

Follow-up questions

  • When are classes vs plain objects + types?
  • Compare composition vs inheritance.
  • How do private fields differ from underscore convention?

Common mistakes

  • Deep inheritance hierarchies.
  • Class with only fields (anaemic).
  • Forgetting super() in subclass constructor.
  • Using _underscore instead of # for privacy.

Performance considerations

  • Class instances optimize well in V8. Arrow methods on instance vs prototype methods — minor memory tradeoff. Avoid mutating prototypes after construction.

Edge cases

  • this binding loss when method passed as callback.
  • Static fields per subclass — fresh per class, not shared.
  • Mixin chains.

Real-world examples

  • EventEmitter, Map/Set built-ins, Node streams, service classes in Angular, domain models in DDD.

Senior engineer discussion

Seniors choose classes for boundaries with behavior, plain objects + types for data, and keep hierarchies shallow with DI instead of inheritance.

Related questions