This blog post is part of a series on new members in bodies of class definitions:
This post explains private static methods and accessors in classes, as described in the ECMAScript proposal “Static class features” by Shu-yu Guo and Daniel Ehrenberg.
The following kinds of private static methods and accessors exist:
class MyClass {
static #staticPrivateOrdinaryMethod() {}
static * #staticPrivateGeneratorMethod() {}
static async #staticPrivateAsyncMethod() {}
static async * #staticPrivateAsyncGeneratorMethod() {}
static get #staticPrivateGetter() {}
static set #staticPrivateSetter(value) {}
}
The following class has a private static method .#createInternal()
:
class Point {
static create(x, y) {
return Point.#createInternal(x, y);
}
static createZero() {
return Point.#createInternal(0, 0);
}
static #createInternal(x, y) {
const p = new Point();
p.#x = x; // (A)
p.#y = y; // (B)
return p;
}
#x;
#y;
}
This code shows the key benefit of private static methods, compared to external (module-private) helper functions: They can access private instance fields (line A and line B).
this
Accessing public static constructs via this
lets us avoid redundant use of class names. It works because those constructs are inherited by subclasses. Alas, we can’t do the same for private static constructs:
class SuperClass {
static #privateData = 2;
static getPrivateDataViaThis() {
return this.#privateData;
}
static getPrivateDataViaClassName() {
return SuperClass.#privateData;
}
}
class SubClass extends SuperClass {
}
// Works:
assert.equal(SuperClass.getPrivateDataViaThis(), 2);
// Error:
assert.throws(
() => SubClass.getPrivateDataViaThis(), // (A)
{
name: 'TypeError',
message: 'Cannot read private member #privateData from an object whose class did not declare it',
}
);
// Work-around for previous error:
assert.equal(SubClass.getPrivateDataViaClassName(), 2);
The problem in line A is that this
points to SubClass
which does not have the private field .#privateData
. For more information, see the blog post “Private class fields”.
Support for static methods and accessors is based on mechanisms that were introduced for prototype methods and accessors (more information).
We will only examine private methods, but everything we discover also applies to private getters and setters.
Consider a method .#privateMethod()
that is created “inside” an object HomeObj
. This method is stored externally, in a specification data structure called private name. Private names are also used to represent other private class elements. They are looked up via private environments, which map identifiers to private names and exist next to environments for variables. Private environments are explained later.
In this case, the private name has the following slots:
.[[Description]] = "#privateMethod"
.[[Kind]] = "method"
.[[Brand]] = HomeObj
.[[Value]]
points to the method object (a function)The brand of a private method is the object it was created in.
Each object Obj
has an internal slot Obj.[[PrivateBrands]]
which contains the brands of all methods that can be invoked on Obj
. There are two ways in which elements are added to the private brands of an object:
When a class C
is new
-invoked, it adds C.prototype
to the private brands of this
. That means that C
’s private prototype methods (whose brand is C.prototype
) can be invoked on this
.
If a class C
has private static methods, C
is added to the private brands of C
. That means that C
’s private static methods (whose brand is C
) can be invoked on C
.
Therefore, the private brands of an object are related to the prototype chain of an object. Why has this mechanism been introduced if it is so similar?
Private methods are designed to be actually private and to have integrity. That means that they shouldn’t be affected by outside changes. If the private brands of an object were determined by its prototype chain, we could enable or disable private methods by changing the chain. We could also observe part of the executions of private methods by observing the traversal of the prototype chain via a Proxy.
This approach guarantees that, when we invoke a private method on an object, its private fields also exist (as created by constructors and evaluations of class definitions). Otherwise, we could use Object.create()
to create an instance without private instance fields to which we could apply private methods.
Execution contexts now have three environments:
LexicalEnvironment
points to the environment for let
and const
(block scoping).VariableEnvironment
points to the environment for var
(function scoping).PrivateEnvironment
points to an environment that maps identifiers prefixed with #
to private name records.Functions now have two lexical environments:
[[Environment]]
refers to the environment of the scope in which the function was created.[[PrivateEnvironment]]
refers to the environment with the private names that was active when the function was created.The operation ClassDefinitionEvaluation
temporarily changes the current execution context for the body of a class:
LexicalEnvironment
is set to classScope
, a new declarative environment.PrivateEnvironment
is set to classPrivateEnvironment
, a new declarative environment.For each identifier dn
of the PrivateBoundIdentifiers
of the class body, one entry is added to the EnvironmentRecord
of classPrivateEnvironment
. The key of that entry is dn
, the value is a new private name.
The following parts of the runtime semantics rule ClassDefinitionEvaluation
are relevant for static private constructs (F
refers to the constructor):
Step 28.b.i: Perform PropertyDefinitionEvaluation(F, false)
for each static ClassElement
staticFields
(so that it can be attached to F
later).Step 33.a: If there is a static method or accessor P
in PrivateBoundIdentifiers
of ClassBody
and P
’s .[[Brand]]
is F
: Execute PrivateBrandAdd(F, F)
. Intuitively, that means: object F
can be receiver of methods stored in object F
Step 34.a: For each fieldRecord
in staticFields
: DefineField(F, fieldRecord)
Let’s look at an example. Consider the following code from earlier in this post:
class Point {
static create(x, y) {
return Point.#createInternal(x, y);
}
static createZero() {
return Point.#createInternal(0, 0);
}
static #createInternal(x, y) {
const p = new Point();
p.#x = x;
p.#y = y;
return p;
}
#x;
#y;
toArray() {
return [this.#x, this.#y];
}
}
Internally, it is roughly represented as follows:
{ // Begin of class scope
class Object {
// Maps private names to values (a list in the spec).
__PrivateFieldValues__ = new Map();
// Prototypes with associated private members
__PrivateBrands__ = [];
}
// Private name
const __x = {
__Description__: '#x',
__Kind__: 'field',
};
// Private name
const __y = {
__Description__: '#y',
__Kind__: 'field',
};
class Point extends Object {
static __PrivateBrands__ = [Point];
static __PrivateBrand__ = Point.prototype;
static __Fields__ = [__x, __y];
static create(x, y) {
PrivateBrandCheck(Point, __createInternal);
return __createInternal.__Value__.call(Point, x, y);
}
static createZero() {
PrivateBrandCheck(Point, __createInternal);
return __createInternal.__Value__.call(Point, 0, 0);
}
constructor() {
super();
// Setup before constructor
InitializeInstanceElements(this, Point);
// Constructor itself is empty
}
toArray() {
return [
this.__PrivateFieldValues__.get(__x),
this.__PrivateFieldValues__.get(__y),
];
}
dist() {
PrivateBrandCheck(this, __computeDist);
__computeDist.__Value__.call(this);
}
}
// Private name
const __createInternal = {
__Description__: '#createInternal',
__Kind__: 'method',
__Brand__: Point,
__Value__: function (x, y) {
const p = new Point();
p.__PrivateFieldValues__.set(__x, x);
p.__PrivateFieldValues__.set(__y, y);
return p;
},
};
} // End of class scope
function InitializeInstanceElements(O, constructor) {
if (constructor.__PrivateBrand__) {
O.__PrivateBrands__.push(constructor.__PrivateBrand__);
}
const fieldRecords = constructor.__Fields__;
for (const fieldRecord of fieldRecords) {
O.__PrivateFieldValues__.set(fieldRecord, undefined);
}
}
function PrivateBrandCheck(obj, privateName) {
if (! obj.__PrivateBrands__.includes(privateName.__Brand__)) {
throw new TypeError();
}
}
Acknowledgements: