JavaScriptClasses

Classes

The class keyword arrived in ES2015 and gave JavaScript familiar object-oriented syntax — but it is syntactic sugar over the prototype system the language already had. A class is a function under the hood, methods live on the prototype, and new does the same thing it has always done. Knowing that demystifies the corners of class behaviour.

The basic shape

JS
class Point {
  // Field declaration (modern syntax — ES2022)
  label = "(unnamed)";

  // Constructor — runs once per "new"
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  // Instance method — lives on Point.prototype
  distanceTo(other) {
    const dx = this.x - other.x;
    const dy = this.y - other.y;
    return Math.hypot(dx, dy);
  }
}

const a = new Point(0, 0);
const b = new Point(3, 4);
a.distanceTo(b);   // 5
What a class desugars to

Roughly, a class is equivalent to a constructor function plus a prototype object. Before ES2015 we wrote the equivalent by hand:

Pre-ES2015 equivalent

JS
function Point(x, y) {
  this.label = "(unnamed)";  // fields were always done by hand
  this.x = x;
  this.y = y;
}

Point.prototype.distanceTo = function (other) {
  const dx = this.x - other.x;
  const dy = this.y - other.y;
  return Math.hypot(dx, dy);
};

const a = new Point(0, 0);
const b = new Point(3, 4);
a.distanceTo(b);   // 5  — same shape, same behaviour
Why this matters
Methods are *shared* via the prototype, so all instances of a class point at one function in memory. Fields, in contrast, are *copied* onto each instance. Define large constant data on the prototype (or as a static), not as a field.
The new keyword

new ClassName(args) does four things in order:

  • Create a fresh empty object whose prototype is ClassName.prototype.

  • Bind that object as this and run the constructor.

  • If the constructor returns an object explicitly, that object is the result; otherwise this is returned.

  • Hand the result back to the caller.

JS
class Counter {
  constructor() { this.n = 0; }
}

const a = new Counter();
typeof a;                  // "object"
a instanceof Counter;      // true

// Forgetting new with a class throws
// Counter();              // TypeError: cannot call a class without new
Field declarations

The shorthand x = 1 at the top of a class declares an instance field — initialised on every new instance, before the constructor body runs. It is the modern replacement for assigning fields inside the constructor.

JS
class Timer {
  // Initialised once per instance, before constructor runs
  ticks = 0;
  startedAt = Date.now();

  tick() { this.ticks++; }
}

const t = new Timer();
t.tick();
t.tick();
t.ticks;        // 2
t.startedAt;    // a timestamp
Fields vs prototype properties
Anything declared as a field is per-instance. Anything declared as a method (no `=`) lives on the prototype. Choose deliberately: per-instance data → field, shared behaviour → method.
Class expressions

Classes can be expressions as well as declarations. Named expressions are useful for stack traces; anonymous expressions are useful when assigning to a variable.

JS
const Animal = class {
  speak() { return "..."; }
};

const Dog = class Dog {        // name is visible inside the body only
  speak() { return "woof"; }
};

new Animal().speak();   // "..."
new Dog().speak();      // "woof"
Classes are not hoisted (in a useful way)

Like let and const, class declarations live in a temporal dead zone — referring to them before the declaration line throws.

JS
// new Foo();         // ReferenceError
class Foo {}

// Contrast with function declarations, which ARE hoisted
sayHi();                // "hi"
function sayHi() { console.log("hi"); }
hi
Classes use strict mode by default

Code inside a class body is implicitly in strict mode. That means duplicate parameter names, octal literals and silent failures on assignments to read-only properties all throw. You do not need (and cannot use) "use strict" inside a class.

When to reach for classes
  • You have a clear concept that bundles state plus behaviour — a Player, a Cache, a Connection.

  • You expect to create many instances and call the same methods on each.

  • You want a natural place to define toString, Symbol.iterator, or other protocol methods.

  • You may extend it later (a class hierarchy of ShapeCircle, Rectangle).

For shapeless bags of data, a plain object is usually clearer. For pure transformations, a function is fine. Classes shine when state and behaviour belong together.

The next pages dig into the constructor, instance vs static methods, getters and setters, inheritance with extends, and the new #private field syntax.