JavaScript Intermediate #1 Classes

6 min read

The first post in the JavaScript Intermediate series. After finishing the 7-part Basics series you can confidently build small tools and scripts — Intermediate is where you layer on modern JavaScript’s expressiveness.

A 7-post series.

  • #1 Classes ← this post
  • #2 Async intro — Promise, async/await
  • #3 Iterators and generators
  • #4 Destructuring/spread/rest in depth
  • #5 Optional chaining and nullish coalescing
  • #6 fetch API and error handling
  • #7 Working with JSON and serialization

This post covers JavaScript’s class syntax from start to finish.

Class basics #

first class
class User {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`hi, ${this.name} (${this.age})`);
  }
}

const u = new User('Curtis', 30);
u.greet();   // hi, Curtis (30)

It starts with the class keyword, and constructor is the initializer that runs when an instance is created. Inside, this refers to the new instance.

Calling without new throws an error — classes are always used with new.

new is required
User('Curtis', 30);   // ✗ TypeError
new User('Curtis', 30);  // OK

Methods and this #

Inside a method, this defaults to the calling instance. But detaching the function can lose this.

where this disappears
class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    console.log(`hi, ${this.name}`);
  }
}

const u = new User('Curtis');
u.greet();             // hi, Curtis

const fn = u.greet;     // detached
fn();                    // hi, undefined  ← this gone

This is the most common pitfall with JavaScript classes. Frequent when passing a method as a callback or registering it as an event handler. Two solutions.

1) Wrap with an arrow function #

callback wrapped in an arrow
button.addEventListener('click', () => u.greet());

2) bind or arrow function as a class field #

field arrow — this is locked in
class User {
  constructor(name) {
    this.name = name;
  }
  greet = () => {
    console.log(`hi, ${this.name}`);
  };
}

const u = new User('Curtis');
const fn = u.greet;
fn();     // hi, Curtis — this is preserved even when detached

You may have seen this in React callbacks. The cost — a fresh method per instance, so it’s not recommended for heavy classes.

getter / setter — methods that look like properties #

getter / setter
class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }

  get fahrenheit() {
    return this._celsius * 9 / 5 + 32;
  }

  set fahrenheit(value) {
    this._celsius = (value - 32) * 5 / 9;
  }
}

const t = new Temperature(25);
console.log(t.fahrenheit);   // 77 — accessed like a property, not a method call
t.fahrenheit = 100;
console.log(t._celsius);     // ~37.7

Defined with get / set — read and write without parentheses. Fits when you want to compute on read or validate on write.

Static members — static #

Methods/properties that belong to the class itself, not instances.

static
class MathUtil {
  static PI = 3.14159;

  static square(n) {
    return n * n;
  }
}

MathUtil.PI;          // 3.14159
MathUtil.square(5);   // 25

const m = new MathUtil();
m.square(5);          // ✗ not on the instance

Fits utility-function bundles, or factory methods that help create instances (e.g., User.fromJSON(...)).

static block — ES2022 #

When static-member initialization needs complex logic, use a static { ... } block.

static block
class Config {
  static defaults = {};

  static {
    Config.defaults = JSON.parse(loadConfigFile());
    Config.defaults.timestamp = Date.now();
  }
}

Not common — but you’ll meet it during library initialization.

Private fields — # #

The official private syntax added in ES2022. External access is impossible.

private fields
class BankAccount {
  #balance = 0;

  deposit(amount) {
    this.#balance = this.#balance + amount;
  }

  get balance() {
    return this.#balance;
  }
}

const acc = new BankAccount();
acc.deposit(100);
console.log(acc.balance);     // 100
console.log(acc.#balance);    // ✗ SyntaxError — external access blocked

Prefix the name with #. Not a convention — language-level enforcement, real privacy. Unlike old JavaScript’s “convention-only private (_balance),” external access is blocked.

Difference from _underscore #

The old code’s _balance is a promise only.

_underscore — externally accessible
class OldStyle {
  constructor() {
    this._balance = 0;       // convention: please don't access
  }
}

const o = new OldStyle();
o._balance = 999;            // actually accessible

In new code, use #.

Inheritance — extends and super #

inheritance
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);             // call parent constructor
    this.breed = breed;
  }
  speak() {
    super.speak();           // call parent method
    console.log(`${this.name} barks (woof woof)`);
  }
}

const d = new Dog('Bori', 'Shiba');
d.speak();
// Bori makes a sound
// Bori barks (woof woof)

extends declares the parent class — and inside the child constructor you must call super(...) before using this. Forget it and you’ll see a ReferenceError.

super.method() calls the parent’s method, and a same-named method on the child overrides the parent’s.

Inheritance — worth using deeply? #

Inheritance is a tool that, beyond JavaScript and across OOP, breaks code fast when overused. Modern JavaScript recommends:

  • Don’t introduce inheritance where a single class is enough
  • Prefer composition to inheritance — have an object hold other objects
  • For shared interfaces/signatures — functions and objects also suffice

React’s move from class components to function components is similar in spirit. Cases where classes are essential are rarer than they seem.

instanceof — class check #

instanceof
const d = new Dog('Bori', 'Shiba');

d instanceof Dog;       // true
d instanceof Animal;    // true (parent is also true)
d instanceof Object;    // true
d instanceof User;       // false

Walks the inheritance chain. Often used to check whether something is an instance of a particular class.

Classes vs object literals #

Guideline for choosing between a class and a plain object.

CaseBetter choice
Many instances of the same shapeclass
Behavior (methods) tightly bound to dataclass
Single instance, settings object, etc.object literal
Functional transformations onlyfunctions + objects
Hierarchy deep enough to need inheritanceclass (or reconsider via composition)

At first you’ll often find that not using classes is fine. Reach for classes only when one really fits.

Wrap-up #

What we covered:

  • class and new, initialize this inside constructor
  • Detached methods lose this — arrow function or field arrow
  • Method-like properties via get/set
  • static for class-level members, static {} blocks (ES2022)
  • True private with #field (_underscore is convention only)
  • Inheritance with extends, call super(...) first
  • Beware overusing inheritance — modern convention is composition first
  • Where classes fit vs object literals

In the next post (#2 Async Intro) we cover one of JavaScript’s most distinctive features — async — starting with Promises and async/await from scratch.

X