Closed shannon closed 6 years ago
What would the [[Prototype]] be for this object? Would i be able to pass it around and expose private values to mutation later, perhaps by accident as the receiver with this.#.foo()
? Would i be able to install getters and setters on this.#
?
See this recent thread on computed property access and iteration.
Re: the syntax for declaration, I am strongly opposed to any proposal which has private x
as the declaration and does not have this.x
for access for the reasons given in the FAQ.
There's some other issues with this suggestion which I can go into detail about if you'd like, but these are some of the major points.
@ljharb
What would the prototype be for this object? Would i be able to pass it around and expose private values to mutation later, perhaps by accident as the receiver with this.#.foo()? Would i be able to install getters and setters on this.#?
Sure just as you could break encapsulation now by accidentally passing any enclosed object around. Accidentally passing this.#
seems pretty unlikely to me. Is there some reason that I'm not seeing as to why we wouldn't want to allow getters and setters?
@bakkot
See this recent thread on computed property access and iteration.
I've read that thread, I'm not sure I understand completely the opposition in the context of my suggestion. this.#
should only be able to be referenced from within the class methods. If the suggestion in that thread is to place the private properties into a private map, how is that any different than my suggestion?
As you can see from my examples in the description, there are very common use cases for being able to reference a private property from a variable. If this is prohibited then it's going to lead to a lot of ugly and redundant code.
Re: the syntax for declaration, I am strongly opposed to any proposal which has private x as the declaration and does not have this.x for access for the reasons given in the FAQ.
I've read the FAQ but it doesn't really give a convincing argument. The same could be said accidentally using this.x
instead of this.#x
. As many times as it's been said throughout these threads that other languages shouldn't be used as a measurement for this proposal I have to say assuming this.x access of a private field because other languages do fails under the same logic.
@ljharb Not sure why you would but if you want to prevent getters and setters from being added later you can use Object.seal
.
(function(){
const __private__ = new Weakmap();
return class {
constructor(){
__private__.set(this, Object.seal({
x: 'x'
}));
}
}
})();
@shannon
If the suggestion in that thread is to place the private properties into a private map, how is that any different than my suggestion?
The suggestion is that you should use a private property with a Map if you for some reason need a private map, not that you should always do this. I would prefer to encourage the mental model that private properties are record-like.
As you can see from my examples in the description, there are very common use cases for being able to reference a private property from a variable.
I do not share your intuition that these are very common uses cases for private fields.
If this is prohibited then it's going to lead to a lot of ugly and redundant code.
I don't share this intuition either - I don't think putting an object on a private field is all that ugly, and it seems to me it would satisfy your use cases without particular redundancy. In fact, if you had #_ = { x: 'x', y: 'y' }
in the class body under the current proposal, you could use this.#_.x
or this.#_['y']
for access to get almost identical syntax to what you're asking for without requiring reifying and reasoning about an actual object for the more common record-like use case.
I've read the FAQ but it doesn't really give a convincing argument. The same could be said accidentally using
this.x
instead ofthis.#x
. As many times as it's been said throughout these threads that other languages shouldn't be used as a measurement for this proposal I have to say assumingthis.x
access of a private field because other languages do fails under the same logic.
I'm sorry you don't find the argument convincing. I do. I am less concerned (not unconcerned, but less) about this.x
vs this.#x
because of the symmetry between declaration and use, which is shared by public fields: class C { x = 0; #y = 1; m(){ return this.x + this.#y; } }
is very consistent, to my eye.
Also, we do care what happens in other languages and would prefer to avoid misleading people with a background elsewhere, especially in cases where there is very similar syntax which has significantly different semantics in another language. We don't necessarily want to replicate other languages identically (and often are not in a position to in any case), but that doesn't mean we feel free to totally ignore them.
@bakkot
I would prefer to encourage the mental model that private properties are record-like.
If we want a record-like syntax than it shouldn't be accessed via this
at all (i.e. this.#x
or this.x
or this.#.x
). It should just be blocked scoped and accessed via bare x. A mental model that all javascript developers are familiar with. Yes, I have read the FAQ on bare x but it seems to me that we've decided on trying to pick and choose from two different paradigms and ended up with something that is pretty inconsistent with the rest of javascript.
In fact, if you had # = { x: 'x', y: 'y' } in the class body under the current proposal, you could use this.#.x or this.#_['y'] for access to get almost identical syntax to what you're asking for without requiring reifying and reasoning about an actual object for the more common record-like use case.
Having to do #_ = {...}
is not exactly pretty. But more importantly without a proposal implementation to sort out context for private methods it becomes really messy really quick.
class {
x = 'x';
#y = 'y';
#_ = {
method () {
//how do I even reference x and y?
}
}
}
vs
class {
x = 'x';
private y = 'y';
private method () {
return this.x + this.#.y;
}
}
Being sugar for something like:
(function(){
const __private__ = new Weakmap();
const __methods__ = {
method: function() {
return this.x + __private__.get(this).y;
}
}
return class {
constructor(){
__private__.set(this, {
y: 'y',
method: __methods__.method.bind(this)
});
}
}
})();
@bakkot it seems your biggest complaints against this proposal are this:
I personally think that (with some small tweaks) this proposes a much less complicated, easier to understand implementation of privates, without really deviating too far from what's already in the main proposal.
This would essentially be the same as the current implementation, except with only one private member, named simply #
, as opposed to infinitely many prefixed with #
.
To address concern # 1, the following would provide consistent declaration and access:
class Foo {
x = "I'm pubic"
# = {
y: "I'm private",
method = () => this.#.x + this.y;
}
}
This to me seems much easier for the uninitiated to comprehend. Rather than explain "Oh yeah, well a member prefixed by # means that the member is private. Except that really, it's not a member at all anymore. You can write a new member at any point, but privates all have to be declared up front in the class body. Also the name isn't #foo
, it's actually just foo
, but you have to use the #
sigil to differentiate between public and private members. They're also not dynamically accessible, or iterable...."
You get the point. It's a drastic break from what we're used to with js classes (or js anything, for that matter), and takes some effort to explain.
With this, the explanation is a lot simpler. "this.#
is the private object. Put anything you want to keep private in there.". It intuitively makes sense. Sure, there's this one special property that behaves different, but it's just one, and it gets to be special because it's the private object. And you can write whatever you want into it, iterate it's properties, etc.
(This has another nice parallel to how react state is usually declared and used, just to put that out there.. state = { foo: 1 }
, method = () => this.state.foo + 3
)
For concern # 2, I don't know that this is any more potentially dangerous than what is already possible. Sure, you could accidentally leak some information, but any potential for leak would also be present in the current implementation, via this.#_ = {}
.
As for # 3, I really don't think correct to say that privates should only be record-like, or even to encourage users to only think of privates as record-like. Sure, the use cases for dynamic access and iterability are not the most common cases, but they certainly aren't non-existent. I don't see any value to treating private values as record-only. To the contrary; I think that having them be so, when the reasons why they are are so technical and non-obvious, creates a very strange limitation that breaks the user's intuition about how the language should work. When a user asks "why can't I iterate over private properties?" or "why can't I access this dynamically?" the discussion that will follow will be lengthy, and a tad unconvincing. The sheer number issues (and comments) coming up in this repo to the tune of "I read all the FAQ, and much of the conversation, but I still think it would be better to do ___" should be proof enough of that. Despite there being logical explanations for the choices made thus far, the community at large finds the reasoning dissatisfying. There's a general air of "I get it, but that doesn't mean I have to like it."
As such, I feel like it's much easier to swallow having only one private member (an object named #
). Just one exception to the rule of property access, as opposed to a giant bag of arbitrarily named exceptions. You don't ever have to worry about what types of access are available for something private. With this.#foo
, it almost feels like this
is two separate objects, from different languages entirely. You put the funky sigil in there and you're whisked away to a foreign land where property access is completely different than everything you know.
But with this.#.foo
(or perhaps even #.foo
, why even need the this
at this point?) you're just accessing a property of a plain old javascript object. It's comfortable and familiar. It behaves the way you expect it to (the way all javascript objects behave).
I apologize if any of this comes off as combative or stubborn. I don't really have any major technical arguments against the proposal as-is, other than the aforementioned feeling of "I get it, but that doesn't mean I have to like it". For better or for worse, whatever design is eventually accepted into javascript, we will be stuck with forever. I just want to make sure that we truly exhaust all our options before settling on a design. It would be a shame if a feature as highly desired as private data finally arrived, but was generally found to be unpleasant to work with, and confusing to new users. Before committing to any one design, we need to try to be absolutely sure that there truly is no better option.
@EthanRutherford I'm ok with this adjustment. Looks clean and simple to me. I also prefer just #.x
. The only ambiguous part is the arrow functions. I'm not entirely sure how they behave with class properties to begin with.
I would have written it like this:
class Foo {
x = "I'm pubic";
# = {
y: "I'm private",
method() { return #.y + this.x; }
}
}
How will this behave with the proposal as it is?
class Foo {
x = "I'm pubic";
y = () => { console.log(this.x); }; //what is the context of this here?
}
*Edited for clarity
That one is simple. Arrow functions are lexically scoped, so the this
of method y
in your post would be the instance of the class.
class Foo {
bar() {
const test = () => this;
console.log(test());
}
baz() {
const test = {
me: () => this,
};
console.log(test.me());
}
}
new Foo().bar(); // output: Foo {}
new Foo().baz(); // output: Foo {}
Just as in the functions above, arrow functions declared this way would have a this
lexically bound to the class instance. So, the following methods would both work intuitively.
class {
x = "I'm public";
y = () => this.x;
# = {
z: () => this.x,
}
}
This proposal is very interesting, though I share many of @bakkot's concerns. It seems to me like it could be added on top of the existing proposal as a follow on, driven by eexperimentation in transpilers. What do you think, @Shannon?
@littledan I would agree with that. It almost feel like a special case for a blank '#' property. It removes the need to use #_
and if it handles the context within methods that would solve all my requirements.
I prefer the syntax
class Foo {
# = {
methodX() { ... }
async methodY () { ... }
}
}
over
class Foo {
# = {
methodX: () => { ... },
methodY: async () => { ... }
}
}
But if it's easier because of the lexical scoping as @EthanRutherford described then I would be ok with it.
@EthanRutherford, I don't think your design would work the way you wanted to, unless it actually did mean something very different from what it appears to.
In particular, I think that in
class C {
# = {
getThis() {
return this;
}
};
receiverIsPrivateObject() {
return this.#.getThis() === this.#;
}
receiverIsObject() {
return this.#.getThis() === this;
}
}
(new C).receiverIsPrivateObject();
(new C).receiverIsObject();
it must be the case that receiverIsPrivateObject
holds and receiverIsObject
does not.
There's a few reasons for this. The most important is that this is how it would work if you used a public field (e.g. if you s/#/_/g
above), and if it works differently for #
, then it is not just a regular object: it's this weird other thing that looks exactly like an object but has different semantics. Less significantly, but also important, if methods in the object came pre-bound it would mean they couldn't be rebound. Plus it creates a weird asymmetry between # = { m(){} }
and # = { m: function(){} }
.
So I don't think you can really get the semantics you want, here.
But that means that it's pretty awkward to use: you can't readily refer to a public field from the private object, etc.
I really do not think that reifying an actual object distinct from the instance where private state lives is a good design. I think it's a lot nicer all around to have private state live on the instance, just as it does in other languages.
This to me seems much easier for the uninitiated to comprehend. Rather than explain "Oh yeah, well a member prefixed by
#
means that the member is private. Except that really, it's not a member at all anymore. You can write a new member at any point, but privates all have to be declared up front in the class body. Also the name isn't#foo
, it's actually justfoo
, but you have to use the#
sigil to differentiate between public and private members. They're also not dynamically accessible, or iterable...."
See, this hasn't been my experience at all. I explain to people "to declare a private field, begin its name with #
". Sometimes I add that it's like how they might previously have begun its name with _
to suggest privacy, except the language will enforce it. And that's pretty much the whole explanation! People get it just fine. Yes, there's difference about iterability and so on, but these really don't come up that much and tend to be covered pretty well by "oh, right, it's private". I think the whole "private object" thing would be a lot more confusing.
As an aside, I'm not sure what you mean by "also the name isn't #foo
, it's actually just foo
". I would encourage thinking of the #
as part of the name; this comment is certainly not something I would include in my explanation. The only way the language has any notion of what the name of a field is apart from how it's accessed is by function name inference, but #f = function(){}
will create a function with .name
"#f"
.
I think you misunderstand me. No, I don't intend for this object to have any exotic behavior: it will behave exactly the same as an object assigned to #_
under the current proposal. And I entirely disagree: it is not particularly awkward to access other fields from the object. Use an arrow function.
class {
x = 5;
# = {
y: 10,
// accessing both the public and the private members,
// in exactly the way that intuitively makes sense
z: () => this.x + this.#.y,
};
}
In this way, there is absolutely no confusing behavior or usage: it's just a plain javascript object. I don't see any way this is more confusing, and in fact is largely identical to the way people have been making their own implementations of private data using WeakMaps.
// some-module.js
const privates = new WeakMap();
class Foo {
constructor(x) {
privates.set(this, {x}); // initialization of private data
}
method(that) { // access of private data, including from another instance of Foo
return privates.get(this).x - privates.get(that).x;
}
// it is trivial to show that I can dynamically access, write, and iterate over private data as well
}
Also, you could just say "to declare a private field, begin its name with #", but you'd also be lying (or at the very least, withholding the truth). Because the truth is, it doesn't behave like like other fields. Calling these private fields, without also being up front about the fact that their access and write semantics are entirely different is deceptive. We're going to end up with people trying to do things like iterate through the private members, access private members dynamically, write new private members at will within methods, and even dynamically write new private members and these things won't work, and it won't be immediately obvious why. In order to explain how private members work, you need to explain all of it.
And I really doubt anyone is going to encounter not being able to iterate through or write new private members and think "ah yes, that makes sense, because they're private". There's no immediately obvious reason that there can't be a private way to iterate through private members. There's certainly no obvious reason I can't add a new private member in a method, either. I can understand the technical reasons, having read much of the discussion here, but I highly doubt that anyone unaware of the implementation details of private members will be able to make those connections.
As to the naming comment, it was my assumption that the sigil would not be parsed as part of the identifier token (#
is not a valid identifier character, after all) and would be parsed instead as a symbol or operator token, indicating that the following identifier should be looked up from the private slots, rather than using the standard property lookup.
But the biggest issue with the name is that this would be an unprecedented change to identifiers. There is nowhere else in all of javascript where the way you name a value impact behavior or meaning. This is not only adding a new concept to javascript, but a concept that fits in with nothing else in the language.
Using the lone #
, however, we could even treat the sigil as an operator. .#
would be an operator meaning "get the private object for this object", whit #.foo
still being shorthand for this.#.foo
. This would also make a babel transform fairly simple to implement, using an object stored in a WeakMap, providing a far less compicated parallel to what is already possible in JS. Having a reference to the pre-privates-javascript way to do private data greatly simplifies the explanation.
Is there even a parallel to the proposal as-is in JS? Is it even possible (without excessive overhead) to transform a class with private members into current-JS, with full spec compliance?
I think you misunderstand me. No, I don't intend for this object to have any exotic behavior: it will behave exactly the same as an object assigned to
#_
under the current proposal.
Ah, yes, I was conflating your proposal with @shannon's, sorry.
And I entirely disagree: it is not particularly awkward to access other fields from the object. Use an arrow function.
Since classes as they exist currently only allow methods, not arrows, I disagree with you about how awkward this is.
In this way, there is absolutely no confusing behavior or usage: it's just a plain javascript object. I don't see any way this is more confusing, and in fact is largely identical to the way people have been making their own implementations of private data using WeakMaps.
Again, I disagree about how confusing it is to have an object's private fields be properties of a different object.
Also, you could just say "to declare a private field, begin its name with
#
", but you'd also be lying (or at the very least, withholding the truth). Because the truth is, it doesn't behave like like other fields.
Mm. I disagree that failing to explain every piece of the semantics of something constitutes lying, and have not found most people to be confused by the behavior of private fields. This because private fields behave a great like other fields except that they are private. There are of course details to what that means exactly, but that alone seems to give people an extremely good idea of how they work and let them get on with writing code. If they're interested in how privacy is enforced, I will tell them that it's enforced by the name of the field only being available within the lexical scope in which it was declared, and this name being the only way to refer to the field.
People have been teaching a pattern of creating "private members" with privacy enforced by lexical scope for a decade. These likewise do not show in iteration, cannot be created dynamically, etc. I don't think I would call Crockford a liar, here. And the expectations to which you allude don't seem to have meant people avoid teaching this pattern, even to absolute novices.
I do not share your beliefs about how people tend to expect private fields to behave.
I highly doubt that anyone unaware of the implementation details of private members will be able to make those connections.
Well, quite a few of the people to whom I have explained private fields as above have made those connections, so "anyone" is overstating your case, I think.
As to the naming comment, it was my assumption that the sigil would not be parsed as part of the identifier token (
#
is not a valid identifier character, after all) and would be parsed instead as a symbol or operator token,
Yes, it's not a valid identifier character. I don't understand why this means it isn't part of the name from the point of view of the programmer. If your concern is people thinking about implementation details - well, in the spec there is a production called PrivateName
, which includes the #
.
There is nowhere else in all of javascript where the way you name a value impact behavior or meaning.
__proto__
and constructor
spring immediately to mind, but that aside: yes, this is new syntax. In my experience it has been very easy to explain its behavior, so while the lack of precedent is a concern, it doesn't seem to me that it ought to be a blocking one.
Having a reference to the pre-privates-javascript way to do private data greatly simplifies the explanation.
I contend that to the extent there is a "the" pre-privates-javascript way to do private data, it is the pattern based on lexical scope I refer to above. I further contend that said pattern is much closer to this proposal than to a new notion of a "private object".
Is there even a parallel to the proposal as-is in JS? Is it even possible (without excessive overhead) to transform a class with private members into current-JS, with full spec compliance?
Yes, with WeakMaps very much like what you're doing. It's awkward and you might debate what counts as "excessive overhead", but it's doable and matches the spec precisely, as long as you're careful about receivers. You can either use a single WeakMap, as you have, or have one per field, as babel is doing.
@bakkot if #
is part of the name can I just use a lone #
as the private field? In other words, is there any reason I can't just do as @EthanRutherford has described with the current proposal?
Now that I have rethought about it, I think the # = { ... }
or #_ = { ... }
workaround is still somewhat lacking.
x = 5
# = {
y: 10,
z: () => this.x + this.#.y,
}
My concern is that lexically scoped functions are not optimized for this use case. Each instance would create new functions. This is just antithetical to the idea of prototypes in JS and by extension classes. Needing to iterate/dynamically access/destructure methods is far less common so if this proposal goes through as it is then I will just learn to live with it I guess.
Now that I have reread this thread I'm not sure if the comment from @littledan was about my original proposal or the modified version from @EthanRutherford.
This thread is such a confusing mess at this point so let me see if I can get back to my original proposal.
@bakkot
People have been teaching a pattern of creating "private members" with privacy enforced by lexical scope for a decade. These likewise do not show in iteration, cannot be created dynamically, etc.
The difference here is this.#x
doesn't look like lexical variable access. It looks like a property access. The two normally behave very differently. I understand after reading through all this that it is in fact meant to behave more like a variable access. This is a somewhat easy concept to explain but doesn't resolve the fact that it doesn't look right. Yes yes, we will all get used to it. But it still won't look like anything else in JS so it does require further explanation.
Say I'm a new JavaScript developer and I have never heard of this proposal and I have no idea #
is normally not a valid character for a variable. I've seen $
so why not #
. Then I see this in someone's code and I can see they are just accessing it like any other property, this.#x
, so it must just be a property. I won't say to myself, oh this is a private variable. I would just be confused when I get errors trying to access the property from outside the class or trying to name a variable with #
. This initial confusing behavior is what discourages people from using different langauges.
Look at this simple example from the perspective of an inexperienced JS developer:
class Foobar {
$x = 'x';
#y = 'y';
method() {
return this.$x + this.#y;
}
}
this.#y
and this.$x
are any different from eachotherIt would have made more sense to do something like #this
or #obj
which would at least look like variable access for something lexically scoped. As a new developer I would say, what is #this
, since I can clearly see it's not defined anywhere.
In my original proposal here, by not using #
in the property declaration it's somewhat more clear that this.#
isn't user declared and it's not just a normal property. This would be even clearer by doing #this
and I think I will edit my proposal to include this. However I still think #this
should be an object that contains all the private properties and methods bound to this
. It would behave like a simplified Proxy of this
.
The use cases for iteration, dynamic access, destructuring may not seem common but it's important to keep the language consistent. If I can iterate over public properties I would expect to be able to iterate over private properties. If I can dynamically assign/access public properties I would expect to be able to for private properties. If I can destructure for public properties I would expect to be able to for private properties. The key here is they aren't variables, they are properties. They should behave like properties first and foremost. When they don't it's a major diversion from what is expected.
I have an alternative proposal, which while it unfortunately does not address the last issue @shannon referred to, would hopefully address most of the concerns presented by both sides.
The syntax would be largely the same as the proposal as-is, but we augment with a few things.
Firstly, only use one internal slot. This contains a pseudo-object who's this
parameter refers to the instance of the class, so that private methods need not be bound or arrow-ed. Accesses and declarations are as proposed, #foo = "bar"
, this.#foo | #foo
. However, this internal container also has a few methods in its prototype chain, such that (names up for debate) this.#getProperties()
returns an iterator over all private properties, this.#getProperty()
provides dynamic access, and this.#foo = ...
is allowed for adding new private values (for completeness, there should probably also be a this.#setProperty()
equivalent). These methods should probably be non-writeable. Perhaps a ##
prefix for these would be appropriate, but I'm not attached to any particular name scheme.
This provides the same protections against accidental leakage of data and again, is mostly identical to the proposal as-is, but also provides capability to dynamically read/write and iterate over private data (but importantly, only for instances of the class). The syntax, and therefore the conceptualization, remains the same, but the usage of a internal object-like container allows straightforward avenues for providing dynamic access and iterability. By providing access to these features through the same private interface (this.#name
), we also maintain the strict privacy of these properties. One could still, of course, leak out information, but one would have to be pretty intentional in writing such a leak.
foo(name) {
// clearly not an accident
return this.#getProperty(name);
}
@ljharb @bakkot @EthanRutherford I have provided an update in the description that I think may address some of the concerns with my proposal.
@EthanRutherford
Firstly, only use one internal slot.
Internals slots are purely a spec mechanism; I don't know what observable semantics you intend for this to imply.
This contains a pseudo-object who's
this
parameter refers to the instance of the class, so that private methods need not be bound or arrow-ed.
I'm sorry, I don't know what this means.
Accesses and declarations are as proposed,
#foo = "bar"
,this.#foo | #foo
.
The #foo
shorthand syntax for property access is not currently a part of this proposal, in case that wasn't clear. If there's still a reference to it lying around somewhere, let us know and we'll try to get it cleaned up.
However, this internal container also has a few methods in its prototype chain, such that (names up for debate)
this.#getProperties()
returns an iterator over all private properties,this.#getProperty()
provides dynamic access,
We could add some method of dynamically iterating over private properties in a follow-on proposal, but I am really not convinced it's worth doing. I just don't get why you expect or want this.
and
this.#foo = ...
is allowed for adding new private values
This is the sticking point for me. There's a few different problems:
Could you only create #foo
on instances of the class? Remember, you can .call
a public method on any random object. How would that be enforced? And what does "instances of the class" even mean, recalling that you can manipulate prototypes at runtime? In the current proposal, all of the private fields are installed onto instances when they are constructed, so that attempting to access a field on an object which lacks it is an error, so this has a clear definition, but what definition are you going to use if new fields can be created after the class has been defined?
I am uncomfortable creating new names dynamically, especially when they may already be in scope (see below).
It doesn't play well with nested classes. In the current proposal, this works:
class List {
#nodes = [];
static Node = class Node {
#owner;
constructor(owner) {
this.#owner = owner;
}
delete() {
this.#owner.#nodes = remove(this.#owner.#nodes, this); // this is the interesting part
}
};
// etc
}
That is, a nested class can refer to private fields defined on its parent, since it has visibility of the field name.
But that means it is not clear - even to the spec - whether obj.#foo = whatever
is an attempt to create a new field on an instance of the current class or to manipulate an existing field of an instance of an outer class (or to create a new field on such an instance).
I continue to feel that a Map (or plain object!) in a private field is a strictly better option than creating new private fields at runtime, in the case that you want dynamically named private state. It's much clearer what you're doing and avoids the issues above. I also fundamentally do not understand why you feel it is so important to support dynamically creating fields.
@shannon
This doesn't fully address the issue with private keyword implying this.x access but I think it comes pretty close.
I don't think it gets close enough - like I say, anything which has private x
for declaration and not literally this.x
for access seems way too dangerous to me.
It also has some problems with chained access:
class Node {
#data;
#next;
constructor(data, next) {
this.#data = data;
this.#next = next;
}
lookaheadType() {
return this.#next.#data.type; // works in the current proposal
}
}
let head = new Node({type: 'eos'}, null);
head = new Node({type: 'token'}, head);
How would you do the equivalent of this.#next.#data.type
under your proposal?
Similar ideas have been proposed a number of times in the existing extremely lengthy previous discussions of alternate syntax, and have been rejected for the similar reasons.
@bakkot
I don't think it gets close enough - like I say, anything which has private x for declaration and not literally this.x for access seems way too dangerous to me.
This may be true but this.#x
implies property access when it isn't. this['#x'] = 'x'
will silently create a public property as well. I'd say they are equally dangerous.
How would you do the equivalent of this.#next.#data.type under your proposal?
I hadn't thought about that use case but with a slight change to the syntax verification you could do something like this:
class Node {
private data;
private next;
constructor(data, next) {
#this.data = data;
#this.next = next;
}
lookaheadType() {
return #(#this.next).data.type;
}
}
let head = new Node({type: 'eos'}, null);
head = new Node({type: 'token'}, head);
I think of #
as a keyword itself that gives me the private properties and methods of any object that is an instance of this class.
This may be true but
this.#
x implies property access when it isn't.this['#x'] = 'x'
will silently create a public property as well. I'd say they are equally dangerous.
It is property access. It's just accessing a private property.
I agree that there's a risk with the asymmetry with dynamic access (it's called out in the FAQ), but strongly disagree that it is of even remotely the same level of risk.
I hadn't thought about that use case but with a slight change to the syntax verification you could do something like this:
Technically, yes, but #(#this.next).data.type
is significantly harder to read, for me, than this.#next.#data.type
. I don't really understand why you'd prefer this syntax.
Very similar proposals have been raised and rejected a number of times previously in the main syntax discussion thread.
It may have been from old comments on issues, but I believe it's in the faq: "why not this#foo
?: asi issues due to the shorthand #foo
".
As to the code example in your post, I don't see how this is enforced even as is. Who's to say I don't pass in something completely different than a List as the "owner" of a node? How would the access violation in the delete
method be detected at any point before runtime? Is the parser going to walk through every possible code path to make sure the wrong thing isn't passed in?
I don't really see a difference in enforceability whether private fields are preinstalled or not, we still don't know the type of what could possibly be being passed in at runtime until runtime.
The answer to the "why" you keep asking is fairly simple: consistency with public fields. As has been stated by both @shannon and myself, what we find objectionable is primarily that these look like fields, but behave more like lexically closed-over variables. What makes it even more complicated is that they're actually even a mashup of the two. In order to allow two instances of the same class to access each other's private fields, they go from lexically closed over fields to more like lexically closed-over keys for entries stored in weakmaps.. or something to that effect.
Additionally, there's a concern that, to the unaware, it is not clear that this._foo
and this.#foo
imply different semantics. The general readability (particularly to outsiders or beginners) is reduced. I feel like it could be significantly better if we could have something like
class Foo {
private field = 0;
bar(other) {
return private.field + other.private.field;
}
}
But I understand the potential for backward compatibility issues.
However, I don't think the argument that private foo
implies access would be this.foo
is all that strong, because static foo
does not imply access would be this.foo
.
@bakkot
Technically, yes, but #(#this.next).data.type is significantly harder to read, for me, than this.#next.#data.type. I don't really understand why you'd prefer this syntax.
I'd prefer this syntax because it's closer to anything else we have in JS. And it behaves as I would expect once I know what the #
means. It's easier to go from no knowledge to complete understanding, instead of having to know all sorts of trivia about how private properties work in JS.
@bakkot
Very similar proposals have been raised and rejected a number of times previously in the main syntax discussion thread.
Sorry it is long but I don't see anything quite like what I am suggesting in there. private.x
or obj.private.x
is very different from #obj.x
. These would actually go against my arguments here for moving #
to the instance reference instead of the property reference.
Edit* nevermind I see your comment in there but I'm not exactly sure why it would be fatal with the #
shorthand
@EthanRutherford
As to the code example in your post, I don't see how this is enforced even as is.
Attempting to access a private field of an object which isn't present on said object throws an error. If you pass something else in, the delete
method would throw. (In real code, you'd do this check in the constructor.)
I don't really see a difference in enforceability whether private fields are preinstalled or not, we still don't know the type of what could possibly be being passed in at runtime until runtime.
You're proposing that private fields could be created at runtime, presumably in code outside of the constructor. Currently private fields can only be installed during construction, when there is no ambiguity about what it means for something to be an instance of the class - private field creation is tied to instance creation.
The answer to the "why" you keep asking is fairly simple: consistency with public fields.
#getProperty
is not especially consistent with public fields. I'm still confused.
I also don't understand why this particular kind of consistency is so important. I do not remotely share the expectation that being able to for-in
over (enumerable) public fields implies there must be some way to iterate over private fields, for example.
that these look like fields, but behave more like lexically closed-over variables
Again, they behave a great deal like fields. They are per instance, they are installed by the constructor at the same time as public fields, they are accessed with obj.
, they establish the right receiver if you do this.#method()
, etc. In particular, they behave almost exactly like fields whose name is only available within a particular scope.
In order to allow two instances of the same class to access each other's private fields, they go from lexically closed over fields to more like lexically closed-over keys for entries stored in weakmaps.. or something to that effect.
The name is the thing which is closed over. I'm not entirely sure what it would mean for a field to be closed over.
Additionally, there's a concern that, to the unaware, it is not clear that this._foo and this.#foo imply different semantics.
Yes, that's a concern. It doesn't seem that major, to me. Because attempting to write #foo
anywhere outside a class which declares #foo
throws a syntax error, presumably with a note implying what the syntax means, I think people will generally learn pretty quick.
However, I don't think the argument that
private foo
implies access would bethis.foo
is all that strong, becausestatic foo
does not imply access would bethis.foo
.
As far as I'm aware static foo
doesn't work that way in other languages, and does not create a per-instance thing. (And it is accessible with this.foo
in static contexts!) private foo
very much does work that way in other languages, and does create a per-instance thing.
@shannon
I'd prefer this syntax because it's closer to anything else we have in JS.
I just don't get this. this.#foo
is very similar to something we have in JS: namely, public property access.
It's easier to go from no knowledge to complete understanding, instead of having to know all sorts of trivia about how private properties work in JS.
It really, really is not that hard to explain how private properties work in this proposal. I have done it many many times to people of varied levels of experience.
Sorry it is long but I don't see anything quite like what I am suggesting in there.
People have proposed private(obj).x
at least twice to my recollection. Possibly not #(obj).x
precisely, but similar concerns apply.
@bakkot
I just don't get this. this.#foo is very similar to something we have in JS: namely, public property access.
Is this.#foo
somehow more similar than #this.foo
? Because every way that it should be (more similar), it actually isn't in implementation.
class Foobar() {
#x = 'x';
method(obj) {
this.#x !== this['#x'];
this.#y = 'y'; //runtime error
const { #x } = this; //syntax error
for(const prop in this){ } //#x doesn't exist even though I have access here
}
}
At least with #this
it's immediately clear that these would not work with this
and you would need to reference #this
instead. This would be true if #
was not a special character and #this
was just a lexically scoped variable. There's nothing strange about it once you know where #this comes from with no other prior knowledge. This is what is closer to the rest of javascript. Any syntax errors thrown as I have described in my proposal would come from accidents. Not anything useful. You don't need to pass #this
around since it's available everywhere internally.
It really, really is not that hard to explain how private properties work in this proposal. I have done it many many times to people of varied levels of experience.
You have explained. They didn't come to the conclusion on how it works based on their knowledge of the rest of JavaScript. I can explain it too. You prefix private variables with #
and then you can't access it this way, or this way, or this way, or this way, good luck. No logical explanation is to why all of these restrictions. If I'm in the class I should be able to access it in every way that I can access a public property. Any other restrictions seem like they just weren't thought out enough.
This proposal is putting a lot of restrictions in the wrong place. It seems that all the negatives are against the class developer just so there's no chance to accidentally leak. When, as I have described, you can still keep leaking from happening without removing any functionality from the class developer.
People have proposed private(obj).x at least twice to my recollection. Possibly #(obj).x precisely, but similar concerns apply.
It seems the only real concern about this is the syntax for chaining. This seems an acceptable compromise to me to regain all the functionality we are used to. Plus I don't think you really need the parenthesis around obj
(e.g. #(obj)
) unless you are chaining.
@shannon Why would this.#y
be a runtime error? You can statically determine, at parse time, that that’s referring to a private field that doesn't exist. I would expect that to be a syntax error.
Separately, for..in only enumerates enumerable properties, so “i have access to it” in no way guarantees “i can find it in an enumeration loop” - I’m not sure why you have that expectation.
#this
would make, as has been demonstrated, accessing private fields on non-receiver objects very unergonomic. This is not the primary use case, surely, but it’s relatively common - including on any comparison or brand-checking method, both of which are common in userland. In other words, this is not an acceptable compromise - it’s an important use case.
@ljharb
Why would this.#y be a runtime error? You can statically determine, at parse time, that that’s referring to a private field that doesn't exist. I would expect that to be a syntax error.
My mistake.
Separately, for..in only enumerates enumerable properties, so “i have access to it” in no way guarantees “i can find it in an enumeration loop” - I’m not sure why you have that expectation.
Because I have access to public properties declared in a class this way. Removing enumeration on a field is another thing all together and is usually done intentionally, not as a side effect of doing something else.
this would make, as has been demonstrated, accessing private fields on non-receiver objects very unergonomic. This is not the primary use case, surely, but it’s relatively common - including on any comparison or brand-checking method, both of which are common in userland. In other words, this is not an acceptable compromise - it’s an important use case.
Please show me a use case that isn't solved by writing a single line slightly differently. Where as my use case requires writing multiple entire branches of code. This is not acceptable to me. Destructuring alone should be enough to motivate you guys on this. There is a reason it was added to the langauge.
You don’t have access to public properties that have been made non-enumerable, nor do you have access to Symbol properties in a for..in loop.
Private fields are meant to be private; enumerability is about discoverability, but private fields are meant to be unobservable, not just undiscoverable. Non-enumerability is the explicit intention of creating a private field, imo.
That it can be done in a single line doesn’t automatically mean that something is ergonomic.
Destructuring imo was partially added to avoid typos (by typing the name twice); private fields lack that problem because they’re not dynamic and can be statically verified. There’s no need for it here.
Could you clarify your use case that becomes untenable with the current proposal, that your suggestion addresses?
@ljharb please review my description, there are 3 cases there.
As for specific use cases for what I am working on, it involves creating a game engine library that uses classes as the API for creating game objects. There often times when game modes determine what methods or properties to access. For example, in-editor vs in-game. This proposal would lead to lots of branching when private properties are used or a major refactor of the API.
There are also times when iterating over properties (private or not, I don't know what the developer will use) is required but that could be resolved with a restrictions on my API even though I see no reason why this should logically be prevented.
Destructuring is more than just about typos. Reduced typing, readability, and one important use case that comes to mind, default values. In a game engine it is important to manage memory and sometimes loading a default asset needs to happen at a specific point in the pipeline. You can't always assign it during declaration. And in this case you don't actually want to assign it, you just want to use it until the real asset is loaded. So again this would lead to another branch to check if it's loaded. Destructuring with a default value would read cleaner than a ternary operator especially if I have several assets, which is quite often the case.
I don’t see any concrete cases in your description, just arbitrary code snippets.
As for your concrete use case, private field syntax is a static hardcoded thing - if you need metaprogramming and to be able to change privacy at runtime, would you not want to use a decorator to dynamically create private fields, including a static private method that could dynamically provide the values of those fields - hardly even using the syntax?
@ljharb
As for your concrete use case, private field syntax is a static hardcoded thing - if you need metaprogramming and to be able to change privacy at runtime
I'm not changing privacy at runtime in any of those case so I don't follow. All of these use cases are strictly about access.
would you not want to use a decorator to dynamically create private fields, including a static private method that could dynamically provide the values of those fields - hardly even using the syntax?
My API is already based around decorators on the fields I don't really want to start chaining more together to simulate private fields when it's being added to the language in this proposal. Chaining decorators already leads to all sorts of difficult issues.
It wouldn’t be to simulate them; decorators will be able to dynamically create real private fields.
There often times when game modes determine what methods or properties to access. For example, in-editor vs in-game. This proposal would lead to lots of branching when private properties are used or a major refactor of the API.
There is nothing in the current proposal stopping you from putting objects in private fields, very much like you've proposed to do above. Does that not satisfy your use case?
@ljharb
It wouldn’t be to simulate them; decorators will be able to dynamically create real private fields.
But developers of my library would not define them like private properties so it would be a strange API decision. Basically I would have to say don't use standard private properties or you are going to need at least one obligatory branch in every game object you create.
@bakkot
There is nothing in the current proposal stopping you from putting objects in private fields, very much like you've proposed to do above. Does that not satisfy your use case?
This would either require a major refactor of my API or cause a lot of pain to my consumers.
This would either require a major refactor of my API or cause a lot of pain to my consumers.
Uh. You understand that private fields are not accessible outside of the class in which they're defined, right? They are pretty much by definition not part of your API. That's the whole point.
I realize that's probably obvious, but I genuinely don't understand what you mean here. A mock code sample would be pretty useful, I think.
@bakkot yes I understand that.
Extremely simplified mock
class GameObject {
@component('position') #position; //decorator registers with ECS registry that it cares about entities (dumb data) with position so when it exists it will create a game object and pass in the properties
@component('scale') #scale;
@component('rotation') #rotation;
@component('color') #color;
// ...may be dozens of components
constructor({ position, scale, rotation, color } = {} ){
// can't assign this in super or use object assign here so ¯\_(ツ)_/¯
this.#position= position;
this.#scale = scale;
this.#rotation = rotation;
this.#color = color;
// ...may be dozens more
}
onUpdate(){
// can't iterate over private properties so ¯\_(ツ)_/¯
someCalc(this.#position);
someCalc(this.#scale);
someCalc(this.#rotation);
someCalc(this.#color);
// ...may be dozens more
}
onReplicate(){
// by default this would loop over all properties in the class and send over the network but private properties makes an override required which is fair enough but...
// can't iterate over private properties so ¯\_(ツ)_/¯
sendOverNetwork(this.#position);
sendOverNetwork(this.#scale);
sendOverNetwork(this.#rotation);
sendOverNetwork(this.#color);
// ...may be dozens more
// ... and before you even say anything, there are plenty of reasons to replicate private properties over the network.
}
}
To use a private object this my consumers would need to attempt something like this
class GameObject {
#_ = {
@component('position') position, // O.o
@component('scale') scale,
@component('rotation') rotation,
@component('color') color,
// ...may be dozens of components
}
//... etc ...
}
What does this decorator even do at this point? I would at least need to change my API there. Does it have access to the GameObject class? This isn't even including the game mode branching.
This is really simplified and I'm really tired so there may be a syntax error or two in there.
@shannon
Thanks, that's helpful. I don't really get what your decorator is intended to do here - if those properties are meant to be part of the interface of the object, why are they private? Why do the properties need to be decorated individually, rather than the whole class registering the components it cares about? Why couldn't you do
class GameObject {
@components
#components = {
transform,
scale,
rotation,
color,
};
}
and get everything you need?
It's true that if you were previously relying on people defining a class with public fields, and they would like to switch to using private fields, you'll probably need to change your API for interacting with that class. I don't think that should be all that surprising, and I don't think it's avoidable in general.
@bakkot
Thanks, that's helpful. I don't really get what your decorator is intended to do here - if those properties are meant to be part of the interface of the object, why are they private?
Why does anyone make anything private? If GameObject is to be extended then it may already have a a variable defined down the chain. To limit the object from public mutation. To limit the public documented API. etc. The GameObject has everything it needs from dumb-data through to render/update. The better question is why would the properties be public? Unless you are saying that assigning private properties from parameters in the constructor won't be a common use case then I just don't know anymore.
Why do the properties need to be decorated individually, rather than the whole class registering the components it cares about? Why couldn't you do
For two reasons.
@component('awesomegame:position')
positionThe better question is why would the properties be public?
Because I got the impression they were intended to be part of the public API of the class. I guess not? If all the decorator is intended to do is provide a list of named datums to be provided and aliases for those, why wouldn't you do
@components({ 'awesomegame:position': 'position', 'awesomegame:radius': 'radius'})
class GameObject {
#data;
constructor({ position, radius }) { // you could also just use a binding for the whole object and avoid spelling out the fields
this.#data = { position, radius };
}
}
?
That seems like it much more clearly expresses intent. The private fields the class is using aren't the right thing to be decorated, here: they're internal, not something consumers should need to know about. Forcing people to decorate particular private fields just to get aliasing couples their implementation to yours, which is something private fields are intended to avoid. Why should a consumer be required to have particular private fields at all?
(I really do mean these as questions. I don't meant to criticize your design, I'm just trying to understand it and understand whether it's something it's important we support.)
Because that then requires this.components.transform instead of just this.transform which is just unnecessary for this context.
If you have so many fields of a particular kind that you have to iterate over them, I think it's pretty reasonable to hang them off a new property. Call it #_
if you like. That way you avoid accidentally including other kinds of fields; otherwise adding a new private field to your class could break it, which seems very wrong.
@bakkot One more response then off to bed.
I originally did have an API quite like your snippet until I found out class properties were coming to the JS and I could easily use babel to transpile them. This was before the private #
proposal was merged so I didn't know much about it.
Here are some reasons why I prefer the individual class field declaration.
class MyObject extends GameObject {
@component('color') color = new Color();
}
It's clear, it's concise, it makes perfect sense. I don't even need a constructor at all if I'm not using private properties. The public properties will be assigned properly in the super.
@components({ 'color': 'color' })
class MyObject extends GameObject {
constructor({ color = new Color() } = {}){
this.color = color;
}
}
Less clear, less concise.
Because I got the impression they were intended to be part of the public API of the class. I guess not? If all the decorator is intended to do is provide a list of named datums to be provided and aliases for those, why wouldn't you do
Most of my API is how the class is declared. This part is a declarative API. These exist outside my framework so I know it's not just me. Unreal, Angular, etc. Keeping the declaration simple and concise is the whole point. Most of the dumb-data comes straight over the wire. So the parameters are not part of the API. They are just data being passed in.
Decorators are meant to be used declaratively are they not? I am declaring the properties as being part of the ECS registry.
Each field may also have other metadata attached. For example:
class MyObject extends GameObject {
@replicate @component('color') color = new Color();
onReplicate(){
//replicate all fields that have replicate metadata
}
}
This is very similar to how the Unreal Game engine works.
#pragma once
#include "Core.h"
#include "AReplicatedActor.generated.h"
UCLASS()
class AReplicatedActor : public AActor
{
GENERATED_UCLASS_BODY()
public:
/** A Replicated Boolean Flag */
UPROPERTY(Replicated)
uint32 bFlag:1;
/** A Replicated Array Of Integers */
UPROPERTY(Replicated)
TArray<uint32> IntegerArray;
};
Once you start chaining multiple decorators together then, to follow your approach, I need multiple arrays of field names to mange it all. It starts to get out of control as you build out the engine.
I don't know for sure if private properties will be used by my consumers but based on my previous comment I have to assume they will try at some point. I'd rather not have to say private properties are not supported unless you want a lot of redundant code. If want to support them with any kind of development friendly way as you guys have described I have to really muck up my nice clean API.
@shannon
OK, that makes sense, thanks for explaining.
I think for this particular case, where you somehow manage to create a class with so many decorated private fields that you need to iterate over them, I'd just use a decorator on the class:
import { privateIterable } from "some-lib";
@privateIterable
class GameObject {
#privateIterate; // will be set by the class decorator
@replicated #foo;
@replicated #bar;
onReplicate() {
this.#privateIterate((key, val) => {
// do whatever
});
}
}
Even without a decorator, you can list out the fields which require iterating over in a method, and use that. It requires duplication, but only once:
class GameObject {
@replicated #foo;
@replicated #bar;
#privateIterate(fn) {
fn('#foo', this.#foo);
fn('#bar', this.#bar);
}
onReplicate() {
this.#privateIterate((key, val) => {
// do whatever
});
}
}
Though, separately, I'd first suggest not putting so many similar fields on the same class. I don't think that's a design we want to encourage - in fact, if anything, I'm ok with syntax actively discouraging it in the absence of metaprogramming constructs like decorators.
I don't even need a constructor at all if I'm not using private properties. The public properties will be assigned properly in the super.
Sure, but that's never something that private fields could support unless you explicitly exposed them. If the superclass can set them, they're not private.
Most of my API is how the class is declared. This part is a declarative API.
Private fields are definitely not part of the declarative API. That's the whole point of private fields.
I am declaring the properties as being part of the ECS registry.
I don't exactly understand what you mean by this, but, again, if the properties are meant to be part of the public API of the class, they shouldn't be private.
I'd rather not have to say private properties are not supported unless you want a lot of redundant code.
I don't really get what you mean by "private properties are not supported". Private properties in other people's classes are not part of the interface of those classes. They are purely implementation details. I don't think this is that controversial. They're not something that you as the consumer of the class have anything to do with, unless you are in the unusual case of writing class decorators, which already have the ability to iterate over and create private fields (at class definition time).
@bakkot
Though, separately, I'd first suggest not putting so many similar fields on the same class. I don't think that's a design we want to encourage - in fact, if anything, I'm ok with syntax actively discouraging it in the absence of metaprogramming constructs like decorators.
I don't really understand this. In the absence of metaprogramming constructs like decorators. Why would they be absent in this case?
There are all sorts of reasons you would want to attach metadata to private fields and then iterate over those fields based on metadata. This is just one piece of metadata information in the engine.
I'm not trying to encourage my consumers to make their fields private. I'm trying to keep my API simple. There is a general standard practice to be private by default. This proposal seems to be actively discouraging this.
I also really don't understand what privateIterable
would be in your snippet.
#privateIterate
field from outside the scope of the class delcaration?@replicated
then maintain the list of iterable fields and get their values to pass to #privateIterate? The @replicated
code would be outside the scope of the class as well.I'm entirely confused now.
I don't really understand this. In the absence of metaprogramming constructs like decorators. Why would they be absent in this case?
Sorry, I meant: if you're not doing some sort of metaprogramming, I would discourage having so many fields of the same kind on the same class.
There is a general standard practice to be private by default. This proposal seems to be actively discouraging this.
It's certainly not meant to discourage it! Private by default is a good default.
Yes, there are things you can do with public fields which you can't do with private fields. I don't think that means they shouldn't be the default.
I also really don't understand what
privateIterable
would be in your snippet.
Decorators are a metaprogramming construct. To work at all, they have to have access to class internals including, as currently proposed, private fields. They're conceptually "part of the class". There's more details in that proposal.
@bakkot yea this even more confusing.
let storage = decorators.PrivateName();
What happens if I just pass storage
somewhere else? Doesn't this mean decorator developers can leak access by accident and consumers wouldn't even necessarily know?
Yes, the risks of using third-party decorators are the same as the risks of passing objects to a third-party function.
@ljharb how is this any different than the risk of accidentally leaking a private object from the class itself? It doesn't even have to be third party, it's a lot of abstract headspace when you start adding in decorators with internal private names that are different from the variable defined #
. You don't event know if the field was declared as private so you might not even consider that leaking is an issue.
If this is acceptable for metaprogramming and decorators why not internal iteration and dynamic access?
@ljharb This is how my proposal could be applied to decorators to allow for metaprogramming without leakage.
function observed({kind, key, placement, descriptor, initializer}) {
assert(kind == "field");
assert(placement == "own");
return {
kind: "method",
key,
placement,
descriptor: {
get() { return #this[key]; },
set(value) {
#this[key] = value
// Assume the @bound decorator was used on render
window.requestAnimationFrame(this.render);
},
enumerable: descriptor.enumerable,
configurable: descriptor.configurable,
}
};
}
Decorators also have access to their own private object reference. So #this
here is a reference to a another private object scoped just to this decorator. The syntax rules in the description prevent this from leaking as well. I would expect that placement
would handle setting the variable as private (or prototype/static it doesn't really matter) in the implementation.
Edited removed snark, added clarification, and fixed an obvious grammatical coding issue. Sorry I was tired, I needed to step away from this for a bit.
Update at bottom of description
After reading through the FAQ and issues on the various related repos regarding the sigil I just wanted to run something else by the champions of this proposal.
Please forgive me if this has already been discussed but as I said there are various repos and lots of threads so I may have missed it.
I'm wondering if just making
#
be a an object property ofthis
makes this proposal more consistent with existing paradigms in Javascript.For example:
Would be sugar for:
Why?
Well I get the need for the
#
sigil for assesor efficiency but there are a couple of points of this proposal that I find unnessarily restricitve.this['#x']
orthis[somevar]
). This would seem like any use case where you would need this for public properties could also be applied to private properties.this.#
repeatedly it would be nice to be able to use destructuring at the beginning of a method to access private properties.With this adjustment all of these should be possible without losing any encapsulation.
Variable accessors:
vs
Private property iteration:
vs
Destructuring:
vs
Why private in declaration? (I know it's been talked about but I have to try)
First consistency and readability:
vs
Secondly, with
#
being a property ofthis
instead of part of the name of the property it makes more sense here.Conclusion
To me, this makes this proposal seem a little less strange compared to the rest of Javascript. It's a simple mental model. All private properties are stored on
this.#
which is only accessible from within the class methods.Keeping the sigil for accessors means it's non breaking for any existing code so I'm ok with it there but I really hope we reconsider using
private
in the declaration just to keep the syntax consistent and readable.Edited: typo and broken formatting in github
Updated proposal
I wanted to provide an update to my proposal based on the discussions in the thread. I believe that the
#
should be moved to beforethis
to make it clearer to developers that#this
is a lexically scoped variable.Would be sugar for:
I have also taken the comments of @ljharb and @bakkot into consideration and I have a further addition that will prevent any leakage to mutations.
To avoid any leakage we can make it a syntax error to assign
#this
to any variable, property, return value, or function parameter. This is actually a really simple syntax check.#
must be immediately followed by variable identifier which must be immediately followed by a.
or[
. With the only exception being for destructuring.Syntax error example:
I'm sure there tons I have missed here but as long as they follow those rules there shouldn't be any leakage.
Valid syntax examples:
This prevents any leakage for mutations.
This doesn't fully address the issue with private keyword implying this.x access but I think it comes pretty close.
Edit: I accidentally left out or
[
syntax check that I had in my notes here