Skip to content
This repository was archived by the owner on Jan 25, 2022. It is now read-only.

Commit 2cd1e68

Browse files
committed
Add more code samples to clarify explanations
1 parent 3165c97 commit 2cd1e68

File tree

1 file changed

+215
-22
lines changed

1 file changed

+215
-22
lines changed

README.md

Lines changed: 215 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ The current proposal is similar to the previous semantics, with one modification
1111
1. **Private static methods can be called with the superclass or any subclasses as the receiver; otherwise, TypeError** (NEW)
1212
1. **Private static fields can only be referenced with the class as the receiver; TypeError when used with anything else**. [see below](https://github.com/tc39/proposal-static-class-features/blob/master/README.md#static-private-access-on-subclasses)
1313

14+
Note that private fields and methods are only accessible within the class body where they are defined; the new change here only pertains to what happens when methods in that superclass body is invoked with a receiver which is a subclass constructor.
15+
1416
## Static public fields
1517

1618
Like static public methods, static public fields take a common idiom which was possible to write without class syntax and make it more ergonomic, have more declarative-feeling syntax (although the semantics are quite imperative), and allow free ordering with other class elements.
@@ -48,6 +50,41 @@ Kevin Gibbons [raised a concern](https://github.com/tc39/proposal-class-fields/i
4850
- Lots of current educational materials, e.g., by Kyle Simpson and Eric Elliott, explain directly how prototypical inheritance of data properties in JS works to newer programmers.
4951
- This proposal is more conservative and going with the grain of JS by not adding a new time when code runs for subclassing, preserving identities as you'd expect, etc.
5052

53+
### Semantics in an edge case with Set and inheritance
54+
55+
Example of how these semantics work out with subclassing (this is not a recommended use of constructors as stateful objects, but it shows the semantic edge cases):
56+
57+
```js
58+
static Counter {
59+
static count = 0;
60+
static inc() { this.count++; }
61+
}
62+
class SubCounter extends Counter { }
63+
64+
Counter.hasOwnProperty("count"); // true
65+
SubCounter.hasOwnProperty("count"); // false
66+
67+
Counter.count; // 0, own property
68+
SubCounter.count; // 0, inherited
69+
70+
Counter.inc(); // undefined
71+
Counter.count; // 1, own property
72+
SubCounter.count; // 1, inherited
73+
74+
// ++ will read up the prototype chain and write an own property
75+
SubCounter.inc();
76+
77+
Counter.hasOwnProperty("count"); // true
78+
SubCounter.hasOwnProperty("count"); // true
79+
80+
Counter.count; // 1, own property
81+
SubCounter.count; // 2, own property
82+
83+
Counter.inc(); Counter.inc();
84+
Counter.count; // 3, own property
85+
SubCounter.count; // 2, own property
86+
```
87+
5188
See [Initializing fields on subclasses](#initializing-fields-on-subclasses) for more details on an alternative proposed semantics and why it was not selected.
5289

5390
## Static private methods and accessors
@@ -75,21 +112,20 @@ export class JSDOM {
75112
}
76113

77114
async static fromURL(url, options = {}) {
78-
normalizeFromURLOptions(options);
79-
normalizeOptions(options);
115+
url = normalizeFromURLOptions(url, options);
80116

81117
const body = await getBodyFromURL(url);
82-
return JSDOM.#finalizeFactoryCreated(new JSDOM(body, options), "fromURL");
118+
return JSDOM.#finalizeFactoryCreated(body, options, "fromURL");
83119
}
84120

85-
static fromFile(filename, options = {}) {
86-
normalizeOptions(options);
87-
121+
static async fromFile(filename, options = {}) {
88122
const body = await getBodyFromFilename(filename);
89-
return JSDOM.#finalizeFactoryCreated(new JSDOM(body, options), "fromFile");
123+
return JSDOM.#finalizeFactoryCreated(body, options, "fromFile");
90124
}
91125

92-
static #finalizeFactoryCreated(jsdom, factoryName) {
126+
static #finalizeFactoryCreated(body, options, factoryName) {
127+
normalizeOptions(options);
128+
let jsdom = new JSDOM(body, options):
93129
jsdom.#createdBy = factoryName;
94130
jsdom.#registerWithRegistry(registry);
95131
return jsdom;
@@ -99,6 +135,38 @@ export class JSDOM {
99135

100136
In [Issue #1](https://github.com/tc39/proposal-static-class-features/issues/1), there is further discussion about whether this feature is well-motivated. In particular, static private methods can typically be replaced by either lexically scoped function declarations outside the class declaration, or by private instance methods. However, the current proposal is to include them, due to the use cases in [#4](https://github.com/tc39/proposal-static-class-features/issues/4).
101137

138+
### Subclassing use case
139+
140+
As described above, static private methods may be invoked with subclass instances as the receiver. Note that this does *not* mean that subclass bodies may call private methods from superclasses--the methods are private, not protected, and it would be a SyntaxError to call the method from a place where it's not syntactically present.
141+
142+
In the below example, a public static method `from` is refactored, using a private static method. This private static method has an extension point which can be overridden by subclasses, which is that it calls the `of` method. All of this is in service of factory functions for creating new instances. If the private method `#from` were only callable with `MyArray` as the receiver, a `TypeError` would result.
143+
144+
```js
145+
class MyArray {
146+
static #from(obj) {
147+
this.of.apply(...obj);
148+
}
149+
static from(obj) {
150+
// This function gets large and complex, with parts shared with
151+
// other static methods, so an inner portion #from is factored out
152+
return this.#from(obj);
153+
}
154+
}
155+
156+
class SubMyArray extends MyArray {
157+
static of(...args) {
158+
let obj = new SubMyArray();
159+
let i = 0;
160+
for (let arg of args) {
161+
obj[i] = arg;
162+
i++;
163+
}
164+
}
165+
}
166+
167+
let subarr = MySubArray.from([1, 2, 3]);
168+
```
169+
102170
## Static private fields
103171

104172
### Semantics
@@ -107,16 +175,6 @@ Unlike static private methods, static private fields are private fields just of
107175

108176
As with static public fields, the initializer is evaluated in a scope where the binding of the class is available--unlike in computed property names, the class can be referred to from inside initializers without leading to a ReferenceError. As described in [Why only initialize static fields once](#why-only-initialize-static-fields-once), the initializer is evaluated only once.
109177

110-
Only non-writable ones are copied because only these have behavior which really matches what you'd see in ordinary prototype chain access. The semantics of ordinary properties are odd and probably not worthy of replicating. As some background: an ordinary writable property, writes higher up in the prototype chain are reflected in reads further down; on the other hand, a write further down on the prototype chain creates a new own property. This can be seen in the following example:
111-
112-
```js
113-
let x = { a: 1 };
114-
let y = { __proto__: x };
115-
y.a++;
116-
print(x.a); // 1
117-
print(y.a); // 2
118-
```
119-
120178
### Use case
121179

122180
```js
@@ -140,9 +198,34 @@ class ColorFinder {
140198

141199
In [Issue #1](https://github.com/tc39/proposal-static-class-features/issues/1), there is further discussion about whether this feature is well-motivated. In particular, static private fields can typically be subsumed by lexically scoped variables outside the class declaration. Static private fields are motivated mostly by a desire to allow free ordering of things inside classes and consistency with other class features. It's also possible that some initialzer expressions may want to refer to private names declared inside the class, e.g., in [this gist](https://gist.github.com/littledan/19c09a09d2afe7558cdfd6fdae18f956).
142200

143-
### Why this TypeError is not so bad
201+
The behavior of private static methods have, in copying to subclass constructors, does not apply to private static fields. This is due to the complexity and lack of a good option for semantics which are analogous to static public fields, as explained below.
144202

145-
Justin Ridgewell [raised a concern](https://github.com/tc39/proposal-class-fields/issues/43) that static fields and methods will lead to a TypeError when `this` is used as the receiver from within a static method, and they are invoked from a subclass. This concern is hoped to be not too serious because:
203+
### TypeError case
204+
205+
Justin Ridgewell [expressed concern](https://github.com/tc39/proposal-class-fields/issues/43) about the TypeError that results from static private field access from subclasses. Here's an example of that TypeError, which occurs when code ported from the above static public fields example is switched to private fields:
206+
207+
```js
208+
static Counter {
209+
static #count = 0;
210+
static inc() { this.#count++; }
211+
static get count() { return this.#count; }
212+
}
213+
class SubCounter extends Counter { }
214+
215+
Counter.inc(); // undefined
216+
Counter.count; // 1
217+
218+
SubCounter.inc(); // TypeError
219+
220+
Counter.count; // 1
221+
SubCounter.count; // TypeError
222+
```
223+
224+
A TypeError is used here because no acceptable alternative would have the semantics which are analogous to static public fields. Some alternatives are discussed below.
225+
226+
#### Why this TypeError is not so bad
227+
228+
The above concern is hoped to be not too serious because:
146229
- Programmers can avoid the issue by instead writing `ClassName.#field`. This phrasing should be easier to understand, anyway--no need to worry about what `this` refers to.
147230
- It is not so bad to repeat the class name when accessing a private static field. When implementing a recursive function, the name of the function needs to be repeated; this case is similar.
148231
- It is statically known whether a private name refers to a static or instance-related class field. Therefore, implementations should be able to make helpful error messages for instance issues that say "TypeError: The private field #foo is only present on instances of ClassName, but it was accessed on an object which was not an instance", or, "TypeError: The static private method #bar is only present on the class ClassName; but it was accessed on a subclass or other object", etc.
@@ -188,19 +271,94 @@ If we want to get static private fields to be as close as possible to static pub
188271

189272
This alternative is currently not selected because it would be pretty complicated, and lead to a complicated mental model. It would still not make public and private static fields completely parallel, as writes from subclasses are not allowed.
190273

191-
The following code shows the distinction between this alternate and the main proposal
274+
Here's an example of code which would be enabled by this alternative (based on code by Justin Ridgewell):
192275

193276
```js
277+
class Base {
278+
static #field = 'hello';
279+
280+
static get() {
281+
return this.#field;
282+
}
283+
284+
static set(value) {
285+
return this.#field = value;
286+
}
287+
}
288+
289+
class Sub extends Base {}
290+
291+
Base.get(); // => 'hello'
292+
Base.set('xyz');
293+
294+
Sub.get(); // => 'xyz' in this alternative, TypeError in the main proposal
295+
Sub.set('abc'); // TypeError
194296
```
195297

196298
### Restricting static private field access to `static.#foo`
197299

198300
Jordan Harband [suggested](https://github.com/tc39/proposal-class-fields/issues/43#issuecomment-328874041) that we make `static.` a syntax for referring to a property of the immediately enclosing class. If we add this feature, we could say that private static fields and methods may *only* be accessed that way. This has the disadvantage that, in the case of nested classes, there is no way to access the outer class's private static methods. However, as a mitigation, programmers may copy that method into another local variable before entering into another nested class, making it still available.
199301

302+
With this alternative, the above code sample could be written as follows:
303+
304+
```js
305+
class Base {
306+
static #field = 'hello';
307+
308+
static get() {
309+
return static.#field;
310+
}
311+
312+
static set(value) {
313+
return static.#field = value;
314+
}
315+
}
316+
317+
class Sub extends Base {}
318+
319+
Base.get(); // => 'hello'
320+
Base.set('xyz');
321+
322+
Sub.get(); // => 'xyz'
323+
Sub.set('abc');
324+
325+
Base.set(); // 'abc'
326+
```
327+
328+
Here, `static` refers to the base class, not the subclass, so issues about access on subclass instances do not occur.
329+
200330
### Restricting static field private access to `ClassName.#foo`
201331

202332
The syntax for accessing private static fields and methods would be restricted to using the class name textually as the receiver. This would make it a SyntaxError to use `this.#privateMethod()` within a static method, for example. However, this would be a somewhat new kind of way to use scopes for early errors. Unlike var/let conflict early errors, this is much more speculative--the class name might actually be shadowed locally, which the early error would not catch, leading to a TypeError. In the championed semantics, such checks would be expected to be part of a linter or type system instead.
203333

334+
With this alternative, the above code sample could be written as follows:
335+
336+
```js
337+
class Base {
338+
static #field = 'hello';
339+
340+
static get() {
341+
return Base.#field;
342+
}
343+
344+
static set(value) {
345+
return Base.#field = value;
346+
}
347+
}
348+
349+
class Sub extends Base {}
350+
351+
Base.get(); // => 'hello'
352+
Base.set('xyz');
353+
354+
Sub.get(); // => 'xyz'
355+
Sub.set('abc');
356+
357+
Base.set(); // 'abc'
358+
```
359+
360+
Here, explicit references the base class, not the subclass, so issues about access on subclass instances do not occur. A reference like `this.#field` would be an early error, helping to avoid errors by programmers.
361+
204362
## Alternate proposals not selected
205363

206364
Several alternatives have been discussed within TC39. This repository is not pursuing these ideas further. Some may be feasible, but are not selected by the champion for reasons described below.
@@ -209,16 +367,51 @@ Several alternatives have been discussed within TC39. This repository is not pur
209367

210368
Kevin Gibbons has proposed that class fields have their initialisers re-run on subclasses. This would address the static private subclassing issue by adding those to subclasses as well, leading to no TypeError on use.
211369

370+
With this alternate, the initial counter example would have the following semantics:
371+
372+
```js
373+
static Counter {
374+
static count = 0;
375+
static inc() { this.count++; }
376+
}
377+
class SubCounter extends Counter { }
378+
379+
Counter.hasOwnProperty("count"); // true
380+
SubCounter.hasOwnProperty("count"); // true
381+
382+
Counter.count; // 0, own property
383+
SubCounter.count; // 0, own property
384+
385+
Counter.inc(); // undefined
386+
Counter.count; // 1, own property
387+
SubCounter.count; // 0, own property
388+
389+
// ++ is just dealing iwth own properties the whole time
390+
SubCounter.inc();
391+
392+
Counter.hasOwnProperty("count"); // true
393+
SubCounter.hasOwnProperty("count"); // true
394+
395+
Counter.count; // 1, own property
396+
SubCounter.count; // 1, own property
397+
398+
Counter.inc(); Counter.inc();
399+
Counter.count; // 3, own property
400+
SubCounter.count; // 1, own property
401+
```
402+
212403
However, this proposal has certain disadvantages:
213404
- Subclassing in JS has always been "declarative" so far, not actually executing anything from the superclass. It's really not clear this is the kind of hook we want to add to suddenly execute code here.
214405
- The use cases that have been presented so far for expecting the reinitialization semantics seem to use subclassing as a sort of way to create a new stateful class (e.g., with its own cache or counter, or copy of some other object). These could be accomplished with a factory function which returns a class, without requiring that this is how static fields work in general for cases that are not asking for this behavior.
215406

216407
### Prototype chain walk for private fields and methods
217408

218-
Ron Buckton [has proposed](https://github.com/tc39/proposal-private-methods/issues/18) that private field and method access could reach up the prototype chain. There are two ways that this could be specified, both of which have significant issues:
409+
Ron Buckton [has](https://github.com/tc39/proposal-class-fields/issues/43#issuecomment-348045445) [proposed](https://github.com/tc39/proposal-private-methods/issues/18) that private field and method access could reach up the prototype chain. There are two ways that this could be specified, both of which have significant issues:
219410
- **Go up the normal prototype chain with \[\[GetPrototypeOf]]**. This would be observable and interceptible by proxies, violating a design goal of private fields that they not be manipulated that way.
220411
- **Use a separate, parallel, immutable prototype chain**. This alternative would add extra complexity, and break the way that other use of classes consistently works together with runtime mutations in the prototype chain.
221412

413+
A positive aspect of Ron's proposal is that it preserves identical behavior with respect to subclassing as public static fields.
414+
222415
### Lexically scoped variables and function declarations in classes
223416

224417
Allen Wirfs-Brock has proposed lexically scoped functions and variables within class bodies. The syntax that Allen proposed was an ordinary function declaration or let/const declaration in the top level of a class body:

0 commit comments

Comments
 (0)