Closed rdking closed 6 years ago
@bakkot Thank you for at least agreeing to try. So let's get started.
Why isn't access this.x? ...Having a private field named x must not prevent there from being a public field named x, so accessing a private field can't just be a normal lookup.
This is only an issue in JavaScript because of its lack of static types. Statically typed languages use type declarations to distinguish the external-public/internal-private cases without the need of a sigil. But a dynamically typed language doesn't have enough static information to differentiate those cases.
I'm not arguing that access is supposed to be this.x
. This is ES. It can't be this.x
while still providing hard encapsulation. My issue here is that your justification is completely bogus. The type of the property isn't the problem. Suppose someone wanted to add a public String x
to an object that already had a private String x
. The real issue is that public and extensible is default for objects in ES. Someone adding a new public member to an object that just happens to collide with a private member would be shocked by the TypeError they'd get trying to access it later. You might want to swap out that 2nd paragraph.
The first real problem begins here:
Why doesn't this['#x'] access the private field named #x, given that this.#x does?
- This would complicate property access semantics.
- Dynamic access to private fields is contrary to the notion of 'private'.
1 is true. Given the notation you've chosen, access semantics are broken there. The problem is that you've made the #
part of the property's name. There's a simple way to fix this issue. Swap .#
with #.
. This gives you back dynamic access and eliminates the concerns you mention below.
But doesn't giving this.#x and this['#x'] different semantics break an invariant of current syntax? Not exactly, but it is a concern. this.#x has never previously been legal syntax, so from one point of view there can be no invariant regarding it.
On the other hand, it might be surprising that they differ, and this is a downside of the current proposal.
2 is false. The notion of 'private', as mapped into ES, is only the notion that such a member is not accessible as a property of the owning object. So if x
is private on obj
and obj
doesn't have a public x
, then "x" in obj
is always false. However, if obj
has a member function f
, then f()
will still have access to x
when called with obj
as its context. How that access is carried out, whether through .
or []
is entirely irrelevant to the notion of 'private'.
Someone adding a new public member to an object that just happens to collide with a private member would be shocked by the TypeError they'd get trying to access it later.
If there were a sufficiently good type system, they would not get a TypeError later - you would not be able to add or refer to the public field from within the class, and from outside if it you would only be able to refer the public one. That is in fact what languages like Java do, as in
class Base {
private int x = 0;
public int m() {
return this.x;
}
}
class Derived extends Base {
public int x = 1;
}
Derived foo = new Derived();
System.out.println(foo.m()); // 0, no errors
System.out.println(foo.x); // 1, no errors
I stand by that paragraph in the FAQ.
There's a simple way to fix this issue. Swap
.#
with#.
.
I find the symmetry between declaration and access (as in class { #x; y; m(){ return this.#x + this.y; } }
) sufficiently valuable that I would not want to give it up just to avoid this concern or to allow dynamic access.
The notion of 'private', as mapped into ES, is only the notion that such a member is not accessible as a property of the owning object.
That is one mental model; it is not mine. Mine is as given in the FAQ. I suppose it could be worth rewording to "contrary to one possible notion of private"
@bakkot In the example you gave above, Base.m cannot access Derived.x even if Base.x doesn't exist. In fact, if Base.x doesn't exist, this code won't compile. That has nothing to do with the type of x. Taking this into consideration, if by "sufficiently good type system" you were referring to how properties are bound to objects, and the access limitations placed on Base methods by other languages, then I get what you're saying, but that has nothing to do with static typing, but is rather due to the inheritance and referencing structure of the language. ES could reproduce the same results by keeping functions and non-functions separated within an object, but then functions would no longer be 1st class values. The reason I'm asking you to rework that paragraph is explained here. While you mean to refer to the object type system in use, you use "statically typed language" and "dynamically typed language", both of which refer to how the language handles the type of a variable. That's misleading at best.
I find the symmetry between declaration and access (as in class { #x; y; m(){ return this.#x + this.y; } }) sufficiently valuable that I would not want to give it up just to avoid this concern or to allow dynamic access.
So basically, you're saying you prefer aesthetics over functionality? Aren't there problems with this?
#
is not a valid character in an [[IdentifierName]]
. This proposal isn't changing that fact. Otherwise it would be possible to do this: var #foo;
. Object member access is always either <object>.<identifer>
or <object>.[<IdentifierName>]
. Given that the []
form is out, this means that the #
is part of the identifier. So then what is #
? Everything else in the language has a simple, single meaning, but under this proposal, #
is a non-identifier character that is part of an identifier. That's confusing. What you're replacing (_
) was always a valid identifier character. So the mental models don't match._
) for private members can't be said to be a simple 1-to-1 replacement since many of the properties that have been declared with _
are meant to be protected
instead of private
. So, for those who have complex libraries needing protected
but still won't get it under this proposal, there is not much merit in making the effort to convert to the use of #
. Not to mention that this will need to be done carefully and with much testing so as to avoid accidentally making a protected
member private. What will be even more difficult for the larger libraries is ensuring that all members intended to be private have been marked as such.BTW:
What I offered in counter shares a near symmetry while not disabling functionality (as in class { #x; y; m(){ return this#.x + this.y; } }
). This simple variation maintains both visual and functional symmetry with existing code. It also maintains partial symmetry with the declaration in that in order to access or declare a private member, '#' must be present.
In a somewhat long-winded post here, I made the following arguments about preferring 'private' to '#' for member declarations:
So let me ask a different way. What is so valuable about the symmetry of your proposed notation that it warrants breaking programmer intuition about access notation, how to declare members, and the use of symbols?
In fact, if Base.x doesn't exist, this code won't compile. That has nothing to do with the type of x.
It has to do with the type of Base, and of foo
. In particular, it has to do with whether the compiler can know at compile time which class contains the declaration of a particular field accessed on a particular expression, and whether the shape of objects can be known statically at all. If it can, it can statically enforce access restrictions (and, for example, fail compilation if properties are missing). If it can't, it can't. JavaScript's type system is not sufficiently static to do this. I really don't think that's misleading.
So basically, you're saying you prefer aesthetics over functionality?
I am saying that aesthetics and ergonomics are important, and in this particular instance I am unwilling to sacrifice them to gain a particular functionality, yes. It is not a blanket statement about a preference for one or the other.
What is so valuable about the symmetry of your proposed notation that it warrants breaking programmer intuition about access notation, how to declare members, and the use of symbols?
Symmetry is inherently valuable. It isn't valuable above all else, but it's not nothing. It makes it easier for people to hold the language in their head, to understand its meaning when reading code, to write it fluidly without having to stop and think about the syntax. These things are important.
Also, in teaching this feature, I have not found that it significantly breaks most programmer's intuition about access notation, how to declare members, and the use of symbols. It's never going to be possible for something to be intuitive for everyone, unfortunately, but I strongly suspect your proposal would be much worse in this regard.
@bakkot This one is a TL;DR for sure. Let me summarize it like this: While I get that you truly believe what you said, I think it's only due to familiarity and sheer effort of evangelizing when compared to significantly more complicated and less viable proposals that yours has come out on top. If you were to do some blind testing, your suggested syntax vs mine with developers who haven't seen either, both suggestions being presented in parallel, you won't get the results you expect.
I am saying that aesthetics and ergonomics are important...
Here we agree... to a point. I think the point of disagreement between is in exactly how "ergonomic" or "aesthetically pleasing" your use of the #
actually is. Case in point:
class Example {
#field = 1;
member() {
return this.#field;
}
}
vs
class Example {
private field = 1;
member() {
return this#.field;
}
}
On the issue of ergonomics: You win with the shorter syntax. On the issue of aesthetics: You lose for reduced readability and a declaration syntax that is inconsistent with everything else about the language. It's almost as if you're trying to turn ES into lisp.
...and in this particular instance I am unwilling to sacrifice them to gain a particular functionality, yes. It is not a blanket statement about a preference for one or the other.
Isn't it though? There are many different ES programming paradigms that make rampant use of the simple fact that obj.x
is equivalent to obj['x']
. These paradigms often help to DRY the code further than can be done otherwise. This is another point that works against the current proposal. Less DRY code means longer download and parse times. Marginal to be sure for most cases, but still not a good thing to force onto the developer for at-best arguable aesthetic reasons. It may not be a "blanket preference", but it is definitely a questionable one that will negatively impact developers.
Also, in teaching this feature, I have not found that it significantly breaks most programmer's intuition about access notation, how to declare members, and the use of symbols. It's never going to be possible for something to be intuitive for everyone, unfortunately, but I strongly suspect your proposal would be much worse in this regard.
And yet I, and several others, have had exactly the opposite experience. In general, any decent programmer is going to be able to shrug off the oddities of your syntax after a moment of getting used to it. There's no doubt about this. However, that's not a justification for using an unnecessarily odd syntax.
You suspecting that the proposal I've offered "would be much worse in this regard" has little merit. I've tested your syntax and mine by placing them both in front of developers, gave an unbiased explanation for any questions asked and found that:
.
and []
is broken with your syntax.#
after a .
when accessing private members using your syntax regardless of who's proposal style was requested first.#
after the object name regardless of who's proposal style was requested first.obj.#x.#y.#z
) and no less so with mine (i.e. obj#.x#.y#.z)
. #
in any context.#
as a declarator for writing code, all but 1 preferred to use the private
keyword for its familiarity and readability.You see, it's one thing to indoctrinate people to a particular syntax first, then introduce competition, but it's entirely different when both are introduced in parallel. I'm willing to bet that if you introduced my suggestions along side yours to someone who hasn't yet seen your proposed syntax or mine, you'd be shocked by the response you get.
I'm of the impression that your proposal has such a strong following merely because it truly was the best suggestion anyone had come up with until recently. It has the advantage of the time people have already invested in it. Pride makes it hard to even consider other suggestions in such situations. While I understand that, I find it hard to be sympathetic to that when the result is unnecessarily limiting, functionally disparaging, damages possibilities for future expansion, and so very easy to remedy.
What I meant by damaging towards future expansion is that, suppose your proposal goes through to stage 4. That means that #
will be the token for declaring private fields. Many library developers will then try to use #
notation to protect their code only to very quickly realize that they are still missing support for protected
. You've shown it can be somewhat reasonably done using a convoluted decorator and WeakMaps.
@ljharb has told me that one of the justifications for even attempting private fields is due to the complications involved with properly implementing WeakMaps for this use, and yet every developer wanting protected
support will roll their own implementation or copy one from the net. This will lead to libraries with custom, incompatible implementations of this feature. With enough time, the TC39 board will want to make it a language feature. Even though the 'protected' keyword is perfect for this use, they won't have a choice but to leave it as a decorator. This will limit the ability to improve functionality around this paradigm.
Why will this happen? Simply because you opted to use #
as a declarator, a symbol instead of a word like all the other declarators in ES. I submit to you that your aesthetic taste doesn't have the easy mental image that you claim, and is far more costly in the long run than you're anticipating.
Case in point
I'm sorry, I didn't realize you were still proposing that declaration would be private x
, rather than #x
. As I've said before, I think anything which allows you to write
class A {
private a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
}
and have that constructor and method be referring to a public field named a
is absolutely unacceptable. Very nearly identical code in a number of other languages has very different semantics. I think this used to be the first point in the FAQ and it is still the most important one. The confusion this would cause to readers and authors of code is far too high; if we could not find a syntax which avoided this problem, we would not add private fields to the language.
Yes, I know you are proposing that access would be this#.a
rather than this.a
. But your proposal still allows you to write the above code, and has it mean the wrong thing. It does not matter that there is a different, more correct thing which could have been written instead as long as this code does not error and does not have the semantics of accessing the private field, which it could not.
I think we've covered this particular point several times by now; I don't know what else I can say on the particular question of using private x
for declarations.
While I understand that, I find it hard to be sympathetic to that when the result is unnecessarily limiting, functionally disparaging, damages possibilities for future expansion, and so very easy to remedy.
We disagree about the weight of the costs of the these things and of your proposed remedy. I'm not sure I see a way we can resolve this disagreement.
With enough time, the TC39 board will want to make it a language feature.
OK, this is a sidebar, but - as I've said before, there are a great many possible kinds of access modifiers beyond "public", "private", and "protected". I don't buy the argument that we'll definitely want exactly those three but never any others and so we must provide access modifiers using exactly the three keywords we happen to have reserved (or two, with the default being public
).
Separately, as I’ve said many times, a hill i will die on is that a “protected” keyword that does not actually protect anything will not be a part of the language; the concept of “protected” in all other languages that have it is badly misnamed, and we should not adopt it.
@ljharb We've already had that discussion, and I think we both agree that it is badly named since it doesn't protect anything in any language it is used in. That doesn't mean that you should throw your life away to stop the concept from getting into ES where it is already both highly desired and extremely useful pattern for those who write API's for use by other developers.
I hate to predict a future that will involve you being marched over, but the concept of protected
will become a very high priority among developers using class
within a short amount of time after they've adopted whatever form of private
gets added. If you're willing to die on a hill so fruitlessly, that is your choice. Whether it's protected
, friend
, internal
, or something else entirely, the need to selectively share private methods and fields will be demanded. Good luck avoiding that.
@bakkot
I'm sorry, I didn't realize you were still proposing that declaration would be private x, rather than #x. As I've said before, I think anything which allows you to write (example omitted), and have that constructor and method be referring to a public field named a is absolutely unacceptable.
So we agree on that issue. Your example is wrong for what I've proposed. Here it is corrected:
class A {
private a;
constructor() {
this#.a = 1;
}
m() {
if (this.a === this.#a) {
throw new Error("This should never happen.");
}
return this#.a;
}
}
It can't be denied that private
is what nearly every developer not aware of your proposal will be expecting to be able to use for declaring a private field in a class. You've given a seemingly reasonable aesthetic argument against it, namely:
Very nearly identical code in a number of other languages has very different semantics.
To this I reply "True, but so what? That's just a straw-man argument." Every borrowed concept in ES resembles something present in another language but with very different semantics. Will running i = Integer(5);
in Java return you a primitive 5? No, but running i = Number(5);
in ES will. Will running this.foo()
in the member function of a base class work if foo()
only exists in the derived class that this
is an instance of? Nope, but it works just fine in ES. I could go on for hours listing borrowed concepts that are conceptually the same in ES but semantically very different. The use of private
would be no exception to this, and contrary to this baseless assertion of yours:
The confusion this would cause to readers and authors of code is far too high; if we could not find a syntax which avoided this problem, we would not add private fields to the language.
few if any who would choose to use this feature would be confused by the resulting functionality. In fact, based on the user testing I've done, exactly the opposite is the case. #
takes just a bit more explaining since the testers were unfamiliar with it, but private
was perfectly clear as to what was meant. Have you done any parallel testing of your own to verify your assertions?
Yes, I know you are proposing that access would be this#.a rather than this.a. But your proposal still allows you to write the above code, and has it mean the wrong thing. It does not matter that there is a different, more correct thing which could have been written instead as long as this code does not error and does not have the semantics of accessing the private field, which it could not.
Sadly, you have a good point. Someone making that mistake would still have functional code, but would have leaked their private data. I hate to tell you this, but I still have to say "so what?" to that. Your approach allows the same thing! I get that you don't think so, but don't assert it if you haven't tested it. I'm not saying anything to you that I haven't thoroughly tested with uninitiated developers.
class A {
#a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
}
To a novice programmer, the above code would look like it's supposed to work, especially if they've been told that they "need to use #
to declare a private field." Since var #field;
is an immediate SyntaxError, they're likely to assume that #
is a declarator, not part of the name. This is one place where our approaches differ. With your approach, the developer must think of #
as both:
This is already a higher mental load than my approach, which only requires that the developer remember to use #
on the owning object to access any private field. Another disadvantage for your approach looks like this:
class A {
#a;
c;
constructor() {
this.#a = 1; //works
this.c = 2; //works
this.b = 3; //works
this.#d = 4; //fails
}
m() {
return this.a;
}
}
If #
is to be thought of as a name character, even if it is only valid within classes, it only makes sense that it would be something dynamically addable just like public properties. That means not doing this becomes another thing developers will have to be taught and remember. Sure, the same thing might need to be taught to novices for my approach, but since private
is already a well known concept, the lack of extensibility in the private container should come as no surprise. Look at the difference in what must be taught:
#
must be present in the class
declaration (counter intuitive).Mine: the private field container of a class
is not extensible (well known fact).
While your approach has the advantage of apparent naming symmetry, it comes at the cost of breaking the actual naming and access symmetry currently a long-standing and highly-valued part of ES. Trading in something real for something fake is generally a bad thing.
We disagree about the weight of the costs of the these things and of your proposed remedy. I'm not sure I see a way we can resolve this disagreement.
I do, and it's not hard at all. You just need to prove your assertions to yourself. Find someone you haven't already indoctrinated and write a small piece of sample code using both your approach and mine. Don't label them. Let them ask you whatever questions pop into their heads. Answer without bias. Describe any language features that would be expected to work but won't for each approach without mentioning what the other approach does to mitigate that issue. Ask your test subjects to choose the better of the 2. Ask for explanations.
From this you'll at least gain backing for your assertions. I've already done this many times and have only found 2 developers that preferred your approach to mine. I would have far less issue with this proposal if the majority of developers would say they prefer your approach.
So we agree on that issue. Your example is wrong for what I've proposed. Here it is corrected:
As I said:
Yes, I know you are proposing that access would be this#.a
rather than this.a
. But your proposal still allows you to write the above code, and has it mean the wrong thing. It does not matter that there is a different, more correct thing which could have been written instead as long as this code does not error and does not have the semantics of accessing the private field, which it could not.
I don't know how else I can say this.
Will running
i = Integer(5)
; in Java return you a primitive 5?
This is mostly an aside, but that code is not valid Java at all. There's an important difference "works, with different semantics" and "does not work".
contrary to this baseless assertion of yours
"Far too high" is not really a claim which can be baseless. We agree there's a risk for confusion. You think the level of risk is acceptable. I do not. But there's no objective standard for "acceptable".
To a novice programmer, the above code would look like it's supposed to work, especially if they've been told that they "need to use # to declare a private field."
Yes, which is why instead we say "to make a field private, begin its name with #
".
But I agree this mistake is possible for both possible syntaxes. It's just that it will be much, much more common with yours. This code:
class A {
#a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
}
does not look nearly as much like other languages as this code:
class A {
private a;
constructor() {
this.a = 1;
}
m() {
return this.a;
}
}
and people are consequently much less likely to assume they know what it means. These differences in risk are important.
Trading in something real for something fake is generally a bad thing.
We disagree about which concerns count as "real", I think.
Find someone you haven't already indoctrinated and write a small piece of sample code using both your approach and mine.
To be clear, I've talked to dozens of people about dozens of syntax variations over the last several years without presenting my own opinions, including several which included private x
for declarations and which had something other than this.x
for access. I don't think I've ever brought up yours in particular, but yours has the same flaw as several others which I have, which is that it allows the code in my previous comment. People who have written code in other languages with class
and private
have, in my experience, generally agreed that this flaw was fatal as soon as the possibility was pointed out to them.
But in any case this approach could not resolve the disagreement, because it is a disagreement about which things we value.
@bakkot That was beautiful! You managed to cherry pick against a straw-man argument and miss the point entirely.
Very nearly identical code in a number of other languages has very different semantics.
Will running i = Integer(5); in Java return you a primitive 5?
This is mostly an aside, but that code is not valid Java at all. There's an important difference "works, with different semantics" and "does not work".
The point you missed is that the only reason it works at all in ES is because the semantics of SomeType(val)
are different between Java and ES. For Java, SomeType
is a well-defined type structure with a function that is automatically called when this notation is used. For ES SomeType
is just a function. What matters is that the concept of types was ported from other languages into ES, but the semantics were altered to accommodate the nature of ES.... and the language suffered no loss for it. Likewise, if ES absorbs the concept of private
and uses the private
keyword, but has to alter the semantics a bit to make it fit the language, ES developers who want the feature will feel no loss due to the semantics change.
But there's no objective standard for "acceptable".
Per domain, a standard for "acceptable" can be defined. Problem is, none has been defined for the domain of "acceptable level of confusion due to syntactic similarity." Even that is beside the point I was trying to make. My point was that the possibility that someone will forget to include the #
rises with code complexity. Neither syntax has any ability to mitigate that, and both syntaxes will suffer because of it. It's just the natural cost of trying to enforce undetectability.
And by the way, when you make a claim of this form: "X is far too Y to justify Z.", you automatically imply that you have some basis from which to measure X, Y, and Z. That's just the way the language works. By stating what you did in your previous post, you confirmed my claim of "baseless".
Yes, which is why instead we say "to make a field private, begin its name with #".
But I agree this mistake is possible for both possible syntaxes. It's just that it will be much, much more common with yours.
Again a baseless assumption. Your syntax can be taught with the following 2 rules.
obj['#field']
(unintuitive)My syntax also has 2 rules.
private
#
to the owning object name (unintuitive)To this end, I would agree that it would be "more common", but not nearly as much as you seem to want to claim. This is a testable assertion. I encourage you to do so as I have. This time, try using developers who primarily work in ES. I've tested with both groups. Some spotted the issue without it being pointed out, but in the end, they still preferred the syntax I suggest.
@bakkot you know, I've decided to agree with you in that we need to agree to disagree on the value of this issue. Don't worry about replying to my previous post. My next post will be a solution to the problem that works regardless of who's syntax gets chosen. I am hoping that, assuming this solution is amicable to you, that then you will be willing to reconsider your position.
@bakkot I'm thinking that if we can do something to significantly reduce the likelihood that someone will code access to a public field when they meant to access a private field by the same name, then your rather persistent desire to (imho)break the language as a countermeasure should subside. Am I wrong on this?
Assuming I'm not, I'm thinking the problem comes down to the issue of dynamically adding properties to class
instances. If it was acceptable to simply seal object instances after construction, then the entire issue would become moot and this.x
would be valid for private fields. But I've already accept that this is not even going to be remotely considered.
However, the principle behind that simple suggestion still remains. Here's the real idea:
.
notation that was not part of the class
declaration if the declaration includes private fields.For example:
class Ex1 {
constructor() {
this.x = 2; //works
}
}
class Ex2 {
#zed = 1;
constructor() {
this.x = 2; //SyntaxError
}
}
class Ex3 {
#zed = 1;
x;
constructor() {
this.x = 2; //works
}
}
class Ex4 {
#zed = 1;
constructor() {
this['x'] = 2; //works
}
}
The reason Ex4 works is because using []
notation doesn't suffer from the same historical use issue that .
notation does, so I'm thinking it's ok to exclude it. However, I'm not against making that a SyntaxError as well. It would certainly be more consistent to do so. However, I can imagine that there are scenarios where not being able to iterate through appended public properties would be disruptive.
Functions added to either the instance or the class
prototype beyond the class
definition won't be subject to this restriction. This makes sense given that they also won't have access to private fields. This idea means that for the example code you gave before:
class A {
private a;
constructor() {
this.a = 1; //SyntaxError
}
m() {
return this.a; //SyntaxError
}
}
Does this reasonably mitigate the issue for you?
Just in case it must be fully stated:
class
declaring private fields and is not affected by the prototype chain.@rdking, I don't think your proposed mitigation is sufficient. Consider operating on a parameter.
class A {
private a;
getA(param) {
return param.a; // is this dereferencing a private field?
}
}
Your proposed dereferencing syntax is identical for public and private fields, so the intent of the class author is lost. Maybe the author was intending to dereference a public field a
on another class of object B
. But now any user can now abuse the API to access A
's private a
.
There's no way to lock down the properties accessed on method parameters because JavaScript does not know the type of the parameter. It might be homogeneous. It might not.
The FAQ addresses this here.
@robpalme Thanks for the input, and good question.
To answer your question, I considered that scenario in my wording. If param is an instance of A, then return param.a becomes an Error. Thinking about it more, I should have said ReferenceError instead of SyntaxError since this is something that can only be evaluated at the time the .
operator is running.
Your proposed dereferencing syntax is identical for public and private fields...
Not true. The syntax I'm proposing for accessing private fields still includes the #
. The difference is that #
is a binary operator instead of a name character, and its effect is to retrieve the private container owned by the LValue. The RValue for this operator is always an access operator('.' or '[]'). If that seems awkward, an equivalent way of saying it is that #
is a postfix operator, and it's a SyntaxError if you don't immediately follow it with an access operation. So it's just obj#.x
for my suggestion instead of obj.#x
as in the current proposal.
Let me restate the rules for this idea:
class
declaration to use .
notation to access anything on an instance of the same class
that was not part of the class
declaration if the declaration includes private fields.class
also declaring private fields and is not affected by private fields occurring on class
definitions appearing in the prototype chain.class
.So by using a private field, I’ve denied myself direct access to a public field of the same name? I’m not sure why that tradeoff is an improvement.
@ljharb Incorrect. You still get access to a public field of the same name, but only if it is explicitly declared as field of the class
or one of its ancestors. This only prevents declared member functions from being able to dynamically access properties on the object using '.' notation. Here's a (hopefully) clarifying example:
class Example {
#counter = 0;
constructor() {
//this.otherField = true; //Would cause a ReferenceError
this['otherField'] = true; //Works
}
getCustomField(field) {
++this.#counter;
/*The line below is still a ReferenceError even though this.otherField exists. */
//if (this.otherField)
if (this['otherField'])
return this[field];
}
}
var ex = new Example();
if (ex.otherField) {
ex.bar = "foo";
}
ex.bar === ex.getCustomField('bar'); //returns true
The net effect is that regardless of notational symmetry, most cases of accidental public access would be caught and flagged as a ReferenceError.
@borela That was originally dismissed as being less clear than .#
, and a potential ASI hazard. It's in the FAQ. While that notation has the advantage of using #
as an operator, it still fails to support symmetry with []
. Put another way: if #
is the private version of .
, then what's the private version of []
? You could try #[]
but that would have the same feel as .[]
, which is illegal. Not an insurmountable problem, though.
Forcing bracket access to a normal property from inside the class is also unacceptable, and would violate almost every common styleguide.
You missed something again. Look at the Ex3 example I gave before. If you want to use .
notation to access a public field, just make sure to declare that field as part of the class
definition. Basically, I'm saying that if the issue of accidentally accessing public fields when you meant to access private fields is such a big deal, then don't allow developers to be sloppy in the presence of private fields.
Thanks, i think i understand what you mean now.
What about inherited properties? If i declare one, it becomes an own property, and shadows the inherited one - so i can’t have a private field “length” and also use an inherited “length” property via dot access?
Are you talking about this case?
class Base {
constructor() {
this.pi = Math.PI;
}
}
class Derived extends Base {
#circumference;
constructor() {
super();
this.#circumference = 2 * this.pi; //Does this work or not?
}
}
class Derived2 extends Base {
#circumference;
pi;
constructor() {
super();
this.#circumference = 2 * this.pi; //returns NaN
}
}
If so, then even though it means that code may have to be rewritten (which will likely be the case anyway) I'd say it shouldn't work. Base
adding pi
that way is the same as if it had been done external to the class
. Derived has no way of knowing it's there.
... If not, then were you referring to this?
class Base {
pi;
constructor() {
this.pi = Math.PI;
}
}
class Derived extends Base {
#circumference;
constructor() {
super();
this.#circumference = 2 * this.pi; //Does this work or not?
}
}
This works as expected. Derived
can see that Base
has a pi
pubic field, so that field can be accessed.
-- Edit note: forgot about super()....
There's 1 other case that I find bothersome.
function Base() {
this.pi = Math.PI;
}
class Derived extends Base {
#circumference;
constructor() {
super();
this.#circumference = 2 * this.pi; //Does this work or not?
}
}
In this case, since Base
is not a class
, I would want to rely on the prototype. That means this would fail since pi
is not in Base.prototype
.
It’s tenable to force the author of a subclass to make changes when adding a private field; but not to force the superclass to be rewritten. Whether the property is on the prototype or not is irrelevant, because member access in JavaScript walks up the prototype chain.
Whether the property is on the prototype or not is irrelevant, because member access in JavaScript walks up the prototype chain.
For some reason I was forgetting to take into account that members added to an instance by base ancestor methods are own properties of the instance, so the bothersome case isn't a concern at all. So all that needs to be done is ensure that there exists a public declaration for each public field accessed via .
notation from within the methods of a class
that declares private fields.
It’s tenable to force the author of a subclass to make changes when adding a private field; but not to force the superclass to be rewritten.
We agree on this. So is there a use case I'm not considering that makes this a major concern?
A public property “foo” only on a parent class’ prototype (installed later, perhaps, since syntax can’t give you this kind of benefit about other code) - inside the child class instance, I’d be unable to access it with this.foo
?
I'm assuming that the child class
has a private field, because if it doesn't, then there's no issue at all.
foo
on the parent, you'd get a ReferenceError because you'd be trying to create a public field on the instance.foo
on the parent, there's no issue since foo
is defined by the prototype chain.I’m talking about random code anywhere else doing Parent.prototype.foo = 42
. This is JavaScript, that still has to work fine.
My comments stand as-is. As soon as Parent.prototype.foo = 42;
is run from wherever, child_instance.someFuncAccessingFoo()
will no longer receive a reference error due to use of this.foo
.
It is entirely reasonable for a class to intentionally add properties to things after they've been constructed. I do not think it is acceptable for it to lose the ability to do this with the standard syntax just because it introduces a private field.
@bakkot I agree. So how is that a problem for my suggestion? Can you give an example where it is reasonable for a class
to want to add properties accessible via .
notation that cannot be declared from the beginning? I get the notion of setting values after an instance has been created, but I cannot yet say that this also makes sense for setting properties.
In general, the source code for a class is fixed. If you redeclare it, it's not the same class. If you modify it, the new additions won't have access to private fields. So all the fields a class will access on itself via .
notation must already be contained in the declaration. Otherwise the names have to come from outside sources. In that case, you'd be using []
notation anyway.
Did I miss something?
class Record {
private #id;
constructor(id, data) {
this.#id = id;
Object.assign(this, data);
}
getPrivateID() {
return this.#id;
}
getID() {
return this.id;
}
get(key) {
return this[key];
}
}
Modifying that code to match your proposal, what does const r = new Record('a', { id: 'b' }); [r.getPrivateID(), r.getID(), r.get('id')]
return? I'd expect ['a', 'b', 'b']
.
It throws a ReferenceError because r.getID()
tried to access the undeclared this.id
. It should have been this:
class Record {
private #id;
id;
constructor(id, data) {
this.#id = id;
Object.assign(this, data);
}
getPrivateID() {
return this.#id;
}
getID() {
return this.id;
}
get(key) {
return this[key];
}
}
This would do as you want. I'm thinking that is is not unreasonable to require the public declaration of id
when you obviously knew you were going to use that field when you wrote this.id
. There's no chance of ambiguity when using .
notation to access a field. So there's no harm in requiring it either.
I think that it is unreasonable for the language to require explicit declaration of public properties in a dynamic language like JavaScript, under any circumstances.
Then there is also either:
Private properties are being billed by this proposal as if they are non-own properties of an object. ES currently doesn't allow duplication of property names on a single object. If you're going to claim that private properties are not properties of the object from which they can be accessed, then they are logically properties of some other object. Therefore, by your own reasoning, it is unreasonable for the language to require explicit declaration. If, however, they can indeed be thought of as non-own properties of the object from which they can be accessed, then their names must not conflict with any other property accessible from that same object.
You can't logically have your cake and eat it too.
I'm not sure what logic you're using to say that something is not an own property, therefore it can only be a property of another object - variables aren't own properties either.
I don't find it unreasonable to require explicit declaration for private properties - to require it for normal properties, however, does not match the idioms of the language since its inception.
...variables aren't own properties either.
Bag the straw-man argument please. If you have to access it by preceding it with a variable name and a .
, then it is a property of something. Therefore a private field is a property of something. That something is either:
Please understand that this is the mental model this proposal must align with lest it require every ES developer to alter their understanding of the language in a very peculiar way.
...to require ( explicit declaration) for normal properties, however, does not match the idioms of the language since its inception.
Doesn't disagree with it either. What I'm trying to get you to see is that this is a whole new scenario that has never been present in this language. Explicit declaration of properties of any kind has never been frowned upon in the language, but also has never been required, so you're right there. However, it has always been invalid for an object to contain 2 different fields by the same name. But that's in this proposal.
Before we can go any further, these 2 questions need to be answered definitively.
#
an operator or part of the [[IdentifierName]]
?Depending on how you answer these, what is logically reasonable will change.
Is the # an operator or part of the [[IdentifierName]]?
Neither.
Is a private field a non-own property of the accessing object?
These terms are not sufficiently well defined for there to be a meaningful answer to this question.
Is the # an operator or part of the [[IdentifierName]]?
Neither.
Then what is it?
Is a private field a non-own property of the accessing object?
These terms are not sufficiently well defined for there to be a meaningful answer to this question.
If an "own property" of accessing object obj
has a name name
that satisfies obj.hasOwnProperty("name")===true
and is accessible via at least one of obj.name
or obj['name']
, then for the same object, a "non-own" property's name fails to satisfy the hasOwnProperty
condition. Clear enough? Please answer question 2.
Then what is it?
A new kind of thing.
Clear enough?
No, I don't know what you mean by "property". I don't think the spec is ambiguous about the semantics here; I'm not sure why you want me to answer this for you, rather than deriving it yourself from the existing semantics based on your own definitions.
No, I don't know what you mean by "property".
Alright. I hope you're not offended by being compared to Bill Clinton ("That depends on what the definition of is is."). If you don't know what I mean by property, then you're probably not an ES developer and probably shouldn't be making a proposal. But, humoring your (hopefully) feigned ignorance, in the expression obj.x
, x
is a property of obj
. Use the definition of property nearly everyone using ES-based languages use. Please answer question 2.
Then what is (#)?
A new kind of thing.
Please stop evading the question. Every language is composed of a set of component categories in which each token is exclusively categorized. What is the sigil? Is it one of these?
If not, then what exactly is it? Currently everything in the syntax for ES falls into these categories exclusively with varying degrees of overloading. There is nothing in ES that spans multiple of these categories. In fact, you'd be hard pressed to find anything in any programming language that spans multiple categories. So once again what is the #
?
@rdking I’ll remind you that our repos operate under a Code of Conduct - please remain respectful even if you disagree with something that’s been said.
Use the definition of property nearly everyone using ES-based languages use.
Such definitions tend to be provided by reference to existing things, just as you've done. In those cases, when there's a new kind of thing introduced which shares some characteristics with existing things which we would consider to be examples of X and which lack other such characteristics, there is no answer to the question "is this new thing an X?".
If not, then what exactly is it?
Like I said, it's a new kind of thing.
Currently everything in the syntax for ES falls into these categories exclusively with varying degrees of overloading
I don't think this is so. ...
, {
, (
, =>
, .
- none of these fall cleanly into the above categories, unless you're defining them in an unusual way.
Anyway, I don't think this line of discussion is likely to be productive. I'm going to bow out now. Apologies.
@ljharb Fair enough. I thought that one might skirt the line. I'm just having an infuriatingly frustrating time trying to understand why he doesn't wish to provide a clear and definitive answer to the questions I've posed. Even if the #
is "something new", he should be able to categorize that something. If the use of the #
is something he is incapable of categorizing, then that is a serious failing for this proposal.
@bakkot Please tell me you're not being serious.
...
: Spread operator{}
: Object/block literal()
: Call/group operator=>
: Arrow function literal.
: Property access operatorYou left one out:
[]
: Array literal/Property access operatorThis is the only one in the language that can be placed under 2 different categories. However, the category that it can be placed in is based on the context, and those contexts are distinct. Your #
doesn't have such a distinction because the contexts overlap. This is problematic.
So please, instead of just saying "it's a new kind of thing", please either categorize it, or admit that you cannot. If you cannot then this proposal has a serious issue that I hope the TC39 board isn't willing to just gloss over. I care not if it's one of the 6 I listed before or something created specifically for this language, but I do care that you can give it a clear and categorical description.
As for the 2nd question, I'm going to give this one more try. This one is mostly from the ECMAScript specification.
Properties - containers that hold other objects, primitive values, or functions and collectively comprise an object.
If this definition isn't satisfactory for you to provide an answer, then please provide your own definition and answer with respect to that.
@bakkot Can I help you with this a little?
If we have to keep your proposal as is, then I suggest you declare #
to be a literal token. The only way at all that I can justify the way you're using it, is to treat #
the same way as =>
. In this way you could call it a "private field literal", meaning only that it must appear wherever a private field appears. It would have no meaning beyond that. That means it only exists to disambiguate private field syntax from all other similar syntaxes.
Is this even close to the "it's a new kind of thing" you kept saying?
@nicolo-ribaudo I'm putting part of my response to your post here instead...
As for what # is, I think that "# is a sigil which introduces the name of a private property" could be a good definition.
Not really. The definition you gave satisfies its use as a literal in a declaration, but the #
is also being used in access notation. If you had instead said "creates/retrieves(introduces)" instead of "introduces", that would have worked in both cases. The word "introduce" doesn't really fit when the thing being introduced doesn't yet exist. Even with the suggestion I've given, it's a still a basically a literal as described in my previous post.
Thanks for the comments above. We'd welcome more documentation in the FAQ to clarify the points above. In the end, we're moving forward with the #
-based syntax and strong encapsulation boundaries of the current proposal. We still welcome clarifying questions, small tweaks, and documentation in this repository. We've thought about various larger changes, with the help of the community including the discussion in this repo, and decided to stick with the current proposal. For more details, see the README's Status section.
@littledan you just dropped all alternatives without proper reasoning. It's the worst decision you could made...
@lgmat There's lots of reasoning in the responses within these threads. I don't agree with all of the arguments in favor the current approach, but overall I don't really see how we could move to one of the alternatives.
First, don't join in on this conversation unless you've read
The FAQ
This thread is to be a discussion on how the considerations of the FAQ affect not only the usability of the language should this proposal become standard, but also how it affects the future extensibility of the language. If you have issues with this proposal that come about because of the FAQ, and you can support that with reasonable examples and use cases, or if you have counter-arguments in support of the FAQ, also with reasonable examples and use cases, please join in.