Skip to main content
This book is a work in progress. Content may be updated as the source material evolves.
The class-design pattern generally entails defining a type of thing (class), including data (members) and behaviors (methods), and then creating one or more concrete instances of this class definition as actual objects that can interact and perform tasks. Moreover, class-orientation allows declaring a relationship between two or more classes, through what’s called “inheritance”, to derive new and augmented “subclasses” that mix-n-match and even re-define behaviors. Prior to ES6 (2015), JS developers mimicked aspects of class-oriented (aka “object-oriented”) design using plain functions and objects, along with the [[Prototype]] mechanism (as explained in the previous chapter) — so called “prototypal classes”. But to many developers joy and relief, ES6 introduced dedicated syntax, including the class and extends keywords, to express class-oriented design more declaratively. At the time of ES6’s class being introduced, this new dedicated syntax was almost entirely just syntactic sugar to make class definitions more convenient and readable. However, in the many years since ES6, class has matured and grown into its own first class feature mechanism, accruing a significant amount of dedicated syntax and complex behaviors that far surpass the pre-ES6 “prototypal class” capabilities. Even though class now bears almost no resemblance to older “prototypal class” code style, the JS engine is still just wiring up objects to each other through the existing [[Prototype]] mechanism. In other words, class is not its own separate pillar of the language (as [[Prototype]] is), but more like the fancy, decorative Capital that tops the pillar/column.

When Should I Class-Orient My Code?

Class-orientation is a design pattern, which means it’s a choice for how you organize the information and behavior in your program. It has pros and cons. It’s not a universal solution for all tasks. So how do you know when you should use classes? In a theoretical sense, class-orientation is a way of dividing up the business domain of a program into one or more pieces that can each be defined by an “is-a” classification: grouping a thing into the set (or sets) of characteristics that thing shares with other similar things. You would say “X is a Y”, meaning X has (at least) all the characteristics of a thing of kind Y. For example, consider computers. We could say a computer is electrical, since it uses electrical current (voltage, amps, etc) as power. It’s furthermore electronic, because it manipulates the electrical current beyond simply routing electrons around (electrical/magnetic fields), creating a meaningful circuit to manipulate the current into performing more complex tasks. By contrast, a basic desk lamp is electrical, but not really electronic. We could thus define a class Electrical to describe what electrical devices need and can do. We could then define a further class Electronic, and define that in addition to being electrical, Electronic things manipulate electricity to create more specialized outcomes. Here’s where class-orientation starts to shine. Rather than re-define all the Electrical characteristics in the Electronic class, we can define Electronic in such a way that it “shares” or “inherits” those characteristics from Electrical, and then augments/redefines the unique behaviors that make a device electronic. This relationship between the two classes — called “inheritance” — is a key aspect of class-orientation.

Time For An Example

Here’s a short illustration. A couple of decades ago, right after I had gone through nearly all of a Computer Science degree in college, I found myself sitting in my first professional software developer job. I was tasked with building, all by myself, a timesheet and payroll tracking system. I built the backend in PHP (using MySQL for the DB) and used JS for the interface (early as it was in its maturity way back around the turn of the century). Since my CS degree had emphasized class-orientation heavily throughout my courses, I was eager to put all that theory to work. For my program’s design, I defined the concept of a “timesheet” entity as a collection of 2-3 “week” entities, and each “week” as a collection of 5-7 “day” entities, and each “day” as a collection of “task” entities. If I wanted to know how many hours were logged into a timesheet instance, I could call a totalTime() operation on that instance. The timesheet defined this operation by looping over its collection of weeks, calling totalTime() on each of them and summing the values. Each week did the same for all its days, and each day did the same for all its tasks. The notion being illustrated here, one of the fundamentals of design patterns like class-orientation, is called encapsulation. Each entity level encapsulated (e.g., controlled, hid, abstracted) internal details (data and behavior) while presenting a useful external interface. But encapsulation alone isn’t a sufficient justification for class-orientation. Other design patterns offer sufficient encapsulation. How did my class design take advantage of inheritance? I had a base class that defined a set of operations like totalTime(), and each of my entity class types extended/subclassed this base class. That meant that each of them inherited this summation-of-total-time capability, but where each of them applied their own extensions and definitions for the internal details of how to do that work. There’s yet another aspect of the design pattern at play, which is composition: each entity was defined as a collection of other entities.

Single vs Multiple

I mentioned above that a pragmatic way of deciding if you need class-orientation is if your program is going to have multiple instances of a single kind/type of behavior (aka, “class”). In the timesheet example, we had 4 classes: Timesheet, Week, Day, and Task. But for each class, we had multiple instances of each at once. Had we instead only needed a single instance of a class, like just one Computer thing that was an instance of the Electronic class, which was a subclass of the Electrical class, then class-orientation may not offer quite as much benefit. In particular, if the program doesn’t need to create an instance of the Electrical class, then there’s no particular benefit to separating Electrical from Electronic, so we aren’t really getting any help from the inheritance aspect of class-orientation.
If you find yourself designing a program by dividing up a business problem domain into different “classes” of entities, but in the actual code of the program you are only ever need one concrete thing of one kind/definition of behavior (aka, “class”), you might very well not actually need class-orientation.
But if you find yourself wanting to define classes, and subclasses which inherit from them, and if you’re going to be instantiating one or more of those classes multiple times, then class-orientation is a good candidate. And to do class-orientation in JS, you’re going to need the class keyword.

Keep It classy

class defines either a declaration or expression for a class. As a declaration, a class definition appears in a statement position and looks like this:
class Point2d {
    // ..
}
As an expression, a class definition appears in a value position and can either have a name or be anonymous:
// named class expression
const pointClass = class Point2d {
    // ..
};

// anonymous class expression
const anotherClass = class {
    // ..
};
The contents of a class body typically include one or more method definitions:
class Point2d {
    setX(x) {
        // ..
    }
    setY(y) {
        // ..
    }
}
Inside a class body, methods are defined without the function keyword, and there’s no , or ; separators between the method definitions.
Inside a class block, all code runs in strict-mode even without the "use strict" pragma present in the file or its functions. In particular, this impacts the this behavior for function calls, as explained in Chapter 4.

The Constructor

One special method that all classes have is called a “constructor”. If omitted, there’s a default empty constructor assumed in the definition. The constructor is invoked any time a new instance of the class is created:
class Point2d {
    constructor() {
        console.log("Here's your new instance!");
    }
}

var point = new Point2d();
// Here's your new instance!
Even though the syntax implies a function actually named constructor exists, JS defines a function as specified, but with the name of the class (Point2d above):
typeof Point2d;       // "function"
It’s not just a regular function, though; this special kind of function behaves a bit differently:
Point2d.toString();
// class Point2d {
//   ..
// }

Point2d();
// TypeError: Class constructor Point2d cannot
// be invoked without 'new'

Point2d.call({});
// TypeError: Class constructor Point2d cannot
// be invoked without 'new'
You can construct as many different instances of a class as you need:
var one = new Point2d();
var two = new Point2d();
var three = new Point2d();
Each of one, two, and three here are objects that are independent instances of the Point2d class.
Each of the one, two, and three objects have a [[Prototype]] linkage to the Point2d.prototype object (see Chapter 2). In this code, Point2d is both a class definition and the constructor function of the same name.

Class Methods

As shown above, a class definition can include one or more method definitions:
class Point2d {
    constructor() {
        console.log("Here's your new instance!");
    }
    setX(x) {
        console.log(`Setting x to: ${x}`);
        // ..
    }
}

var point = new Point2d();

point.setX(3);
// Setting x to: 3
The setX property (method) looks like it exists on (is owned by) the point object here. But that’s a mirage. Each class method is added to the prototype object, a property of the constructor function. So, setX(..) only exists as Point2d.prototype.setX. Since point is [[Prototype]] linked to Point2d.prototype (see Chapter 2) via the new keyword instantiation, the point.setX(..) reference traverses the [[Prototype]] chain and finds the method to execute. Class methods should only be invoked via an instance; Point2d.setX(..) doesn’t work because there is no such property. You could invoke Point2d.prototype.setX(..), but that’s not generally proper/advised in standard class-oriented coding. Always access class methods via the instances.

Class Instance this

We will cover the this keyword in much more detail in a subsequent chapter. But as it relates to class-oriented code, the this keyword generally refers to the current instance that is the context of any method invocation. In the constructor, as well as any methods, you can use this. to either add or access properties on the current instance:
class Point2d {
    constructor(x,y) {
        // add properties to the current instance
        this.x = x;
        this.y = y;
    }
    toString() {
        // access the properties from the current instance
        console.log(`(${this.x},${this.y})`);
    }
}

var point = new Point2d(3,4);

point.x;                // 3
point.y;                // 4

point.toString();       // (3,4)
Any properties not holding function values, which are added to a class instance (usually via the constructor), are referred to as members, as opposed to the term methods for executable functions.

Public Fields

Instead of defining a class instance member imperatively via this. in the constructor or a method, classes can declaratively define fields in the class body, which correspond directly to members that will be created on each instance:
class Point2d {
    // these are public fields
    x = 0
    y = 0

    constructor(x,y) {
        // set properties (fields) on the current instance
        this.x = x;
        this.y = y;
    }
    toString() {
        // access the properties from the current instance
        console.log(`(${this.x},${this.y})`);
    }
}
Public fields can have a value initialization, as shown above, but that’s not required. If you don’t initialize a field in the class definition, you almost always should initialize it in the constructor. Fields can also reference each other, via natural this. access syntax:
class Point3d {
    // these are public fields
    x
    y = 4
    z = this.y * 5

    // ..
}
You can mostly think of public field declarations as if they appear at the top of the constructor(..), each prefixed with an implied this. that you get to omit in the declarative class body form. But, there’s a catch! See “That’s Super!” later for more information about it.
Just like computed property names (see Chapter 1), field names can be computed:
var coordName = "x";

class Point2d {
    // computed public field
    [coordName.toUpperCase()] = 42

    // ..
}

var point = new Point2d(3,4);

point.x;        // 3
point.y;        // 4

point.X;        // 42

Avoid This

One pattern that has emerged and grown quite popular, but which I firmly believe is an anti-pattern for class, looks like the following:
class Point2d {
    x = null
    y = null
    getDoubleX = () => this.x * 2

    constructor(x,y) {
        this.x = x;
        this.y = y;
    }
    toString() { /* .. */ }
}

var point = new Point2d(3,4);

point.getDoubleX();    // 6
See the field holding an => arrow function? I say this is a no-no. But why? Let’s unwind what’s going on. First, why do this? Because JS developers seem to be perpetually frustrated by the dynamic this binding rules (see Chapter 4), so they force a this binding via the => arrow function. That way, no matter how getDoubleX() is invoked, it’s always this-bound to the particular instance. That’s an understandable convenience to desire, but… it betrays the very nature of the this / [[Prototype]] pillar of the language.
By defining a function value and attaching it as a field/member property, we’re losing the shared prototypal method’ness of the function, and it becomes just like any per-instance property. That means we’re creating a new function property for each instance, rather than it being created just once on the class constructor’s prototype.
Consider:
Object.hasOwn(point,"x");               // true -- good
Object.hasOwn(point,"toString");        // false -- good
Object.hasOwn(point,"getDoubleX");      // true -- oops :(
That’s wasteful in performance and memory, even if by a tiny bit. That alone should be enough to avoid it. But I would argue that way more importantly, what you’ve done with this pattern is invalidate the very reason why using class and this-aware methods is even remotely useful/powerful! If you go to all the trouble to define class methods with this. references throughout them, but then you lock/bind most or all of those methods to a specific object instance, you’ve basically travelled all the way around the world just to go next door. If all you want are function(s) that are statically fixed to a particular “context”, and don’t need any dynamicism or sharing, what you want is… closure. And you’re in luck: I wrote a whole book in this series (“Scope & Closures”) on how to use closure so functions remember/access their statically defined scope (aka “context”). That’s a way more appropriate, and simpler to code, approach to get what you’re after. Don’t abuse/misuse class and turn it into a over-hyped, glorified collection of closure.

Class Extension

The way to unlock the power of class inheritance is through the extends keyword, which defines a relationship between two classes:
class Point2d {
    x = 3
    y = 4

    getX() {
        return this.x;
    }
}

class Point3d extends Point2d {
    x = 21
    y = 10
    z = 5

    printDoubleX() {
        console.log(`double x: ${this.getX() * 2}`);
    }
}

var point = new Point2d();

point.getX();                   // 3

var anotherPoint = new Point3d();

anotherPoint.getX();            // 21
anotherPoint.printDoubleX();    // double x: 42
The base class Point2d defines fields (members) called x and y, and gives them the initial values 3 and 4, respectively. It also defines a getX() method that accesses this x instance member and returns it. But the Point3d class extends Point2d, making Point3d a derived-class, child-class, or (most commonly) subclass. In Point3d, the same x property that’s inherited from Point2d is re-initialized with a different 21 value, as is the y overridden to value from 4, to 10.

Overriding Methods

In addition to overriding a field/member in a subclass, you can also override (redefine) a method:
class Point2d {
    x = 3
    y = 4

    getX() {
        return this.x;
    }
}

class Point3d extends Point2d {
    x = 21
    y = 10
    z = 5

    getX() {
        return this.x * 2;
    }
    printX() {
        console.log(`double x: ${this.getX()}`);
    }
}

var point = new Point3d();

point.printX();       // double x: 42
The Point3d subclass overrides the inherited getX() method to give it different behavior. However, you can still instantiate the base Point2d class, which would then give an object that uses the original (return this.x;) definition for getX(). If you want to access an inherited method from a subclass even if it’s been overridden, you can use super instead of this:
class Point3d extends Point2d {
    x = 21
    y = 10
    z = 5

    getX() {
        return this.x * 2;
    }
    printX() {
        console.log(`x: ${super.getX()}`);
    }
}

var point = new Point3d();

point.printX();       // x: 21
The ability for methods of the same name, at different levels of the inheritance hierarchy, to exhibit different behavior when either accessed directly, or relatively with super, is called method polymorphism. It’s a very powerful part of class-orientation, when used appropriately.

That’s Super!

In addition to a subclass method accessing an inherited method definition (even if overriden on the subclass) via super. reference, a subclass constructor must manually invoke the inherited base class constructor via super(..) function invocation:
class Point2d {
    x
    y
    constructor(x,y) {
        this.x = x;
        this.y = y;
    }
}

class Point3d extends Point2d {
    z
    constructor(x,y,z) {
        super(x,y);
        this.z = z;
    }
    toString() {
        console.log(`(${this.x},${this.y},${this.z})`);
    }
}

var point = new Point3d(3,4,5);

point.toString();       // (3,4,5)
An explicitly defined subclass constructor must call super(..) to run the inherited class’s initialization, and that must occur before the subclass constructor makes any references to this or finishes/returns. Otherwise, a runtime exception will be thrown when that subclass constructor is invoked (via new). If you omit the subclass constructor, the default constructor automatically — thankfully! — invokes super() for you.

But Which Kind Of Instance?

You may want to introspect a certain object instance to see if it’s an instance of a specific class. We do this with the instanceof operator:
class Point2d { /* .. */ }
class Point3d extends Point2d { /* .. */ }

var point = new Point2d(3,4);

point instanceof Point2d;           // true
point instanceof Point3d;           // false

var anotherPoint = new Point3d(3,4,5);

anotherPoint instanceof Point2d;    // true
anotherPoint instanceof Point3d;    // true
It may seem strange to see anotherPoint instanceof Point2d result in true. The instanceof operator doesn’t just look at the current object, but rather traverses the entire class inheritance hierarchy (the [[Prototype]] chain) until it finds a match. Thus, anotherPoint is an instance of both Point3d and Point2d. If you instead wanted to check if the object instance was only and directly created by a certain class, check the instance’s constructor property:
point.constructor === Point2d;          // true
point.constructor === Point3d;          // false

anotherPoint.constructor === Point2d;   // false
anotherPoint.constructor === Point3d;   // true

“Inheritance” Is Sharing, Not Copying

It may seem as if Point3d, when it extends the Point2d class, is in essence getting a copy of all the behavior defined in Point2d. Moreover, it may seem as if the concrete object instance anotherPoint receives, copied down to it, all the methods from Point3d (and by extension, also from Point2d). However, that’s not the correct mental model to use for JS’s implementation of class-orientation.
Object.hasOwn(anotherPoint,"x");                       // true
Object.hasOwn(anotherPoint,"y");                       // true
Object.hasOwn(anotherPoint,"z");                       // true

Object.hasOwn(anotherPoint,"toString");                // false
Where is that toString() method located? On the prototype object:
Object.hasOwn(Point3d.prototype,"toString");    // true
And anotherPoint has access to that method via its [[Prototype]] linkage (see Chapter 2). In other words, the prototype objects share access to their method(s) with the subclass(es) and instance(s). The method(s) stay in place, and are not copied down the inheritance chain. As nice as the class syntax is, don’t forget what’s really happening under the syntax: JS is just wiring up objects to each other along a [[Prototype]] chain.

Static Class Behavior

We’ve so far emphasized two different locations for data or behavior (methods) to reside: on the constructor’s prototype, or on the instance. But there’s a third option: on the constructor (function object) itself. Not all behavior that we define and want to associate/organize with a class needs to be aware of an instance. Moreover, sometimes a class needs to publicly define data (like constants) that developers using that class need to access, independent of any instance they may or may not have created. So, how does a class system enable defining such data and behavior that should be available with a class but independent of (unaware of) instantiated objects? Static properties and functions. We use the static keyword in our class bodies to distinguish these definitions:
class Point2d {
    // class statics
    static origin = new Point2d(0,0)
    static distance(point1,point2) {
        return Math.sqrt(
            ((point2.x - point1.x) ** 2) +
            ((point2.y - point1.y) ** 2)
        );
    }

    // instance members and methods
    x
    y
    constructor(x,y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `(${this.x},${this.y})`;
    }
}

console.log(`Starting point: ${Point2d.origin}`);
// Starting point: (0,0)

var next = new Point2d(3,4);
console.log(`Next point: ${next}`);
// Next point: (3,4)

console.log(`Distance: ${
    Point2d.distance( Point2d.origin, next )
}`);
// Distance: 5
The Point2d.origin is a static property, which just so happens to hold a constructed instance of our class. And Point2d.distance(..) is a static function that computes the 2-dimensional cartesian distance between two points.
Don’t forget that when you use the class syntax, the name Point2d is actually the name of a constructor function that JS defines. So Point2d.origin is just a regular property access on that function object. Take care not to confuse those with properties stored on the constructor’s prototype (methods) and properties stored on the instance (members).

Static Inheritance

Class statics are inherited by subclasses (obviously, as statics!), can be overriden, and super can be used for base class references (and static function polymorphism), all in much the same way as inheritance works with instance members/methods:
class Point3d extends Point2d {
    // class statics
    static origin = new Point3d(
        super.origin.x, super.origin.y, 0
    )
    static distance(point1,point2) {
        return Math.sqrt(
            ((point2.x - point1.x) ** 2) +
            ((point2.y - point1.y) ** 2) +
            ((point2.z - point1.z) ** 2)
        );
    }

    // instance members/methods
    z
    constructor(x,y,z) {
        super(x,y);     // <-- don't forget this line!
        this.z = z;
    }
    toString() {
        return `(${this.x},${this.y},${this.z})`;
    }
}

Point2d.maxDistance;        // 10
Point3d.maxDistance;        // 10
Remember: any time you define a subclass constructor, you’ll need to call super(..) in it, usually as the first statement.

Private Class Behavior

Everything we’ve discussed so far as part of a class definition is publicly visible/accessible, either as static properties/functions on the class, methods on the constructor’s prototype, or member properties on the instance. But how do you store information that cannot be seen from outside the class? This was one of the most asked for features, and biggest complaints with JS’s class, up until it was finally addressed in ES2022. class now supports new syntax for declaring private fields (instance members) and private methods. In addition, private static properties/functions are possible.

Motivation?

With closure-oriented design patterns (again, see the “Scope & Closures” book of this series), we automatically get “privacy” built-in. When you declare a variable inside a scope, it cannot be seen outside that scope. Period. Reducing the scope visibility of a declaration is helpful in preventing namespace collisions (identical variable names). But it’s even more important to ensure proper “defensive” design of software, the so called “Principle of Least Privilege”. POLP states that we should only expose a piece of information or capability in our software to the smallest surface area necessary.
In short, we should hide implementation details if they’re not necessary to be exposed. In this sense, JS’s class system feels a bit too permissive in that everything defaults to being public. Class-private features are a welcomed addition to more proper software design.

Too Private?

All that said, I have to throw a bit of a damper on the class-private party. One of the most important aspects of class-orientation is subclass inheritance, as we’ve seen illustrated numerous times so far in this chapter. Guess what happens to a private member/method in a base class, when it’s extended by a subclass? Private members/methods are private only to the class they’re defined in, and are not inherited in any way by a subclass.
There’s not a particularly great answer here, to be honest. JS has no protected visibility, and it seems (even as useful as it is!) to be unlikely as a JS feature. And protected visibility is actually, in practice, way more useful than private visibility.

Private Members/Methods

You’re excited to finally see the syntax for magical private visibility, right? Please don’t shoot the messenger if you feel angered or sad at what you’re about to see.
class Point2d {
    // statics
    static samePoint(point1,point2) {
        return point1.#ID === point2.#ID;
    }

    // privates
    #ID = null
    #assignID() {
        this.#ID = Math.round(Math.random() * 1e9);
    }

    // publics
    x
    y
    constructor(x,y) {
        this.#assignID();
        this.x = x;
        this.y = y;
    }
}

var one = new Point2d(3,4);
var two = new Point2d(3,4);

Point2d.samePoint(one,two);         // false
Point2d.samePoint(one,one);         // true
No, JS didn’t do the sensible thing and introduce a private keyword like they did with static. Instead, they introduced the #. The #whatever syntax (including this.#whatever form) is only valid inside class bodies. It will throw syntax errors if used outside of a class. Unlike public fields/instance members, private fields/instance members must be declared in the class body. You cannot add a private member to a class declaration dynamically while in the constructor method; this.#whatever = .. type assignments only work if the #whatever private field is declared in the class body. Moreover, though private fields can be re-assigned, they cannot be deleted from an instance, the way a public field/class member can.

Existence Check

You may want to check to see if a private field/method exists on an object instance. Such a check could be rather convoluted, because if you access a private field that doesn’t already exist on the object, you get a JS exception thrown, requiring ugly try..catch logic. But there’s a cleaner approach, so called an “ergonomic brand check”, using the in keyword:
class Point2d {
    // statics
    static samePoint(point1,point2) {
        // "ergonomic brand checks"
        if (#ID in point1 && #ID in point2) {
            return point1.#ID === point2.#ID;
        }
        return false;
    }

    // privates
    #ID = null
    #assignID() {
        this.#ID = Math.round(Math.random() * 1e9);
    }

    // publics
    x
    y
    constructor(x,y) {
        this.#assignID();
        this.x = x;
        this.y = y;
    }
}
The #privateField in someObject check will not throw an exception if the field isn’t found, so it’s safe to use without try..catch and use its simple boolean result.

Private Statics

Static properties and functions can also use # to be marked as private:
class Point2d {
    static #errorMsg = "Out of bounds."
    static #printError() {
        console.log(`Error: ${this.#errorMsg}`);
    }

    // publics
    x
    y
    constructor(x,y) {
        if (x > 100 || y > 100) {
            Point2d.#printError();
        }
        this.x = x;
        this.y = y;
    }
}

var one = new Point2d(30,400);
// Error: Out of bounds.
The #printError() static private function here has a this, but that’s referencing the Point2d class, not an instance. As such, the #errorMsg and #printError() are independent of instances and thus are best as statics. Moreover, there’s no reason for them to be accessible outside the class, so they’re marked private.

Build docs developers (and LLMs) love