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
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); // 5What 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
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 behaviourThe new keyword
new ClassName(args) does four things in order:
Create a fresh empty object whose prototype is
ClassName.prototype.Bind that object as
thisand run the constructor.If the constructor returns an object explicitly, that object is the result; otherwise
thisis returned.Hand the result back to the caller.
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 newField 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.
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 timestampClass 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.
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.
// 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, aCache, aConnection.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
Shape→Circle,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.