-
Notifications
You must be signed in to change notification settings - Fork 19
Private fields are tied to classes #93
Comments
This proposal deliberately avoids making private fields a kind of property. That approach was attempted at TC39 in the past, but it leads to the question: If private fields are object properties, then how do Proxies view them? It didn't seem like there was any good answer to the question that preserved the properties that Proxies were intended to have. cc @erights |
@littledan Thanks, I get that. I guess what I was really trying to say was that making PrivateFieldEnvironment, PrivateFieldIdentifers and [[PrivateFieldValues]] usable by means other than in class declaration would make private fields more useful. This does not necessarily mean that existing reflection methods like But I think making a new universal object slot [[PrivateFieldValues]] exclusively for class declaration is very limiting. This does not seem to contradict the arguments made in this FAQ entry. |
Sorry for misunderstanding your message. I've been thinking about what sort of reflection mechanism we should have for private fields; one draft is in this file. However, I've been thinking about this mostly as it relates to decorators--not use outside of a class. I don't think it'd be great if people had to go around using imperative, semi-reflective machinery to use private fields outside of classes. It would be nicer if some sort of syntax just works. For example, we could make the Do you have another particular idea for how we should enable this feature? |
Sorry for being unclear, I see how my objections are vague. My prime motivation for creating this issue after reading the proposal was that adding new slots to objects and especially the execution context to be used only in classes (which are a derived concept themselves) left me uncomfortable since desire for hard encapsulation is not necessarily tied exclusively to classes or OOP. Another thing is the inability to define private properties dynamically/reflectively. PrivateFieldEnvironment looks like just like the usual lexical environment, except it is used for private identifiers. Very similarly to how lexical environment is used for common identifiers. Unfortunately I can't imagine a really-really nice way to extend this proposal in it's current form to allow general use of PrivateFieldEnvironment, but let me propose the best I got: function Point () {
private #x;
private #y;
const p = {
#x: 0,
#y: 0
};
p.#x; // 0
p.#y; // 0
return p;
} I think this is pretty self-explanatory (function declarations get a PrivateFieldEnvironment just like class declarations), but If there is any interest in this, I can try to formalize this. EDIT: The lack of reflection still bugs me, even though if you view private identifiers like another kind of lexical identifiers, the lack of reflection kind of makes sense. |
Interesting idea! Would the following be the desugaring into WeakMaps? function Point() {
const x = new WeakMap();
const y = new WeakMap();
const p = {};
x.set(p, 0);
y.set(p, 0);
x.get(p); // 0
y.get(p); // 0
return p;
} |
Yeap, that's right. |
Oops, no, actually, that's not quite right. The trick is that PrivateFieldEnvironment for classes is created when class definition is evaluated. It's better be the same for functions. The following desugaring gets this right. const Point = (function () {
const x = new WeakMap();
const y = new WeakMap();
return function Point() {
const p = {};
x.set(p, 0);
y.set(p, 0);
x.get(p); // 0
y.get(p); // 0
return p;
};
})(); But if we want to be really thorough we would replace |
@futpib Interesting idea. Do you think this proposal should have any changes to "future proof" for that as a follow-on? |
@littledan Is this too major of a change for the proposal at it's current stage? I think something like this should be included with the first landing of private fields (and should have been included from the get-go). If that's not an option, I guess it's future-proof enough for this to be included later. This fits the JavaScript's class-function duality so nicely >_< Click to expand large desugaring examplesclass Point {
#x;
#y;
constructor(x = 0, y = 0) {
#x = +x;
#y = +y;
}
get x() { return #x }
set x(value) { #x = +value }
get y() { return #y }
set y(value) { #y = +value }
equals(p) { return #x === p.#x && #y === p.#y }
toString() { return `Point<${ #x },${ #y }>` }
} After desurating class: const Point = function () {
private #x;
private #y;
function Point(x, y) {
this.#x = +x;
this.#y = +y;
}
Point.prototype.equals = function equals(p) {
return this.#x === p.#x && this.#y === p.#y;
};
Point.prototype.toString = function toString() {
return "Point<" + this.#x + "," + this.#y + ">";
};
Object.defineProperty(Point.prototype, "x", {
enumerable: true,
configurable: true,
get: function get() {
return this.#x;
},
set: function set(value) {
this.#x = +value;
}
});
Object.defineProperty(Point.prototype, "y", {
enumerable: true,
configurable: true,
get: function get() {
return this.#y;
},
set: function set(value) {
this.#y = +value;
}
});
return Point;
}(); After desugaring private identifiers: const Point = function () {
const x = new WeakMap();
const y = new WeakMap();
function Point(x, y) {
y.set(this, +y);
x.set(this, +x);
}
Point.prototype.equals = function equals(p) {
return x.get(this) === x.get(p) && y.get(this) === y.get(p);
};
Point.prototype.toString = function toString() {
return "Point<" + x.get(this) + "," + y.get(this) + ">";
};
Object.defineProperty(Point.prototype, "x", {
enumerable: true,
configurable: true,
get: function get() {
return x.get(this);
},
set: function set(value) {
x.set(this, +value);
}
});
Object.defineProperty(Point.prototype, "y", {
enumerable: true,
configurable: true,
get: function get() {
return y.get(this);
},
set: function set(value) {
y.set(this, +value);
}
});
return Point;
}(); |
Almost certainly. This would be a huge addition, and we've been trying to get class features in for a long time. I would strongly prefer not to add more new things to the proposal at this point. I want to point out a potential conflict with the current proposal: function Point () {
private #x;
const p = {
#x: 0
};
class Other {
#x = 1; // Is this the same '#x' as above?
}
} We previously considered (and rejected) declaring private fields in classes as |
@bakkot I thinks the "conflict" you pointed out is not an issue when private identifiers are lexically scoped. class A {
#x = 0;
constructor() {
class B {
#x = 0;
constructor() {
#x = 1; // lexically closest #x
}
}
}
} |
@futpib It's a conflict in the sense that it's not obvious that |
Hang on, @bakkot; it's not at Stage 3 yet. TC39 has been working on this problem for several years; I really want to see it shipped now too, but let's make sure to keep taking any useful feedback we get. Anyway, @futpib: I think the way you're resolving the conflict there makes sense--classes introduce a new scope for these identifiers, as does any block that contains "private #foo", and shadowing works lexically. It all makes sense to me, and seems like a consistent extension of the current proposal. This is actually pretty similar to some ideas that I remember seeing on the ECMAScript wiki, but I don't know where to find that; @allenwb do you remember this? It's really nice to have a story for object literals here, and it would actually be consistent with the destructuring proposed in a different issue. The only concerns I have would be:
A good repository to follow up on this further would be the unified class features proposal, which currently assumes that objects' curly brackets will be what sets off the new private name scope. |
Doesn't future proofing for this extension require using private #id;
let instances = 0;
export class A {
// declare a new private name with same name
#id = instances++;
}
export function getId(a) {
// refers to a different name than A's, so will never return an id.
return a.#id;
} I'd expect the above to work, though you might expect it to work even with a |
@bterlson, Would you expect the following example to work (apologies for the horrible names)? Maybe if you come with a Java-like intuition, and you expect such a reference to be disambiguated, you would. The current proposal, though, expects that users can understand what it means for a private name to be shadowed. Do you think if we introduced the keyword
|
@littledan I know this has been discussed already, but nevertheless, if |
@futpib We already have some other differences with classes compared to outside of classes. Outside of a class, you define a function by |
This is similar in some ways to an old proposal by @allenwb for name declaration (back from the private name days), with the difference that the names themselves are not reified. This deserves some thought. |
A precursor proposal to the current private fields proposal used the The "private name" proposal that @zenparsing mentioned is Syntactic Support for Private Names from 2012. Note that in this context, a Name is more or less what was eventually called a Symbol in ES 2015. It built upon an earlier Instance Variable proposal that dates to early 2011. So, none of these ideas are particularly new, the challenge all along has been building sufficient consensus around any particular private state scheme. Perhaps not surprisingly, I like some of the ideas in this thread as they address some defficienceis with both the current private fields proposal and plausible extensions to it. One issue is that strictly linking the lexical declaration of private field identifiers with the definition of private object field limits the utility of inner class definitions. For example while this is legal: class Outer {
#foo=42;
getFooAccessWrapper() {
const anOuter = this;
return new class {
get value() {return anOuter.#foo} //inner class can access outer class private
}
}
console.log(new Outer().getFooAccessWrapper().value); // 42 There is currently no way to allow an outer class to access an private field of an inner class: class Outer {
#helper = new class Helper {#foo};
exec(service) {
service(this.#helper); //services needs Helper instances
}
set foo(v) {this.#helper.#foo = v} //Syntax Error: #foo not visibly declared
} and trying to declare #foo in the outer class doesn't help: class Outer {
#foo; //create a #foo field to eliminate syntax error
#helper = new class Helper {#foo}; //note introduces a inner #foo
exec(service) {
service(this.#helper); //services needs Helper instances
}
set foo(v) {this.#helper.#foo = v} //Reference Error: helper instances don't have this #foo
} Adding private fields to object literals would have similar issues because, applying the principles of the current proposal, such private fields would be instance private: function fooFactory(value) {
return {
#value: value,
sameAs(aFoo) {return this.#value === aFoo.#value}
}
}
console.log(fooFactory(42).sameAs(fooFactory(42)));
//Reference Error: Each evaluation of obj lit creates a new distinct field idetity Adding a class Outer {
private #foo, #helper; //create new private field ids #foo and helper
#helper = new class Helper {#foo};
//Outer instances have a #helper field, Helper instances have a #foo field
exec(service) {
service(this.#helper); //services needs Helper instances
}
set foo(v) {this.#helper.#foo = v} //works! #outer can reference Helper's #foo
}
private #value;
function fooFactory(value) {
return {
#value: value, //defines a private field using the lexically visible #value binding
sameAs(aFoo) {return this.#value === aFoo.#value}
}
}
console.log(fooFactory(42).sameAs(fooFactory(42))); // true! Note that in the Outer class I used a In practice, I'm sure that for ergonomic reasons, for the most common cases people would still want to be able to make declarations like: class P {
//don't need to also say: private #x,#y;
#x;
#y;
constructor(x,y)
this.#x=x;
this.#y=y;
}
} So, I would define the semantics of class private field definition for #foo as using the A few final thoughts
private #x,#y;
//define functions that reference #x or #y and put them in a propertiesDescriptor object
let obj = Object.create({__proto__: desiredProto, #x, #y}, propertiesDescriptor); |
@allenwb, @littledan I'd still argue that we are better off with a mandatory The confusion outlined in this comment is a valid concern. Without mandatory class Outer {
#x = 0;
// imagine a lot of code here
labelFactory() {
return class Inner {
#x = 1; // is this a reference or a declaration?
}
}
} Similarly, without mandatory class Outer {
#x = 0; // imagine this line wasn't here before
// imagine a lot of code here
labelFactory() {
return class Inner {
// this was a declaration, but it became a reference
// when one introduced #x to the outer scope
#x = 1;
}
}
} You can see that with mandatory Another point is keeping syntax consistent. Currently one can introduce lexical identifiers only with keywords ( It seems that these arguments are valid even against proposal in it's current form (with private identifiers being exclusive to classes). |
@allenwb Thanks a lot for the historical links, Allen. It seems like we may be on a good path, if we're thinking along the lines of those proposals, but then avoiding the Proxy issue by not making private fields be properties. Your analysis seems to make sense to me; all together, sounds like you're suggesting this proposal first, and explicit private lexical declarations could fit in well as a follow-on proposal, and that it's reasonable to use different syntax for these (since they are doing very different things); am I understanding correctly? One small thing about @futpib What you're saying sounds like a downside of not having a token before the declaration in the class (or possibly of having the private access shorthand), rather than something that's specific to private fields in particular. For understanding whether a declaration is a declaration or assignment, the issue appears for public fields as well, such as |
@littledan It seems to me that if you wanted to expand the syntax with private name declarations in the future, then the Presumably, one of the primary use cases for this feature would be "friend" classes. private #x;
class A {
#x; // A has an "#x" field
}
class B {
readX(a) {
console.log(a.#x); // B has access to "#x";
}
} Note that in The semantics required for such a pattern would be:
|
Crazy idea (feel free to trounce it):
So this: class A {
private _x;
constructor() {
this._x = 1;
}
} is rewritten at compile-time to have the following semantics: class A {
private #x;
constructor() {
this.#x = 1;
}
} The idea is that with private declarations, the user opts-in to private lookup semantics for that identifier name only. Advantages:
Disadvantages:
|
This has the (IMO terrible) consequence that if this same code operates on an external object that just happens to use the same name as a public property name, then the obvious When people express the preference you're reacting to, their preference does not take into account this cost. |
@erights Yes, that problem was what eventually cause all previous non-sigil based approached to be abandoned. Statically typed languages use type declarations to distinguish the external-public/internal-private cases with out the need of a sigil. But a dynamically typed language doesn't have enough static information to differentiate those cases. |
@zenparsing that one is in the FAQ, even! |
@bakkot It might be worth pulling in Allen's comment about how statically typed languages disambiguate here; I think this is pretty critical to the point. |
BTW this comment makes me especially convinced about the hazard. Not sure what exactly we should do right now. |
@littledan Do you think changing the private field declaration in classes between I think this idea actually works well with the current proposal except for the above-noted conflict, and that conflict (it seems to me) is resolved very nicely by requiring |
I added a refinement my (half-serious) proposal above, requiring that names start with an underscore. This reduces the hazard you point out (though it certainly doesn't eliminate it). |
@zenparsing which raises the question: why is (an unreliable) "_" better than (a reliable) "#" ? |
@erights Consider it a Plan B for the possibility that there are no available characters to choose that are both reliable and palatable. |
"#" is only significantly less palatable than "_" until one gets used to it. (Though I admit I'll probably never like it.) Unreliable is hugely less palatable than reliable. The generalized private discussed in this thread sure seems to harken back to the old "relationship" ideas and your ( @zenparsing 's) old generalized access notation. Rather than introduce a new kind of lexical namespace, we could instead introduce a the following new bits of syntax:
For suitable choice of keyword that means hoist out one level: class Foo ... {
keyword decl2 name = initExpr;
...
} expands to const Foo = (function(){"use strict";
decl2 name = initExpr;
return class Foo ... {
...
};
})(); or, more generally decl1 Foo ... {
keyword decl2 name = initExpr;
...
} expands to const Foo = (function(){"use strict";
decl2 name = initExpr;
return decl1 Foo ... {
...
};
})(); where decl1 may be at least "class" or "function" and decl2 may be at least "let", "const", (or with obvious local changes) "function", or "class". I am not saying that we should necessarily go there. But if we are considering the generalizations in this thread, then why not go all the way back to this? |
@bakkot We need to work the I'm not convinced that If we're talking about reliability: You could see |
@erights One step further and we arrive at "operator overloading with magic methods" 😄. This proposal is still about private fields, I think that's the reason we don't go that general. This issue is about making private fields usable outside of classes. That's it, and it's not even that huge of a generalization. Already in this proposal:
This issue proposes:
The mandatory |
@allenwb I'm mixed on your point about not allowing Now this would be doable as is without exporting them as you could always have: private #state;
export default class SomeClass {
#state = {};
}
export function _readState(someClass) {
return someClass.#state
} But this is just as bad as allowing Ultimately it's always going to be possible to break private state if people want to spread things over multiple files. e.g. I can always do this: class SomeClass {
private #state
__breakState() {
return this.#state
}
} What developers should really do instead is make sure that the exposed entry point to the application doesn't expose those private states e.g. // privateImplementation.js
private #initializer
class Task {
constructor(initializer) {
this.#initializer = initializer
}
...
}
export default Task
export #initializer
// someOperator.js
import Task, { #initializer } from "./privateImplementation.js"
function delay(task, time) {
return new Task((resolve, reject) =>
setTimeout(_ => task.#initializer(resolve, reject), time)
)
}
// publicTask.js
export { default } from ".../privateImplementation.js"
// someConsumer.js
import Task from ".../publicTask.js"
import delay from ".../operators/delay.js" |
@erights I still like that old proposal! The only issue is that we needed to distinguish between "creating" the field and assigning to the field, and the |
Thanks!
No positive ideas yet, but a possible direction. Since this proposal would separate the (hoisted) declaration of the WeakMap-ish from the initialization of per-instance state, we could again consider moving the per-instance-state initialization back inside the constructor, where it can see the constructor arguments. |
One issue with this is that it conflicts with some requirements @wycats has been advocating for about the syntax of decorated private fields. The hope is that you can use something like this for a field which has a private field and a generated reader: class C {
@reader #x;
} Here, the claim is that it would be awkward to insert a keyword such as However, such logic would not explain why |
Decorators are the new Proxy: "But have you thought about how this interacts with decorators?" 😄 |
I don't think adding class C {
// A reader for the private field x
@reader private #x
} The fact that it generates a public field is just a detail of I would personally expect this to work though if the class C {
// Declare the field
private #x
// use it in a decorator
@reader #x
} |
OK, it'd be good to get more context from @wycats here on why exactly |
Since class is syntactic sugar for function and "special"
"prototype"
property (at least class constructors are defined in terms of function and "prototype"), I feel like it's better to tie private fields to those constructs. In other words, one should be able to achieve exactly same results with function and prototype as with a class.Currently this proposal adds PrivateFieldEnvironment to Execution Contexts and the only defined way to create new PrivateFieldEnvironment is ClassDefinitionEvaluation (that is evaluation of a
class ...
expression).This will result in that whenever user wants to create on object with truly private fields, they will have to use a dummy
class
declaration with all it's limitation (like inability to declare arbitrary class members in runtime in reflective manner (withouteval
-like features)).I propose that at least
Object.create
EDIT: The
Object.create
part has been revised as unnecessary later in this thread.The text was updated successfully, but these errors were encountered: