Static class members

Instance vs. static

As mentioned before, in JavaScript classes are objects (of data type "function"). Like to any object in JavaScript, we can assign a property directly to a class. However, such property will not become a property of an instance of that class.


class MyClass {}
MyClass.staticProperty = "I am a property";
const myInstance = new MyClass();

console.log(MyClass.staticProperty); // logs: "I am a property"
console.log(myInstance.staticProperty); // logs: undefined

We can also define such a class property within the class body, by perpending the property or method by a keyword static.


class MyClass {
  static staticProperty = "I am a property";
}

const myInstance = new MyClass();

console.log(MyClass.staticProperty); // logs: "I am a property"
console.log(myInstance.staticProperty); // logs: undefined

Thus, we distinguish:

Static class members are often used for auxiliary data or in auxiliary functions pertaining to the class itself, but not to any particular instance of the class. In the next example a static counter keeps track of the number of instantiations of the class.


class MyClass { 
  static #counter = 0;   
     
  constructor () { 
    MyClass.#counter += 1;   
  }  
  
  static get counter() {
    return MyClass.#counter;
  }
} 

const myInstance1 = new MyClass();
const myInstance2 = new MyClass(); 
console.log(MyClass.counter) // logs: 2

console.log(myInstance2.counter) // logs: undefined

MyClass.counter = 5; // Ignored. In strict mode: TypeError: setting getter-only property "counter"
// MyClass.#counter = 5; // SyntaxError: reference to undeclared private field or method #counter

BTW. Note that also static private members can only be used in code within the class body. In this case they do not act "under the hood" of an instance, but "under the hood" of the class.

this and static members

Static methods and static properties cannot be called using the this keyword, from instance methods, instance properties or the constructor. The this keyword in an instance refers to the instance, not to the class. You need to call static class members from non-static class members by using the class name or by calling the method as a property of the constructor property (which references the class).

Note that it is, of course, never possible to call non-static class members from static class members.


class MyClass {
  constructor() {
    console.log(MyClass.staticProperty);
    console.log(this.constructor.staticProperty);
    console.log(MyClass.staticMethod()); 
    console.log(this.constructor.staticMethod());
  }

  static staticProperty = "static property";
  static staticMethod() {
    return "static method has been called.";
  }
}

new MyClass;
//logs:
// 'static property'
// 'static property'
// 'static method has been called.'
// 'static method has been called.'

Static members may call other static members directly using this. In this case this references the class, instead of an instance.


class MyClass {
  static staticProperty = "static property";
  static staticMethod() {
    return `Static method and ${this.staticProperty} has been called`;
  }
  static anotherStaticMethod() {
    return `${this.staticMethod()} from another static method`;
  }
}

console.log(MyClass.staticMethod());
//logs: 'Static method and static property has been called'

console.log(MyClass.anotherStaticMethod());
//logs: 'Static method and static property has been called from another static method'

Example from MDN.

Note that replacing this by MyClass in the above example, makes no direct difference. So you may consider to always use the class name instead of this to call static members. However, if you want it to be possible to call the static method on an object other than MyClass, you must use this:


class MyClass {
  static myProperty = "some property";
  static staticMethod() {
    return `Static method and ${this.myProperty} has been called`;
  }
}

console.log(MyClass.staticMethod()); //logs: 'Static method and some property has been called'
console.log(MyClass.staticMethod.call({
  myProperty: "some property of another object"
})); //logs: 'Static method and some property of another object has been called'

Simulating private constructors

As mentioned earlier, unlike in many other languages, in JavaScript the constructor is public and cannot be marked as private. To prevent classes from being constructed from outside the class, we can use a trick:


class MyClass {
  static #isInternalConstructing = false;
  publicInstanceField = "This is some property";

  constructor() {
    if (!MyClass.#isInternalConstructing) {
      throw new TypeError("MyClass is not constructable");
    }
  }

  static create() {
    MyClass.#isInternalConstructing = true;
    const instance = new MyClass();
    MyClass.#isInternalConstructing = false;
    return instance;
  }
}

let myInstance;
// myInstance = new MyClass(); // TypeError: MyClass is not constructable
myInstance = MyClass.create();

console.log(myInstance.publicInstanceField); // logs: "This is some property"

Example from MDN.

Static initialization blocks

At instantiation the constructor can be used to include the code needed to create an instance. At initialization, the creation of the class itself, we only have fields and methods at our disposal to include the code needed to create the class, unless we use static initialization blocks.

In the next example static properties y and z are assigned a value depending on static property x. A "helper" property or method, or code outside the class is needed to accomplish this, unless we use a static initialization block (static {}).


class MyClass {
  static x = 2;
  static #helper = MyClass.x * Math.random();
  static y = 3 * MyClass.#helper;
  static z = 4 * MyClass.#helper;
}

// Now without a "helper", and with a static initialization block:
class MyClass {
  static x = 2;
  static y;
  static z;
  // static initialization block:
  static {
    const factor = MyClass.x * Math.random();
	MyClass.y = 3 * factor;
	MyClass.z = 4 * factor;
  }
}

Static initialization blocks are evaluated during initialization, at class declaration. Unlike a constructor, a static initialization block does not take parameters and cannot return anything.

The scope of the variables (including var variables) and functions declared inside the static block is local to the block. The scope of the static block is nested within the lexical scope of the class body, and can access the private instance members of the class.

A class can have any number of static initialization blocks in its class body. Static initialization blocks are evaluated, along with static fields, in the order they are declared.


class MyClass {
  static field1 = console.log('Call 1');
  static {
    console.log('Call 2');
  }
  static field2 = console.log('Call 3');
  static {
    console.log('Call 4');
  }
}

/* logs:
// "Call 1"
// "Call 2"
// "Call 3"
// "Call 4"
*/

Access to private fields

There should not be any access to private members from outside the class. However, static initialization blocks make it possible to access private instance fields outside the class, since static initialization blocks have access to the class's private instance fields:


let getDPrivateField, setDPrivateField;

class MyClass {
  #privateField;
  
  constructor(v) {
    this.#privateField = v;
  }
  
  static {
    getDPrivateField = (myclass) => myclass.#privateField;
	setDPrivateField = (myclass, value) => { myclass.#privateField = value; }
  }
}

const myInstance = new MyClass("private value");
console.log(getDPrivateField(myInstance)); // logs: "private value"

setDPrivateField(myInstance, "not so private value");
console.log(getDPrivateField(myInstance)); // logs: "not so private value"

Example from v8.dev - Shu-yu Guo.