Dirk Harriman Banner Image

 

Notes Javascript - Classes


 

 

Classes


// DECLARATION class Rectangle { constructor(height, width) { this.height = height; this.width = width; } } // EXPRESSION; THE CLASS IS ANONYMOUS BUT ASSIGNED TO A VARIABLE const Rectangle = class { constructor(height, width) { this.height = height; this.width = width; } }; // EXPRESSION; THE CLASS HAS ITS OWN NAME const Rectangle = class Rectangle2 { constructor(height, width) { this.height = height; this.width = width; } };

class MyClass { // CONSTRUCTOR constructor() { // CONSTRUCTOR BODY } // INSTANCE FIELD myField = "foo"; // INSTANCE METHOD myMethod() { // myMethod body } // STATIC FIELD static myStaticField = "bar"; // STATIC METHOD static myStaticMethod() { // myStaticMethod body } // STATIC BLOCK static { // STATIC INITIALIZATION CODE } // Fields, methods, static fields, and static methods all have // "private" forms #myPrivateField = "bar"; }

Class Hoisting

Unlike function declarations, class declarations are not hoisted (or, in some interpretations, hoisted but with the temporal dead zone restriction), which means you cannot use a class before it is declared.

Constructor

A color class that represents the three values r, g & b, representing Red, Green & Blue.

class Color { constructor(r, g, b) { // ASSIGN THE RGB VALUES AS A PROPERTY OF 'this'. this.values = [r, g, b]; } getRed() {return this.values[0];} getGreen() {return this.values[1];} getBlue() {return this.values[2];} setRed(value) {this.values[0] = value;} setGreen(value) {this.values[1] = value;} setBlue(value) {this.values[2] = value;} getHexColor() { let hexRed = ""; let hexGreen = ""; let hexBlue = ""; hexRed = this.values[0].toString(16); if (hexRed.length == 1) {hexRed += "0";} hexGreen += this.values[1].toString(16); if (hexGreen.length == 1) {hexGreen += "0";} hexBlue += this.values[2].toString(16); if (hexBlue.length == 1) {hexBlue += "0";} return hexRed + hexGreen + hexBlue; } setHexWhole(hexWhole) { // BREAK STRING INTO TWO CHARACTER PIECES const hexRedString = hexWhole.substring(0, 2); const hexGreenString = hexWhole.substring(2, 4); const hexBlueString = hexWhole.substring(4, 6); // CONVERT THE HEXADECIMAL STRING TO A NUMBER. this.values[0] = parseInt(hexRedString, 16); this.values[1] = parseInt(hexGreenString, 16); this.values[2] = parseInt(hexBlueString, 16); } } function runScript1(){ let divResult = document.getElementById("results1"); let myColor = new Color(255,55,25); divResult.innerHTML = "Red Value: "+ myColor.getRed(); divResult.innerHTML += "<br/>Green Value: "+ myColor.getGreen(); divResult.innerHTML += "<br/>Blue Value: "+ myColor.getBlue(); divResult.innerHTML += "<br/>"+ myColor.getHexColor(); myColor.setRed(12); myColor.setGreen(20); myColor.setBlue(100); divResult.innerHTML += "<br/><b>Change Colors:</b>"; divResult.innerHTML += "<br/>Red Value: "+ myColor.getRed(); divResult.innerHTML += "<br/>Green Value: "+ myColor.getGreen(); divResult.innerHTML += "<br/>Blue Value: "+ myColor.getBlue(); divResult.innerHTML += "<br/>"+ myColor.getHexColor(); myColor.setHex("FF","09","CD"); divResult.innerHTML += "<br/><b>Set With Hex:</b>"; divResult.innerHTML += "<br/>Red Value: "+ myColor.getRed(); divResult.innerHTML += "<br/>Green Value: "+ myColor.getGreen(); divResult.innerHTML += "<br/>Blue Value: "+ myColor.getBlue(); divResult.innerHTML += "<br/>"+ myColor.getHexColor(); myColor.setHexWhole("AABD25"); divResult.innerHTML += "<br/><b>Set With Whole Hex:</b>"; divResult.innerHTML += "<br/>Red Value: "+ myColor.getRed(); divResult.innerHTML += "<br/>Green Value: "+ myColor.getGreen(); divResult.innerHTML += "<br/>Blue Value: "+ myColor.getBlue(); divResult.innerHTML += "<br/>"+ myColor.getHexColor(); }

Run Script 1 
 

Response


 

In the above code, the Color class's value field is not private. Code can be written to modify it without error.

myColor.values[0] = 55; // THIS WILL NOT CAUSE AN ACCESS ERROR

Private Fields

A private field is an identifier prefixed with # (the hash symbol). The hash is an integral part of the field's name, which means a private property can never have name clash with a public property. In order to refer to a private field anywhere in the class, you must declare it in the class body (you cannot create a private property on the fly). Apart from this, a private field is pretty much equivalent to a normal property.

class Color { // DECLARE: EVERY COLOR INSTANCE HAS A PRIVATE FIELD CALLED #values. #values; constructor(r, g, b) { this.#values = [r, g, b]; } getRed() {return this.#values[0];} ... setRed(value) {this.#values[0] = value;} ... }

Private fields in JavaScript are hard private: if the class does not implement methods that expose these private fields, there's absolutely no mechanism to retrieve them from outside the class. This means you are safe to do any refactors to your class's private fields, as long as the behavior of exposed methods stay the same.

After we've made the values field private, we can add some more logic in the getRed and setRed methods, instead of making them simple pass-through methods. For example, we can add a check in setRed to see if it's a valid R value:

class Color { #values; constructor(r, g, b) { this.#values = [r, g, b]; } getRed() { return this.#values[0]; } setRed(value) { if (value < 0 || value > 255) { throw new RangeError("Invalid R value"); } this.#values[0] = value; } }

class ColorWeb { #values constructor(r, g, b) { // Assign the RGB values as a property of `this`. this.#values = [r, g, b]; } getRed() {return this.#values[0];} getGreen() {return this.#values[1];} getBlue() {return this.#values[2];} setRed(value) { if (value < 0 || value > 255) { alert("Invalid Red Value - RangeError"); throw new RangeError("Invalid R value"); } this.#values[0] = value; } setGreen(value) { if (value < 0 || value > 255) { alert("Invalid Green Value - RangeError"); throw new RangeError("Invalid G value"); } this.#values[0] = value; } setBlue(value) { if (value < 0 || value > 255) { alert("Invalid Blue Value - RangeError"); throw new RangeError("Invalid B value"); } this.#values[0] = value; } getHexColor() { let hexRed = ""; let hexGreen = ""; let hexBlue = ""; hexRed = this.#values[0].toString(16); if (hexRed.length == 1) {hexRed = "0" + hexRed;} hexGreen += this.#values[1].toString(16); if (hexGreen.length == 1) {hexGreen = "0" + hexGreen;} hexBlue += this.#values[2].toString(16); if (hexBlue.length == 1) {hexBlue = "0" + hexBlue;} return hexRed + hexGreen + hexBlue; } setHex(hexRed,hexGreen,hexBlue) { // CONVERT THE HEXADECIMAL NUMBER TO A STRING. const hexRedString = hexRed.toString(); const hexGreenString = hexGreen.toString(); const hexBlueString = hexBlue.toString(); // CONVERT THE HEXADECIMAL STRING TO A NUMBER. this.#values[0] = parseInt(hexRedString, 16); this.#values[1] = parseInt(hexGreenString, 16); this.#values[2] = parseInt(hexBlueString, 16); } setHexWhole(hexWhole) { // BREAK STRING INTO TWO CHARACTER PIECES const hexRedString = hexWhole.substring(0, 2); const hexGreenString = hexWhole.substring(2, 4); const hexBlueString = hexWhole.substring(4, 6); // CONVERT THE HEXADECIMAL STRING TO A NUMBER. this.#values[0] = parseInt(hexRedString, 16); this.#values[1] = parseInt(hexGreenString, 16); this.#values[2] = parseInt(hexBlueString, 16); } } function runScript2(){ let divResult = document.getElementById("results2"); let myColor = new ColorWeb(255,55,25); divResult.innerHTML += "<br/><b>Set With Valid Numbers:</b>"; divResult.innerHTML += "<br/>Red Value: "+ myColor.getRed(); divResult.innerHTML += "<br/>Green Value: "+ myColor.getGreen(); divResult.innerHTML += "<br/>Blue Value: "+ myColor.getBlue(); divResult.innerHTML += "<br/>"+ myColor.getHexColor(); divResult.innerHTML += "<br/><b>Set With An Invalid Number:</b>"; myColor.setRed(12); myColor.setGreen(256); myColor.setBlue(100); divResult.innerHTML += "<br/>Red Value: "+ myColor.getRed(); divResult.innerHTML += "<br/>Green Value: "+ myColor.getGreen(); divResult.innerHTML += "<br/>Blue Value: "+ myColor.getBlue(); divResult.innerHTML += "<br/>"+ myColor.getHexColor(); }

caught RangeError: Invalid G value

Run Script 2 
 

Response


 
Private Field Scope

A class method can read the private fields of other instances, as long as they belong to the same class.
In the following code, a method called redDifference(anotherColor), compares the value of the current class instance with that of another class instance.

class Color { #values; constructor(r, g, b) { this.#values = [r, g, b]; } redDifference(anotherColor) { // #values doesn't necessarily need to be accessed from this: // you can access private fields of other instances belonging // to the same class. return this.#values[0] - anotherColor.#values[0]; } } const red = new Color(255, 0, 0); const crimson = new Color(220, 20, 60); red.redDifference(crimson); // 35

However, if anotherColor is not a Color instance, #values won't exist. (Even if another class has an identically named #values private field, it's not referring to the same thing and cannot be accessed here.) Accessing a nonexistent private property throws an error instead of returning undefined like normal properties do. If you don't know if a private field exists on an object and you wish to access it without using try/catch to handle the error, you can use the in operator.

class Color { #values; constructor(r, g, b) { this.#values = [r, g, b]; } redDifference(anotherColor) { if (!(#values in anotherColor)) { throw new TypeError("Color instance expected"); } return this.#values[0] - anotherColor.#values[0]; } }

Private Getters & Setters

Methods, getters, and setters can be private as well. They're useful when you have something complex that the class needs to do internally but no other part of the code should be allowed to call.

For example, imagine creating HTML custom elements that should do something somewhat complicated when clicked/tapped/otherwise activated. Furthermore, the somewhat complicated things that happen when the element is clicked should be restricted to this class, because no other part of the JavaScript will (or should) ever access it.

class Counter extends HTMLElement { #xValue = 0; constructor() { super(); this.onclick = this.#clicked.bind(this); } get #x() { return this.#xValue; } set #x(value) { this.#xValue = value; window.requestAnimationFrame(this.#render.bind(this)); } #clicked() { this.#x++; } #render() { this.textContent = this.#x.toString(); } connectedCallback() { this.#render(); } } customElements.define("num-counter", Counter);

In this case, pretty much every field and method is private to the class. Thus, it presents an interface to the rest of the code that's essentially just like a built-in HTML element. No other part of the program has the power to affect any of the internals of Counter.

Accessor Fields

color.getRed() and color.setRed() allow us to read and write to the red value of a color. If you come from languages like Java, you will be very familiar with this pattern. However, using methods to simply access a property is still somewhat unergonomic in JavaScript. Accessor fields allow us to manipulate something as if its an "actual property".

class Color { constructor(r, g, b) { this.values = [r, g, b]; } get red() { return this.values[0]; } set red(value) { this.values[0] = value; } } const red = new Color(255, 0, 0); red.red = 0; console.log(red.red); // 0

It looks as if the object has a property called red, but actually, no such property exists on the instance!
There are only two methods, but they are prefixed with get and set, which allows them to be manipulated as if they were properties.

Read Only

If a field only has a getter but no setter, it will be effectively read-only.

class Color { constructor(r, g, b) { this.values = [r, g, b]; } get red() { return this.values[0]; } } const red = new Color(255, 0, 0); red.red = 0; console.log(red.red); // 255

In strict mode, the red.red = 0 line will throw a type error: "Cannot set property red of #<Color> which has only a getter". In non-strict mode, the assignment is silently ignored.

Public Fields

Private fields also have their public counterparts, which allow every instance to have a property.
Fields are usually designed to be independent of the constructor's parameters.

class MyClass { luckyNumber = Math.random(); ... } console.log(new MyClass().luckyNumber); // 0.5 console.log(new MyClass().luckyNumber); // 0.3

Public fields are almost equivalent to assigning a property to this. For example, the above example can also be converted to:

class MyClass { constructor() { this.luckyNumber = Math.random(); } }

Static Properties

A static property is a property that does not exist for each class instance, but it belongs collectively to all class instances. This is helpful for situations where you need information about a class collectively rather than instance-wise.

The javascript Date object has a Date.now() method, which returns the current date. This method does not belong to any date instance, it belongs to the class itself. However, it's put on the Date class instead of being exposed as a global DateNow() function, because it's mostly useful when dealing with date instances.

Note: Prefixing utility methods with what they deal with is called namespacing and is considered a good practice. For example, in addition to the older, unprefixed parseInt() method, JavaScript also later added the prefixed Number.parseInt() method to indicate that it's for dealing with numbers.

Static properties are a group of class features that are defined on the class itself, rather than on individual instances of the class. These features include:

Everything also has private counterparts. For example, for our Color class, we can create a static method that checks whether a given triplet is a valid RGB value:

class Color { static isValid(r, g, b) { return r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255; } } Color.isValid(255, 0, 0); // true Color.isValid(1000, 0, 0); // false

Static Properties

Static properties are very similar to their instance counterparts, except that:

console.log(new Color(0, 0, 0).isValid); // UNDEFINED

Static Initialization Block

There is also a special construct called a static initialization block, which is a block of code that runs when the class is first loaded.

Static initialization blocks are declared within a class. It contains statements to be evaluated during class initialization. This permits more flexible initialization logic than static properties, such as using try...catch or setting multiple fields from a single value. Initialization is performed in the context of the current class declaration, with access to private state, which allows the class to share information of its private properties with other classes or functions declared in the same scope (analogous to "friend" classes in C++).

class ClassWithStaticInitializationBlock { static staticProperty1 = 'Property 1'; static staticProperty2; static { this.staticProperty2 = 'Property 2'; } } console.log(ClassWithStaticInitializationBlock.staticProperty1); // Expected output: "Property 1" console.log(ClassWithStaticInitializationBlock.staticProperty2); // Expected output: "Property 2"

class ClassWithSIB { static { // ... } }

Without static initialization blocks, complex static initialization might be achieved by calling a static method after the class declaration:

class MyClass { static init() { // Access to private static fields is allowed here } } MyClass.init();

However, this approach exposes an implementation detail (the init() method) to the user of the class. On the other hand, any initialization logic declared outside the class does not have access to private static fields. Static initialization blocks allow arbitrary initialization logic to be declared within the class and executed during class evaluation.

A class can have any number of static {} initialization blocks in its class body. These are evaluated, along with any interleaved static field initializers, in the order they are declared. Any static initialization of a super class is performed first, before that of its sub classes.

The scope of the variables declared inside the static block is local to the block. This includes var, function, const, and let declarations. var declarations in the block are not hoisted.

var y = "Outer y"; class A { static field = "Inner y"; static { var y = this.field; } } // var defined in static block is not hoisted console.log(y); // 'Outer y'

The this inside a static block refers to the constructor object of the class.
super.property can be used to access static properties of the super class.
Note however that it is a syntax error to call super() in a class static initialization block, or to use the arguments object.

The statements are evaluated synchronously. You cannot use await or yield in this block. (Think of the initialization statements as being implicitly wrapped in a function.)

The scope of the static block is nested within the lexical scope of the class body, and can access private names declared within the class without causing a syntax error.

Static field initializers and static initialization blocks are evaluated one-by-one. The initialization block can refer to field values above it, but not below it. All static methods are added beforehand and can be accessed, although calling them may not behave as expected if they refer to fields below the current block.

Note: This is more important with private static fields, because accessing a non-initialized private field throws a TypeError, even if the private field is declared below. (If the private field is not declared, it would be an early SyntaxError.)

A static initialization block may not have decorators (the class itself may).

Multiple Blocks

The code below demonstrates a class with static initialization blocks and interleaved static field initializers. The output shows that the blocks and fields are evaluated in execution order.

class MyClass { static field1 = console.log("static field1"); static { console.log("static block1"); } static field2 = console.log("static field2"); static { console.log("static block2"); } } // 'static field1' // 'static block1' // 'static field2' // 'static block2'

Note that any static initialization of a super class is performed first, before that of its sub classes.

Using this & super

The this inside a static block refers to the constructor object of the class. This code shows how to access a public static field.

class A { static field = "static field"; static { console.log(this.field); } } // 'static field'

The super.property syntax can be used inside a static block to reference static properties of a super class.

class A { static field = "static field"; } class B extends A { static { console.log(super.field); } } // 'static field'

Access to Private Properties

This example below shows how access can be granted to a private instance field of a class from an object outside the class (example from the v8.dev blog):

let getDPrivateField; class D { #privateField; constructor(v) { this.#privateField = v; } static { getDPrivateField = (d) => d.#privateField; } } console.log(getDPrivateField(new D("private"))); // 'private'

class MyClass { static { MyClass.myStaticProperty = "foo"; } } console.log(MyClass.myStaticProperty); // 'foo'

Static initialization blocks are almost equivalent to immediately executing some code after a class has been declared. The only difference is that they have access to static private properties.


 
Extends & Inheritance

A key feature that classes bring about (in addition to ergonomic encapsulation with private fields) is inheritance, which means one object can "borrow" a large part of another object's behaviors, while overriding or enhancing certain parts with its own logic.

For example, suppose our Color class now needs to support transparency. We may be tempted to add a new field that indicates its transparency:

class Color { #values; constructor(r, g, b, a = 1) { this.#values = [r, g, b, a]; } get alpha() { return this.#values[3]; } set alpha(value) { if (value < 0 || value > 1) { throw new RangeError("Alpha value must be between 0 and 1"); } this.#values[3] = value; } }

However, this means every instance, even the vast majority which aren't transparent (those with an alpha value of 1), will have to have the extra alpha value, which is not very elegant. Plus, if the features keep growing, our Color class will become very bloated and hard to maintain.

Instead, in object-oriented programming, we would create a derived class. The derived class has access to all public properties of the parent class. In JavaScript, derived classes are declared with an extends clause, which indicates the class it extends from.

class ColorWithAlpha extends Color { #alpha; constructor(r, g, b, a) { super(r, g, b); this.#alpha = a; } get alpha() { return this.#alpha; } set alpha(value) { if (value < 0 || value > 1) { throw new RangeError("Alpha value must be between 0 and 1"); } this.#alpha = value; } }

There are a few things that have immediately come to attention. First is that in the constructor, we are calling super(r, g, b). It is a language requirement to call super() before accessing this. The super() call calls the parent class's constructor to initialize this, here it's roughly equivalent to this = new Color(r, g, b). You can have code before super(), but you cannot access this before super(), the language prevents you from accessing the uninitialized this.

After the parent class is done with modifying this, the derived class can do its own logic. Here we added a private field called #alpha, and also provided a pair of getter/setters to interact with them.

A derived class inherits all methods from its parent. For example, although ColorWithAlpha doesn't declare a get red() accessor itself, you can still access red because this behavior is specified by the parent class:

const color = new ColorWithAlpha(255, 0, 0, 0.5); console.log(color.red); // 255

Derived classes can also override methods from the parent class. For example, all classes implicitly inherit the Object class, which defines some basic methods like toString(). However, the base toString() method is notoriously useless, because it prints [object Object] in most cases:

console.log(red.toString()); // [object Object]

Instead, our class can override it to print the color's RGB values:

class Color { #values; // ... toString() { return this.#values.join(", "); } } console.log(new Color(255, 0, 0).toString()); // '255, 0, 0'

Within derived classes, you can access the parent class's methods by using super. This allows you to build enhancement methods and avoid code duplication.

class ColorWithAlpha extends Color { #alpha; // … toString() { // Call the parent class's toString() and build on the return value return `${super.toString()}, ${this.#alpha}`; } } console.log(new ColorWithAlpha(255, 0, 0, 0.5).toString()); // '255, 0, 0, 0.5'

When you use extends, the static methods inherit from each other as well, so you can also override or enhance them.

class ColorWithAlpha extends Color { // ... static isValid(r, g, b, a) { // Call the parent class's isValid() and build on the return value return super.isValid(r, g, b) && a >= 0 && a <= 1; } } console.log(ColorWithAlpha.isValid(255, 0, 0, -1)); // false

Derived classes do not have access to the parent class's private fields, this is another key aspect to JavaScript private fields being "hard private". Private fields are scoped to the class body itself and do not grant access to any outside code.

class ColorWithAlpha extends Color { log() { console.log(this.#values); // SyntaxError: Private field '#values' must be declared in an enclosing class } }

A class can only extend from one class. This prevents problems in multiple inheritance like the diamond problem. However, due to the dynamic nature of JavaScript, it's still possible to achieve the effect of multiple inheritance through class composition and mixins.

Instances of derived classes are also instances of the base class.

const color = new ColorWithAlpha(255, 0, 0, 0.5); console.log(color instanceof Color); // true console.log(color instanceof ColorWithAlpha); // true

class ColorWeb { #values constructor(r, g, b) { this.#values = [r, g, b]; } getRed() {return this.#values[0];} getGreen() {return this.#values[1];} getBlue() {return this.#values[2];} setRed(value) { if (value < 0 || value > 255) { alert("Invalid Red Value - RangeError"); throw new RangeError("Invalid R value"); } this.#values[0] = value; } setGreen(value) { if (value < 0 || value > 255) { alert("Invalid Green Value - RangeError"); throw new RangeError("Invalid G value"); } this.#values[0] = value; } setBlue(value) { if (value < 0 || value > 255) { alert("Invalid Blue Value - RangeError"); throw new RangeError("Invalid B value"); } this.#values[0] = value; } getHexColor() { let hexRed = ""; let hexGreen = ""; let hexBlue = ""; hexRed = this.#values[0].toString(16); if (hexRed.length == 1) {hexRed = "0" + hexRed;} hexGreen += this.#values[1].toString(16); if (hexGreen.length == 1) {hexGreen = "0" + hexGreen;} hexBlue += this.#values[2].toString(16); if (hexBlue.length == 1) {hexBlue = "0" + hexBlue;} return hexRed + hexGreen + hexBlue; } setHex(hexRed,hexGreen,hexBlue) { // CONVERT THE HEXADECIMAL NUMBER TO A STRING. const hexRedString = hexRed.toString(); const hexGreenString = hexGreen.toString(); const hexBlueString = hexBlue.toString(); // CONVERT THE HEXADECIMAL STRING TO A NUMBER. this.#values[0] = parseInt(hexRedString, 16); this.#values[1] = parseInt(hexGreenString, 16); this.#values[2] = parseInt(hexBlueString, 16); } setHexWhole(hexWhole) { // BREAK STRING INTO TWO CHARACTER PIECES const hexRedString = hexWhole.substring(0, 2); const hexGreenString = hexWhole.substring(2, 4); const hexBlueString = hexWhole.substring(4, 6); // CONVERT THE HEXADECIMAL STRING TO A NUMBER. this.#values[0] = parseInt(hexRedString, 16); this.#values[1] = parseInt(hexGreenString, 16); this.#values[2] = parseInt(hexBlueString, 16); } toString() { return this.#values.join(", "); } } class ColorWithAlpha extends ColorWeb { #alpha; constructor(r, g, b, a) { super(r, g, b); this.#alpha = a; } get alpha() { return this.#alpha; } set alpha(value) { if (value < 0 || value > 1) { throw new RangeError("Alpha value must be between 0 and 1"); } this.#alpha = value; } toString() { // Call the parent class's toString() and build on the return value return `${super.toString()}, ${this.#alpha}`; } } function runScript3(){ let divResult = document.getElementById("results3"); const color = new ColorWithAlpha(255, 0, 0, 0.5); divResult.innerHTML += "color: "+ color.toString(); }

Run Script 3 
 

Response


 

Classes & Forms


 

 

Run Script 3 
 

Response