ECMAScript class access expressions
Class access expressions seek to simplify access to static members of a class as well as provide access to static members of a class when that class is unnamed:
class C {
static f() { ... }
g() {
class.f();
}
}
Stage: 1
Champion: Ron Buckton (@rbuckton)
For detailed status of this proposal see TODO, below.
Today, ECMAScript developers can write classes with static members that can be accessed in one of two ways:
class C {
static x() {}
y() { C.x(); }
}
this
in a static member:
class C {
static x() {}
static y() { this.x(); }
}
However, there is no easy mechanism to access class statics in a class that has no name:
export default class {
static x() { }
y() {
this.constructor.x(); // Not actually guaranteed to be the correct `x`.
}
}
const x = class {
static x() { }
y() {
this.constructor.x(); // Not actually guaranteed to be the correct `x`.
}
}
It’s not guaranteed to be the correct x
in the above examples, because you could be
accessing an overridden member of a subclass.
Also, with current proposal for private static fields and methods, its very easy to
run into errors at runtime when using this
in a static method:
class C {
static #x() {}
static y() { this.#x(); }
}
class D extends C {}
D.y(); // TypeError
// from a non-static method
class C {
static f() { }
g() {
class.f();
class["f"]();
}
}
// from a static method
class C {
static f() { }
static g() {
class.f();
class["f"]();
}
}
// with static private members
class C {
static #f() {}
static g() {
class.#f();
}
}
Field Name Value Meaning [[InitialClassObject]] Object | undefined If the associated function has class
property access and is not an ArrowFunction, [[InitialClassObject]] is the class that the function is bound to as a method. The default value for [[InitialClassObject]] is undefined.
Method Purpose GetClassBinding() Return the object that is the base for class
property access bound in this Environment Record.
Interanl Slot Type Description [[InitialClassObject]] Object | undefined If the function has class
property access, this is the object whereclass
property lookups begin.
super
and this
).ClassProperty: `class` `.` IdentifierName
we return a new Reference with the following properties:
ClassProperty: `class` `[` Expression `]`
we return a new Reference with the following properties:
ClassProperty: `class` `.` PrivateIdentifier
we perform the following steps:
fieldNameString
be the StringValue of PrivateIdentifier.bv
be the [[InitialClassObject]] of GetThisEnvironment().bv
, fieldNameString
)class
bindings are only available in the following declarations:
static
intialization block (see http://github.com/tc39/proposal-class-static-block)class
bindings are not valid in any of the following declarations:
class
declaration, including module or global scope.super
.In class methods or the class constructor, getting the value of class.x
always refers to the value of the property x
on the
containing lexical class:
class Base {
static f() {
console.log(`this: ${this.name}, class: ${class.name})`);
}
}
class Sub extends Base {
}
Base.f(); // this: Base, class: Base
Sub.f(); // this: Sub, class: Base
Base.f.call({ name: "Other" }); // this: Other, class: Base
This behavior provides the following benefits:
export default class {
static f() { ... }
g() { class.f(); }
}
In class methods or the class constructor, setting the value of class.x
always updates the value of the property x
on the
containing lexical class:
function print(F) {
const { name, x, y } = F;
const hasX = F.hasOwnProperty("x") ? "own" : "inherited";
const hasY = F.hasOwnProperty("y") ? "own" : "inherited";
console.log(`${name}.x: ${x} (${hasX}), ${name}.y: ${y} (${hasY})`);
}
class Base {
static f() {
this.x++;
class.y++;
}
}
Base.x = 0;
Base.y = 0;
class Sub extends Base {
}
print(Base); // Base.x: 0 (own), Base.y: 0 (own)
print(Sub); // Sub.x: 0 (inherited), Sub.y: 0 (inherited)
Base.f();
print(Base); // Base.x: 1 (own), Base.y: 1 (own)
print(Sub); // Sub.x: 1 (inherited), Sub.y: 1 (inherited)
Sub.f();
print(Base); // Base.x: 1 (own), Base.y: 2 (own)
print(Sub); // Sub.x: 2 (own), Sub.y: 2 (inherited)
Base.f();
print(Base); // Base.x: 2 (own), Base.y: 3 (own)
print(Sub); // Sub.x: 2 (own), Sub.y: 3 (inherited)
This behavior provides the following benefits:
Invoking class.x()
in a method, an initializer, or in the constructor uses the value of containing lexical class as the receiver:
class Base {
static f() {
console.log(`this.name: ${this.name}, class.name: ${class.name})`);
}
static g() {
class.f();
}
h() {
class.f();
}
}
class Sub extends Base {
}
Base.g(); // this: Base, class: Base
Sub.g(); // this: Sub, class: Base
Base.g.call({ name: "Other" }); // this: Other, class: Base
let b = new Base();
let s = new Sub();
b.h(); // this: Base, class: Base
s.h(); // this: Sub, class: Base
b.h.call({ name: "Other" }); // this: Other, class: Base
class C {
static x = 1;
constructor() {
function f() {
return class.x; // function has no access to `class.`
}
f(); // throws TypeError
const obj = {
method() {
return class.x; // method of object literal has no access to `class`.
}
};
obj.method(); // throws TypeError
}
}
MemberExpression[Yield, Await] :
ClassProperty[?Yield, ?Await]
ClassProperty[Yield, Await] :
`class` `[` Expression[+In, ?Yield, ?Await] `]`
`class` `.` IdentifierName
`class` `.` PrivateIdentifier
This proposal can easily align with the current class fields proposal, providing easier access to static fields without unexpected behavior:
class Base {
static counter = 0;
id = class.counter++; // `Base` is used as `this`
}
class Sub extends Base {
}
console.log(new Base().id); // 0
console.log(new Sub().id); // 1
console.log(Base.counter); // 2
console.log(Sub.counter); // 2
In addition to private methods, this proposal can also align with the current proposals for class private fields, providing
access to class static private state without introducing TypeErrors due to incorrect this
:
class Base {
static #counter = 0;
static increment() {
return class.#counter++;
}
}
class Sub extends Base {
}
console.log(Base.increment()); // 0
console.log(Sub.increment()); // 1
console.log(Base.increment()); // 2
One of the benefits of class
access expressions is that they guarantee the correct reference is used when accessing static private members. Special care must be taken, however, when invoking private and non-private static methods using class
access expressions, as the this
binding within the invoked method will be the lexical class declaration:
class Base {
static #counter = 0;
static #increment() {
class.#counter++;
this.printCounter();
}
static doIncrement() {
class.#increment();
}
static printCounter() {
console.log(class.#counter);
}
}
class Sub extends Base {
static printCounter() {
console.log("Custom Counter");
super.printCounter();
}
}
Base.doIncrement(); // prints: 1
Sub.doIncrement(); // prints: 2
In the example above, Sub
's overriden printCounter
is never invoked. Such method calls would need to be rewritten:
// option 1:
class Base {
...
static #increment() {
...
}
static doIncrement() {
class.#increment.call(this);
}
}
// option 2:
class Base {
...
static #increment(C) {
...
C.printCounter();
}
...
static doIncrement() {
class.#increment(this);
}
}
This is due to the fact that class.
in this example is essentially a substitute for Base.
, therefore Base
becomes the receiver in these method calls.
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process: