Ranando, I share your reservations about private fields being bound too tightly to class syntax. In my case it isn’t because I don’t want to use classes, but rather because in the last few years, using the WeakMap solution, a good number of times I’ve needed to do things which the private field proposal either doesn’t permit or account for:
- Adding the same “slot” to multiple classes which don’t inherit from each other - Selectively sharing access to private state through functions declared outside the class body - Adding slots dynamically, e.g. when adding mix-in methods that may initialize a new slot if necessary when called, since subclassing is not always appropriate With the WeakMap solution, the privacy mechanism is one that already exists: a scope. This makes it very flexible (it handles the above three cases fine), but it has a key limitation in terms of achieving privacy, which is that `global.WeakMap` and `WeakMap.prototype` may be compromised. Given this limitation — plus the amount of boilerplate WeakMap privacy can entail — I am very happy to see private instance state being addressed syntactically. However because the model chosen for “scope of privacy” is “class declaration body” — not previously something that provided a closure/scope at all? — instead of just using existing scopes, I have found them impractical to use in some cases. If I’m understanding your alternative proposal, Ranando, I don’t think it addresses these issues either, not in the way I’m looking for anyway — I’m wishing for a syntactic solution for true private slots on objects, but where said slots are associated with a scope (almost always a module scope) rather than a class declaration. In particular, I’m not convinced that the concept of “protected” makes sense within the JS models of objects and dispatch. I’m gonna get more detailed about what I see as inadequacies in the current proposal. These are subjective, but not hypothetical: I’ve been doing WeakMap-based privacy for a few years now and I’ve tried converting existing code to use private fields since Chrome shipped it behind a flag. I found that, unfortunately, it did not meet my needs. --- Regarding exposing functions that operate on private state but which do not live on the constructor or prototype — there is a way to achieve this in the proposed spec. It’s awkward, but it is technically possible: ```js class Foo { #bar = 1; getBarOfFoo(foo) { return this.#bar; } // [[ ... other methods that may manipulate but do not expose #bar here ... ]] } const { getBarOfFoo } = Foo.prototype; delete Foo.prototype.getBarOfFoo; ``` It gets more awkward in the “multiple classes with the same semantic slot” case, since one will have to wrap each attempted access in a try-catch, as there is no other way to be certain whether the target has the slot. With WeakMap, in contrast, one will just get undefined — and one may use the same WeakMap to manage the same slot across multiple classes that are declared in the same scope as the WeakMap. Assume we have two classes with a private bar “slot” which is meant to be semantically equivalent. It holds an integer. We want to create a function that adds together two bar values from any classes that implement this slot. If an argument has no bar slot, bar defaults to zero. With WeakMaps, such a function might look like this: ```js function addBars(a, b) { return (wm.get(a).bar || 0) + (wm.get(b).bar || 0); } ``` Realizing the same logic with classes that use private field syntax is still possible (using the aforementioned “pop off a method” pattern), but now it looks like this: ```js function addBars(a, b) { let aBar, bBar; try { aBar = getBarOfFoo(a); } catch { try { aBar = getBarOfBaz(a); } catch { aBar = 0; } } try { bBar = getBarOfFoo(b); } catch { try { bBar = getBarOfBaz(b); } catch { bBar = 0; } } return aBar + bBar; } ``` ¯\_(ツ)_/¯ --- This is a more minor issue, but assuming we *can’t* have dynamic slots, I would like to take advantage of the fact that whether-a-function-may-access-a-slot is statically knowable by having immediate brand checking occur in all methods that may access private state. This is actually the main source of boilerplate in the WeakMap solution (for me, but admittedly I’m probably in a tiny minority here): ```js set foo(value) { if (!wm.has(this)) throw new TypeError(`Illegal invocation`); const str = String(value); if (VALID_FOO_VALUES.has(str)) { wm.get(this).foo = str; } else { throw new Error(`Invalid value for foo`); } } ``` The difference between the above function with and without the guard concerns guarantees about behavior. The `String(value)` call actually might throw, but it ought to be predictable that a method which requires a branded receiver always throws the same error when called on anything unbranded — even if (especially if!) private state access occurs in the method only conditionally, since throwing/not-throwing/what-gets-thrown makes an implementation detail observable. The above example is minimal, but there could be more involved state manipulation or observable effects that occur prior to the first private access, possibly leading to being left in an invalid state. Note that all host and intrinsic functions that may access slots perform these checks. It is the existing pattern in the language for this, and with a syntactic solution, it could be enforced automatically. Right now, with the existing proposal, the boilerplate still exists: ```js set foo(value) { try { this.#foo; } catch { throw new TypeError(`Illegal invocation`); } const str = String(value); if (VALID_FOO_VALUES.has(str)) { this.#foo = str; } else { throw new Error(`Invalid value for foo`); } } ``` (You could drop the try-catch if you don’t care whether the error thrown reveals implementation details, but if, like me, you are aiming for behavior matching host APIs, the boilerplate actually increases.) I can understand if these early checks are deemed undesirable, because they are strictly less flexible than the current proposed behavior, and they would also be incompatible with any solution that allows slots to be added dynamically (unlike the current proposal). However between this and the inability to manage privacy by scope instead of by class declaration body, I will probably find myself sticking with WeakMaps in general (in library code, anyway) because my attempted conversions have often increased rather than reduced complexity and verbosity. --- Sorry this is a long post. It’s hard to talk about this subject without getting pretty wordy, but hopefully this is useful feedback about what at least one dev is looking for with private slots. It seems, admittedly, that those of us who need private slots to remain "reflectable" are in the minority. FWIW I actually love `#` syntax though :)
_______________________________________________ es-discuss mailing list es-discuss@mozilla.org https://mail.mozilla.org/listinfo/es-discuss