This is a strawman for the inclusion of a new ClassProperty syntax:
class C {
static f() { ... }
g() {
class.f();
}
}
MemberExpression[Yield, Await] :
ClassProperty[?Yield, ?Await]
ClassProperty[Yield, Await] :
`class` `[` Expression[+In, ?Yield, ?Await] `]`
`class` `.` IdentifierName
- Function Environment Records have a new field in Table 15:
Field Name Value Meaning [[ClassObject]] Object | undefined If the associated function has class
property access and is not an ArrowFunction, [[ClassObject]] is the class that the function is bound to as a method. The default value for [[ClassObject]] is undefined. - Function Environment Records have a new method in Table 16:
Method Purpose GetClassBase() Return the object that is the base for class
property access bound in this Environment Record. - ECMAScript Function Objects have a new internal slot in Table 27:
Interanl Slot Type Description [[ClassObject]] Object | undefined If the function has class
property access, this is the object whereclass
property lookups begin. - During ClassDefinitionEvaluation, the class constructor (F) is set as the [[ClassObject]] on the method.
- During NewFunctionEnvironment, the [[ClassObject]] is copied from the method (F) to envRec.[[ClassObject]].
- Arrow functions use the [[ClassObject]] of their containing lexical environment (similar to
super
andthis
). - A new Class Reference type is added with properties similar to Super Reference.
- When evaluating
ClassProperty: `class` `.` IdentifierName
we return a new Class Reference with the following properties:- The referenced name component is the StringValue of IdentifierName.
- The base value component is the [[ClassObject]] of GetThisEnvironment().
- The actualThis component is either:
- If the containing method is static, the current
this
binding. - Else, the [[ClassObject]] of GetThisEnvironment().
- If the containing method is static, the current
- When evaluating
ClassProperty: `class` `[` Expression `]`
we return a new Class Reference with the following properties:- The referenced name component is the result of calling ?ToPropertyKey on the result of calling GetValue on the result of evaluating Expression.
- The base value component is the [[ClassObject]] of GetThisEnvironment().
- The actualThis component is either:
- If the containing method is static, the current
this
binding. - Else, the [[ClassObject]] of GetThisEnvironment().
- If the containing method is static, the current
- GetThisValue(V) would be modified to add an optional calling argument that is set to true during EvaluateCall.
- GetThisValue(V, true) returns the thisValue component of a Class Reference in the same way that it does for a Super Reference.
- GetThisValue(V, false) returns the base value component of a Class Reference.
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:
- Able to reference static members of the containing lexical class without needing to repeat the class name.
- Able to reference static members of an anonymous class declaration or expression:
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:
- Assignments always occur on the current lexical class, which should be unsurprising to users.
Invoking class.x()
in a static method uses the current this
as the receiver (similar to the behavior of super.x()
):
class Base {
static f() {
console.log(`this.name: ${this.name}, class.name: ${class.name})`);
}
static g() {
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
This behavior provides the following benefits:
- Method invocation preserves the
this
receiver to allow for overriding static methods in a subclass. - Invocation behavior is similar to
super.x()
, so should be less surprising to users.
Invoking class.x()
in a non-static method or 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})`);
}
g() {
class.f();
}
}
class Sub extends Base {
}
let b = new Base();
let s = new Sub();
b.g(); // this: Base, class: Base
s.g(); // this: Base, class: Base
Base.prototype.g.call({ name: "Other" }); // this: Base, class: Base
This behavior provides the following benefits:
- Since instances will not have the lexical class constructor in their prototype hierarchy (other than through narrow corner cases),
users would not expect the lexical
this
to be passed as the receiver from non-static methods. This behavior is the most intuitive and least-surprising behavior for users.
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++; // Assignment, so `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
This proposal can also align with the current proposals for class private methods, providing access without introducing
TypeErrors due to incorrect this
while preserving the ability for subclasses to override behavior:
class Base {
static a() {
console.log("Base.a()");
class.#b();
}
static #b() {
console.log("Base.#b()");
this.c();
}
static c() {
console.log("Base.c()");
}
}
class Sub extends Base {
static c() {
console.log("Sub.c()");
}
}
Base.a(); // Base.a()\nBase.#b()\nBase.c()
Sub.a(); // Base.a()\nBase.#b()\nSub.c()
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
- ClassProperty does not provide a way to access the static members of an outer class:
However, the same issue exists for SuperProperty and can be resolved in the same way:
class Outer { static f() {} static g() { class Inner { static g() { } // cannot use `class` to access `Outer.f` } } }
class Outer { static f() {} static g() { const callOuterF = () => class.f(); class Inner { static g() { callOuterF(); } } } }
Another option might be a
class.static
meta-property. We could also consider aclass.outer
meta-property chain as well:However, with this approach it is a lot trickier to handle the receiver for a call to support subclassing. We might have to do something like this:
Where ClassReference allows you to do
f(class.static)
(ornew class.static()
), but we still could special caseclass.static.x
andclass.static[x]
.