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:
- "instance methods" from "static methods" (aka "class methods") and
- "instance properties" (aka "instance fields" or "instance variables") from "static properties" (aka "static fields" or "class variables").
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'
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"
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"