Skip to content

Commit a942a31

Browse files
authored
Merge pull request #5 from tc39/rbuckton-patch-1
Simplify class access expression semantics
2 parents 3d5fe15 + 944ef8a commit a942a31

File tree

1 file changed

+76
-74
lines changed

1 file changed

+76
-74
lines changed

README.md

Lines changed: 76 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# ECMAScript class property access expressions
33

44
Class access expressions seek to simplify access to static members of a class as well as provide
5-
access to static members of a class that cannot be named:
5+
access to static members of a class when that class is unnamed:
66

77
```js
88
class C {
@@ -150,22 +150,16 @@ class C {
150150
- During ClassDefinitionEvaluation, the class constructor (<var>F</var>) is set as the \[\[ClassObject]] on the method.
151151
- During NewFunctionEnvironment, the \[\[ClassObject]] is copied from the method (<var>F</var>) to <var>envRec</var>.\[\[ClassObject]].
152152
- Arrow functions use the \[\[ClassObject]] of their containing lexical environment (similar to `super` and `this`).
153-
- A new Class Reference type is added with properties similar to Super Reference.
154-
- When evaluating ``ClassProperty: `class` `.` IdentifierName`` we return a new Class Reference with the following properties:
153+
- When evaluating ``ClassProperty: `class` `.` IdentifierName`` we return a new Reference with the following properties:
155154
- The referenced name component is the StringValue of _IdentifierName_.
156155
- The base value component is the \[\[ClassObject]] of GetThisEnvironment().
157-
- The actualThis component is either:
158-
- If the containing method is static, the current `this` binding.
159-
- Else, the \[\[ClassObject]] of GetThisEnvironment().
160-
- When evaluating ``ClassProperty: `class` `[` Expression `]` `` we return a new Class Reference with the following properties:
156+
- When evaluating ``ClassProperty: `class` `[` Expression `]` `` we return a new Reference with the following properties:
161157
- The referenced name component is the result of calling ?ToPropertyKey on the result of calling GetValue on the result of evaluating _Expression_.
162158
- The base value component is the \[\[ClassObject]] of GetThisEnvironment().
163-
- The actualThis component is either:
164-
- If the containing method is static, the current `this` binding.
165-
- Else, the \[\[ClassObject]] of GetThisEnvironment().
166-
- GetThisValue(<var>V</var>) would be modified to add an optional <var>calling</var> argument that is set to **true** during EvaluateCall.
167-
- GetThisValue(<var>V</var>, **true**) returns the thisValue component of a Class Reference in the same way that it does for a Super Reference.
168-
- GetThisValue(<var>V</var>, **false**) returns the base value component of a Class Reference.
159+
- When evaluating ``ClassProperty: `class` `.` PrivateIdentifier`` we perform the following steps:
160+
1. Let `fieldNameString` be the StringValue of _PrivateIdentifier_.
161+
2. Let `bv` be the \[\[ClassObject]] of GetThisEnvironment().
162+
3. Return ?[MakePrivateReference](https://tc39.es/proposal-class-fields/#sec-makeprivatereference)( `bv`, `fieldNameString` )
169163

170164
<!--#endregion:semantics-->
171165

@@ -253,7 +247,7 @@ This behavior provides the following benefits:
253247

254248
### Method Invocation
255249

256-
Invoking `class.x()` in a static method uses the current `this` as the receiver (similar to the behavior of `super.x()`):
250+
Invoking `class.x()` in a method, an initializer, or in the constructor uses the value of containing lexical class as the receiver:
257251

258252
```js
259253
class Base {
@@ -263,47 +257,24 @@ class Base {
263257
static g() {
264258
class.f();
265259
}
260+
h() {
261+
class.f();
262+
}
266263
}
267264
class Sub extends Base {
268265
}
269266

270267
Base.g(); // this: Base, class: Base
271268
Sub.g(); // this: Sub, class: Base
272269
Base.g.call({ name: "Other" }); // this: Other, class: Base
273-
```
274-
275-
This behavior provides the following benefits:
276270

277-
- Method invocation preserves the `this` receiver to allow for overriding static methods in a subclass.
278-
- Invocation behavior is similar to `super.x()`, so should be less surprising to users.
279-
280-
Invoking `class.x()` in a non-static method or the constructor uses the value of containing lexical class as the receiver:
281-
282-
```js
283-
class Base {
284-
static f() {
285-
console.log(`this.name: ${this.name}, class.name: ${class.name})`);
286-
}
287-
g() {
288-
class.f();
289-
}
290-
}
291-
class Sub extends Base {
292-
}
293-
294-
let b = new Base();
271+
let b = new Base();
295272
let s = new Sub();
296-
297-
b.g(); // this: Base, class: Base
298-
s.g(); // this: Base, class: Base
299-
Base.prototype.g.call({ name: "Other" }); // this: Base, class: Base
273+
b.h(); // this: Base, class: Base
274+
s.h(); // this: Sub, class: Base
275+
b.h.call({ name: "Other" }); // this: Other, class: Base
300276
```
301277

302-
This behavior provides the following benefits:
303-
304-
- Since instances will not have the lexical class constructor in their prototype hierarchy (other than through narrow corner cases),
305-
users would not expect the lexical `this` to be passed as the receiver from non-static methods. This behavior is the most
306-
intuitive and least-surprising behavior for users.
307278
<!--#endregion:examples-->
308279

309280
<!--#region:api-->
@@ -324,6 +295,7 @@ MemberExpression[Yield, Await] :
324295
ClassProperty[Yield, Await] :
325296
`class` `[` Expression[+In, ?Yield, ?Await] `]`
326297
`class` `.` IdentifierName
298+
`class` `.` PrivateIdentifier
327299
```
328300
<!--#endregion:grammar-->
329301

@@ -336,7 +308,7 @@ This proposal can easily align with the current class fields proposal, providing
336308
```js
337309
class Base {
338310
static counter = 0;
339-
id = class.counter++; // Assignment, so `Base` is used as `this`
311+
id = class.counter++; // `Base` is used as `this`
340312
}
341313

342314
class Sub extends Base {
@@ -348,35 +320,6 @@ console.log(Base.counter); // 2
348320
console.log(Sub.counter); // 2
349321
```
350322

351-
## Class Private Methods
352-
353-
This proposal can also align with the current proposals for class private methods, providing access without introducing
354-
TypeErrors due to incorrect `this` while preserving the ability for subclasses to override behavior:
355-
356-
```js
357-
class Base {
358-
static a() {
359-
console.log("Base.a()");
360-
class.#b();
361-
}
362-
static #b() {
363-
console.log("Base.#b()");
364-
this.c();
365-
}
366-
static c() {
367-
console.log("Base.c()");
368-
}
369-
}
370-
371-
class Sub extends Base {
372-
static c() {
373-
console.log("Sub.c()");
374-
}
375-
}
376-
377-
Base.a(); // Base.a()\nBase.#b()\nBase.c()
378-
Sub.a(); // Base.a()\nBase.#b()\nSub.c()
379-
```
380323

381324
## Class Private Fields
382325

@@ -399,6 +342,65 @@ console.log(Sub.increment()); // 1
399342
console.log(Base.increment()); // 2
400343
```
401344

345+
## Class Private Methods
346+
347+
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:
348+
349+
```js
350+
class Base {
351+
static #counter = 0;
352+
static #increment() {
353+
class.#counter++;
354+
this.printCounter();
355+
}
356+
static doIncrement() {
357+
class.#increment();
358+
}
359+
static printCounter() {
360+
console.log(class.#counter);
361+
}
362+
}
363+
class Sub extends Base {
364+
static printCounter() {
365+
console.log("Custom Counter");
366+
super.printCounter();
367+
}
368+
}
369+
370+
Base.doIncrement(); // prints: 1
371+
Sub.doIncrement(); // prints: 2
372+
```
373+
374+
In the example above, `Sub`'s overriden `printCounter` is never invoked. Such method calls would need to be rewritten:
375+
376+
```js
377+
// option 1:
378+
class Base {
379+
...
380+
static #increment() {
381+
...
382+
}
383+
static doIncrement() {
384+
class.#increment.call(this);
385+
}
386+
}
387+
388+
// option 2:
389+
class Base {
390+
...
391+
static #increment(C) {
392+
...
393+
C.printCounter();
394+
}
395+
...
396+
static doIncrement() {
397+
class.#increment(this);
398+
}
399+
}
400+
```
401+
402+
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.
403+
402404

403405
<!--#region:references-->
404406
<!--

0 commit comments

Comments
 (0)