This book is a work in progress. Content may be updated as the source material evolves.
[[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 classElectrical 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 atotalTime() 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 oneComputer 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.
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 body typically include one or more method definitions:
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 anew instance of the class is created:
constructor exists, JS defines a function as specified, but with the name of the class (Point2d above):
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: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:
Public Fields
Instead of defining a class instance member imperatively viathis. 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:
this. access syntax:
Avoid This
One pattern that has emerged and grown quite popular, but which I firmly believe is an anti-pattern forclass, looks like the following:
=> 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.
Consider:
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 theextends keyword, which defines a relationship between two classes:
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: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:
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) viasuper. reference, a subclass constructor must manually invoke the inherited base class constructor via super(..) function invocation:
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 theinstanceof operator:
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:
“Inheritance” Is Sharing, Not Copying
It may seem as ifPoint3d, 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.
toString() method located? On the prototype object:
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 thestatic keyword in our class bodies to distinguish these definitions:
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, andsuper can be used for base class references (and static function polymorphism), all in much the same way as inheritance works with instance members/methods:
Private Class Behavior
Everything we’ve discussed so far as part of aclass 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.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.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.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 uglytry..catch logic.
But there’s a cleaner approach, so called an “ergonomic brand check”, using the in keyword:
#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:
#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.
