Closed trusktr closed 5 years ago
Nested properties using one of the suggested syntaxes from above:
// foo is a public property, also instance of this.constructor:
this.foo.publicMethod()
this.foo['public'+'Method']()
this.foo##.privateMethod()
this.foo##['private'+'Method']()
this.foo#.protectedMethod()
this.foo#['protected'+'Method']()
// foo is a protected property, also instance of this.constructor:
this#.foo.publicMethod()
this#.foo['public'+'Method']()
this#.foo##.privateMethod()
this#.foo##['private'+'Method']()
this#.foo#.protectedMethod()
this#.foo#['protected'+'Method']()
// foo is a private property, also instance of this.constructor:
this##.foo.publicMethod()
this##.foo['public'+'Method']()
this##.foo##.privateMethod()
this##.foo##['private'+'Method']()
this##.foo#.protectedMethod()
this##.foo#['protected'+'Method']()
Neste properties using the proposal syntax (plus string access):
// foo is a public property, also instance of this.constructor:
this.foo.publicMethod()
this.foo['public'+'Method']()
this.foo.##privateMethod()
this.foo[##'private'+'Method']()
this.foo.#protectedMethod()
this.foo[#'protected'+'Method']()
// foo is a protected property, also instance of this.constructor:
this.#foo.publicMethod()
this.#foo['public'+'Method']()
this.#foo.##privateMethod()
this.#foo[##'private'+'Method']()
this.#foo.#protectedMethod()
this.#foo[#'protected'+'Method']()
// foo is a private property, also instance of this.constructor:
this.##foo.publicMethod()
this.##foo['public'+'Method']()
this.##foo.##privateMethod()
this.##foo[##'private'+'Method']()
this.##foo.#protectedMethod()
this.##foo[#'protected'+'Method']()
Personally I like obj#.foo
and obj##.foo
where obj#
and obj##
are like objects, because it means we could provide a way to leak protected or private members selectively to other code.
For example, suppose that obj##
means "a reference to obj
in private access mode", and then we can leak it on purpose, so that we have the choice of hard privacy or soft privacy:
// some-module.js
// a "module private" or "module protected" pattern
const privates = new WeakMap
export class Foo {
##foo = 'foo'
constructor() {
privates.set(this, this##)
}
}
// another class in the same module
export class Bar {
constructor() {
let f = new Foo
console.log(privates.get(f).foo)
console.log(privates.get(f)['f'+'oo'])
}
}
import Bar from './some-module'
new Bar
// Output:
// "foo"
// "foo"
Notice that because we use the obj
/obj#
/obj##
syntax, and that therefore .
and []
work like before, we can pass around reference that are in different "access modes" and access props with .
or []
.
The engine would need the new concept of an "access mode" associated with a single reference. Some things (among other things) we'd have to consider:
class Obj {
foo = 1
#bar = 2
##baz = 3
test() {
// same instance
console.assert(this instanceof Obj)
console.assert(this# instanceof Obj)
console.assert(this## instanceof Obj)
// they're all the same object, just that the references are in different "access modes":
console.assert(this === this)
console.assert(this === this#)
console.assert(this === this##)
// enumeration, etc, is based on access mode:
console.log(Object.keys(this)) // ["foo"]
console.log(Object.keys(this#)) // ["bar"]
console.log(Object.keys(this##)) // ["baz"]
// get descriptors, like we already know! Access mode filters them internally
console.log(Object.getOwnPropertyDescriptors(this)) // { foo: { ... } }
console.log(Object.getOwnPropertyDescriptors(this#)) // { bar: { ... } }
console.log(Object.getOwnPropertyDescriptors(this##)) // { baz: { ... } }
// set descriptors, like we already know!
Object.setOwnPropertyDescriptor(this, 'foo', {...})
Object.setOwnPropertyDescriptor(this#, 'bar', {...})
Object.setOwnPropertyDescriptor(this##, 'baz', {...})
// use JavaScript like we already know!
}
}
let o = new Obj
o.test()
I think it is not just easy to reason about it this way, but also just easy to work this syntax with all of today's existing tools.
class Foo {
// creates a accessor descriptor with get/set, just like normal.
// Only access mode determines whether or not you are allowed to interact with
// the foo property, which is tangential to how descriptors work.
get #foo() { ... }
set #foo() { ... }
##bar = "bar"
test() {
// access mode is tangential to concepts we already know:
console.log(typeof this) // "object"
console.log(typeof this#) // "object"
console.log(typeof this##) // "object"
console.log('bar: ', this##.bar) // bar: bar
}
}
const f = new Foo
// We'd may have to bikeshed some things, but it is totally doable:
// the same as in the methods? Seems to make sense. They're the same object.
console.log(typeof f) // object
console.log(typeof f#) // object
console.log(typeof f##) // object
f.foo = 1 // works
f#.foo = 2 // Throws an Error, something like "can not gain protected access outside of a class"?
f##.foo = 2 // Throws an Error, something like "can not gain private access outside of a class"?
// ^ those errors do not leak information about which properties exist.
class Bar extends Foo {
test() {
console.log(typeof this) // object
console.log(typeof this#) // object
console.log(typeof this##) // object
this.foo = 'foo' // it works, new public prop
this#.foo = 'foo' // it works, inherited protected setter used
this##.foo = 'foo' // it works, and is a private property in Bar scope
super.test()
this##.bar = 'lorem'
console.log('bar: ', this##.bar) // bar: lorem
}
}
Notice the output with respect to bar
is:
bar: bar
bar: lorem
because there is one bar in each private scope (hard privacy in that we can not detect private variable by detecting errors setting them, so setting private variables always works, and sets them in the current class scope).
Doing it this way becomes about "permission to access protected or private scope depending on which symbol is used on an object reference".
Something else to bike shed:
Sense muuuuuuch it makes, me thinks!
We'd have to get into more details eventually, but from a top level perspective I like that with this approach I can work with objects just like before.
import _ as 'lodash'
class Foo {
##foo = 3
##bar = 4
test() {
console.log(_.pick(this, [ 'bar' ])) // { bar: 4 }
_.assign(this##, { baz: 'baz' })
console.log(this##.baz) // "baz"
}
}
This also means opportunities to inspect access mode.
// ...
let ref = this
console.log(Reflect.accessMode(ref)) // "public"
ref = this#
console.log(Reflect.accessMode(ref)) // "protected"
ref = this##
console.log(Reflect.accessMode(ref)) // "private"
Access mode also dictates access mode along the prototype chain during lookup, so:
class
es are in an object's class hierarchy by some method other than looking at prototypes because we can change the prototypes of objects created from class
es after they've been constructed.Maybe the engine needs to internally mark each property as "protected" or "private", to look it up during access. Public props don't need any internal value. But although I've no experience even looking at engine code, this "access mode" idea doesn't seem too hard.
A special case: super#
and super##
are treated like super
, not like normal object references; you can't assign them to variables, etc. f.e.
class Foo {
constructor() {
let s = super# // Uncaught SyntaxError: 'super#' keyword unexpected here
}
}
As I’ve stated on other issues; i don’t think the concept of “access levels” is appropriate for javascript. “protected” is a poor choice of name, since it doesn’t actually protect anything (since anything can get access to protected members by temporarily subclassing, and extracting methods).
There’s no way in JS I’m aware of to robustly share access at any future time (ie, to later-created subclasses) without using lexical scope - so I’m not clear on what problem language support for “protected” would solve.
It’s clear that syntax space can be created for it - but not that there’s value in doing so. See #86 for example (your own issue) and many similar issues/comments on this repo.
I think this question is answered both by the several previous threads where you raised this issue, the decorator future path and by @ljharb's post.
since anything can get access to protected members by temporarily subclassing, and extracting methods)
that's not what protected is for. If I give you a reference, you can't call the methods from public space. that's what protected is for. I'm not as much concerned about people grabbing the method source.
I'm confused; a function has the same behavior no matter where it's invoked from.
without using lexical scope - so I’m not clear on what problem language support for “protected” would solve.
I just described. ^
There’s no way in JS I’m aware of to robustly share access at any future time (ie, to later-created subclasses) without using lexical scope
Yeah there is. If I can imagine it, others can too!
obj##
It's not hard to imagine...
I believe there will be strong opposition to ever making a first-class function's call behave differently based on where it's invoked (as opposed to where/how it's defined).
a function has the same behavior no matter where it's invoked from.
yes, but protected/private access will only work on this class with the statically created hierachry slot mentioned in my previous comment (i.e. created during class definition and given to objects at [[construct]])
It's like super
: you can borrow a method, but you ain't gonna change what super
refers to!
@ljharb I just want to mention again because you have stated this before.
(since anything can get access to protected members by temporarily subclassing, and extracting methods).
The final keyword would be used on the class to prevent further subclassing.
edited typos in my previous comment
@shannon thanks, that's a useful clarification - so how would a parent class that provides "protected" data ensure that child classes (that extend the parent long after the parent is finished evaluating) are final
?
@trusktr i see, so you're saying i couldn't extract a protected method from the bottom of one chain and .call it on the bottom of another chain?
plus what Shannon said, and I've also mentioned in the "other threads" how to implement final
at runtime in ES6.
@ljharb
how would a parent class that provides "protected" data ensure that child classes (that extend the parent long after the parent is finished evaluating) are final?
It wouldn't. As a developer I would only export final classes. Protected would be internal to library/module. I could be mistaken, but I believe that is the point of protected. To avoid redundancy, and allow you to keep some shared data between subclasses without exposing it to your public API.
so how would a parent class that provides "protected" data ensure that child classes (that extend the parent long after the parent is finished evaluating) are final?
they don't, only the child classes define if they are final. This is convenience for library authors.
so you're saying i couldn't extract a protected method from the bottom of one chain and .call it on the bottom of another chain?
Right, .call and .apply won't change the internal classes that the methods operate on.
There could be additional checks in place to throw meaningful errors, f.e. maybe methods using the sigils can only be .called or .applied on objects that match the internal hierarchy. Additionally the scope where the .call and .apply happen can be checked, just like regular method calls, and errors thrown when the scope is wrong. or etc
accessing __proto__##.foo
would work fine too, given it happens in the correct scope. Otherwise, everything is just regular properties.
@shannon mentioned
I could be mistaken, but I believe that is the point of protected. To avoid redundancy, and allow you to keep some shared data between subclasses without exposing it to your public API.
additionally if we can add internal mechanics to save these references in their access modes to variables (let o = obj#
), then we can make "package private
" too (not just "package protected
") where hard private variables of one class can be shared with other select classes.
@ljharb here is a rather simple example:
class Vehicle {
#velocity = { x: 0, y: 0 };
#position = { x: 0, y: 0 };
protected #wheels = 0;
protected #topspeed = 10;
update(acceleratorDown, wheelRotation) {
// caculate new #velocity based on user input
// clamp velocity to #topspeed
// caculate new #position based on #velocity
}
}
class Car extends Vehicle {
#wheels = 4;
}
class SlowCar extends Car {
#topspeed = 1;
}
class FastCar extends Car {
#topspeed = 100;
}
export default final class PlayerCar extends Car {};
export final class PlayerSlowCar extends SlowCar {};
export final class PlayerFastCar extends FastCar {};
We don't want to expose these values to be edited by the player but we do want to keep the code DRY and allow for declarative subclassing. This is almost certainly a desirable thing to do and I don't see why it wouldn't fit within JavaScript. So I'm really not sure why you keep stating that there is no value in doing this. The alternative is closed over variables or weak maps and then we are just right back where we started.
I don't know if other languages do this but to me it would be ideal if final didn't prevent subclassing completely, but just ended the chain for protected members. in other words any further subclassing didn't inherit the protected members but it wouldn't error. I don't know how others feel about that though.
Wouldn't you still be able to do
class FastCarSubclass extends Object.getPrototypeOf(PlayerFastCar) {
readProtectedTopSpeed() {
return this.#topspeed;
}
}
const getTopSpeed = FastCarSubclass.prototype.readProtectedTopSpeed;
var fastCar = new PlayerFastCar();
getTopSpeed.call(fastCar); // 100
Even if the exported constructors are final, the parent constructors are not. They'd have to be made final too somehow, after all of your 3 subclasses have been created.
@loganfsmyth Well, I would expect that using the final keyword would walk the prototype chain and set a final parent constructor. So __proto__
would not work either.
@shannon Wouldn't that mean
final class PlayerCar extends Car {};
would make Car
final meaning that
final class PlayerSlowCar extends SlowCar
would then fail because it was extending a final class?
@loganfsmyth not quite what I meant. It would just set the parent constructor of PlayerCar to a final version of Car. It wouldn't retroactively go and and change all the other classes to final.
Edit: I meant to say PlayerCar in the case of your first example. PlayerSlowCar would have a parent constructor of a final version of SlowCar.
@loganfsmyth I think you meant Object.getPrototypeOf(PlayerFastCar).constructor
, but yeah, good point.
There's always some solution that can be imagined:
constructor
property, and implement your own hasInstance
helper or symbol method for use internally, then external code can't access the constructor.Object.getPrototypeOf(PlayerFastCar).constructor
was ever exported, and if not it can not be extended.private class Base {}
could mean that the class can not be extended outside of the scope where it is defined. The scope can be selectively passed around with a function.Shannon's idea seems perfect; a "final version".
Just like the ideas here with obj##
being references, so too can .constructor
be a special reference of the same function, in "final mode". May as well re-use the reference mode idea!
(I'm in mobile, sorry for typos!)
@shannon If we aren't mutating the existing class, what does making a final version of a class entail? We'd have to have a full copy of the prototype object and all the methods with the new [[HomeObject]]
? Wouldn't that also mean that any property access on that would be different? Say I have
class Car {
static foo = 4;
}
final class PlayerCar extends Car {
increment() {
const parent = Object.getPrototypeOf(PlayerCar);
parent.foo += 1;
return parent.foo;
}
}
is parent
this final copy of Car
? So the Car.foo
is not changed by this code?
@trusktr
I think you meant Object.getPrototypeOf(PlayerFastCar).constructor, but yeah, good point.
Nope, the prototype of the class constructor is the parent class. The .constructor
property of class instances is irrelevant in this context.
@shannon Every single subclass, though, is done after the parent class is finished, whether it's exported or not. I don't see how SlowCar would get access to Car's protected methods without allowing anything extending Car at any time to do the same.
@loganfsmyth
Parent would be the same value as the expression (final class extends Car)
.
So as far as I my understanding Car.foo would be changed by this code. Since it's static and not redeclared.
I would expect it to just walk the chain and the parent class of the class retrieved from the expression (final class extends Car)
would be the expression (final class extends Vehicle)
@ljharb Anything extending Car could. But you would never export Car. And by the logic in the rest of this thread about walking the prototype chain you should not be able to get to it unless you export it. Or export a subclass without the final keyword.
@shannon The original Car
is always accessible from the prototype chain of the exported subclasses, whether the subclass is final
or not.
@ljharb I understand that it currently is this way. But the final keyword should walk the chain and set each parent to a finalized version. Preventing any subclassing of parent classes anywhere in the chain.
@shannon that would mean that making the 5th child, say, final
, would mutate all 4 non-final parents - the effect would be that a given parent class could only ever be in the inheritance chain of one final
class, no?
@ljharb it doesn't need to mutate any of the parents. It just sets its own parent to a final version of it's parent.
Am I not explaining this well enough? I don't know why this is so confusing.
I doesn't need to make any changes to existing classes. It just needs to set it's parent to the expression final class extends Parent
when then does the same and it set it's own parent. This happens recursively down the chain until you end up with final versions of every class in the chain.
Sorry forgot the extends keyword. Trying to explain too fast.
@shannon how would you specify that "version" such that it, also, wouldn't expose the original parent on its prototype chain? Also, wouldn't that break code that depends on the identity of instanceof
, or that compares the prototype chain of two final
subclasses (which should both be instances of the same parent)?
How would that work when someone does expose the original parent - suddenly the final subclass isn't an instance of the original parent?
@ljharb Ok so maybe final class extends Parent
wouldn't be completely equivalent because of the issue you have described. The last parent in the chain would have to be handled specially I guess.
I would expect that comparing two final subclasses to have the same prototype chain because I would wouldn't expect that the engine to actually call that expression for every extension. I was just trying to give a succinct example of what the value would be and distinguish it from mutating the original classes. I would expect that they would be deduped accordingly and they would actually be referring to the same objects.
Instanceof should work as they would actually still be instances of the original parent class. Extending the parent class doesn't change that.
I will need to think on this some more.
Perhaps it would be better (rather than chatting on the class fields repo) if you created your own repo for a proposal for protected. If it could be demonstrated that "protected" (no matter what the name of the feature was) was both viable and idiomatically appropriate for the language, I think it would be a more reasonable time to discuss how it interacts with class fields. At the moment, it's not clear to me that there will ever be a viable proposal to interact with it.
Also, wouldn't that break code that depends on the identity of instanceof, or that compares the prototype chain of two final subclasses (which should both be instances of the same parent)?
With the idea of "reference mode" described above, which is extended from the original idea of "access mode", instanceof
and ===
would work the same. Here's @shannon's idea expressed with my "reference mode" idea:
class Foo {
#protectedProp = 42
}
final class Bar extends Foo {}
console.log( Reflect.isFinal(Foo) ) // false
console.log( Reflect.isFinal(Bar) ) // true
console.log( Reflect.isFinal( Object.getPrototypeOf( Bar ) ) ) // true
// the same reference:
console.log( Foo === Object.getPrototypeOf( Bar ) ) // true
// and instanceof works:
console.log( (new Bar) instanceof Foo ) // true
console.log( (new Bar) instanceof Object.getPrototypeOf( Bar ) ) // true
Basically the reference returned by Object.getPrototypeOf( Bar )
is the same reference to Foo
, but it has a "reference mode" of "final".
Just like above ideas, if it gets assigned to a new variable, the reference mode carries over to the new variable, but doesn't change the reference.
Seems like this allows for very dynamic possibilities, and isn't too complicated to understand.
Perhaps it would be better (rather than chatting on the class fields repo) if you created your own repo for a proposal for protected
This is more of a proposal for changing current private fields proposal (and adding protected while at it), but I can definitely see a separate repo being useful.
Plus, even if the references were not the same (i.e. if we decide the "reference mode" idea is bad, and that we should have different references), then instanceof
should still work fine because the engine can set Symbol.hasInstance
on the classes to whatever it needs to be to make the check work.
(Note that nothing can ever go on Reflect that isn’t a Proxy trap)
js doesn’t have first-class references; and I’d find it very harmful to make ===
return true for Teo observably distinct objects.
Using Symbol.hasInstance is a good answer to instanceof, but everything still has to make sense when someone does export the original parent class constructor - including === when walking the prototype chain.
The alternative is to make references unique then, just like Function.prototype.bind gives a new "version" of the function.
Okay so in this case Symbol.hasInstance will come into play for instanceof.
but everything still has to make sense when someone does export the original parent class constructor - including === when walking the prototype chain.
Can you provide some code samples?
(Note that nothing can ever go on Reflect that isn’t a Proxy trap)
Didn't know that! Thanks for pointing it out. That seems limited in terms of "reflection", but that's another topic.
Personally I'm not confused about the "reference modes" idea. It's also not complicated to learn, though yes it would be a feature unique to JS if it were added, but IMO it leads to some really cool features.
Cool features:
.
and []
operatorsThe current stage 3 proposal of this repo isn't that flexible. For example.
delete
private properties?With the "reference mode" idea, all of that just simply works, and honestly I don't think it would be difficult to learn.
_.pick(obj#, 'a', 'b', 'c') // huge win! (assuming you trust your libs)
@shannon, in your mind, how would the final
concept work if someone writes
import FinalClass from 'somewhere'
new FinalClass.prototype.__proto__.__proto__.__proto__.constructor
?
How do we make that illegal? How do we make all the constructor
properties in the prototype chain be "final versions" outside of the module without affecting the internal classes in the module? Do we copy the prototypes and make a whole new prototype chain for FinalClass
?
With something like "reference modes", we wouldn't have to make a new prototype chain, which means a definitive source (f.e. an code in an author's library) can update the prototype of a single class dynamically and it will reflect wherever the class is used. Prototype objects themselves would have reference modes depending on which class scope they are accessed in.
@rdking Any thoughts? If anyone, I know you'll find the best and worst pros and cons of the idea. :D
If we think of obj === obj#
as comparing the same object (in different access modes), well, they're the same object, so it is true
. Easy to understand.
New helper functions could be useful (this time not on Reflect
as per above comment):
console.log( obj === obj# ) // true
console.log( Object.sameReferenceMode( obj, obj# ) ) // false
console.log( Object.sameReferenceMode( obj#, obj# ) ) // true
console.log( Object.sameReferenceMode( otherObj#, obj# ) ) // false
@trusktr final
is not supposed to affect base classes, only the class it is applied to. Using it in any other way would be both counter-intuitive and destructive. Think about what happens if you declare a branching class hierarchy with even 1 final leaf class. All subsequently subclassed classes branching off of any base class of that tree would fail because their constructors had been marked final
. So for your question above: "That dog don't hunt."
@ljharb Wow! You really dragged them all down a bad road this time. Without realizing it, they'd all but entirely given up the point of "protected"! After trying this discussion with you many times, I've decided a few things are true
This means that you're right. If a derived class decides to share the protected members of its base, then, due to polymorphism, it has effectively shared the same protected members of all subclasses derived from the same base. This, however, is not a bad thing as it does not affect the base class, nor prevent the base class from altering its private API.
Before I get to the point, it's time for that lexicon:
It should be clear that any class is free to leak its private or protected API to its public API. It's should also be clear that a base class has no say in what a derived class does with the protected or public API. As such, the purpose of protected is to provide a non-private API that is not immediately part of the public API. The reason such a thing is wanted is that it gives us a well-defined method of providing 3rd party code with the ability to take advantage of, or even extend our logic without resorting to the more dangerous practices of monkey-patching and API invalidation.
One of the best arguments for "hard private" is the API invalidation problem, where developers are ill-advisedly making use of API surface that was not intended to be public. By requiring subclassing, that grants the developer the freedom of making monkey-patch-like changes to the functionality of an API without preventing the original API's developer from making reasonable updates. Protected essentially grants control a few otherwise private details to the derived class. What the derived class does with that protected API is no more the responsibility of the base class than would be the public API.
However, that does not mean that the protected API is the same as the public API. Their surfaces are still distinct. A leaking derived class would still need to provide methods that leak the protected interface. This is no different than if a given class decided to leak its private data.
@rdking, well said about protected!
What are your thoughts on the "access mode" and "reference mode" ideas specifically?
final is not supposed to affect base classes, only the class it is applied to
Ah, right! So modifying the constructor
property on only FinalClass.prototype
makes much sense because it does not impact base classes. (But then there's also the question of how to keep classes private only to a library, while the end user can only use the "final" class, but it's whole 'nother discussion).
Anywho, the discussion of final
is getting a little further from the main topic of private/protected properties. I believe Symbol.hasInstance
would have final
covered well enough if FinalClass.prototype.constructor
is a unique reference (and the "reference mode" idea would cover it fully if there aren't any other ideas).
@loganfsmyth
getTopSpeed.call(fastCar); // 100
That wouldn't work with the above idea where I mentioned that with APIs like Function.prototype.call/apply
the engine would know in which class scope the APIs are being called in, and can thus throw an error if they are being accessed in invalid class scope (i.e. outside-of-a-class scope).
Sidenote, @littledan, I think you closed this issue too early because I haven't raised this idea before in another thread ("access mode" and "reference mode" ideas), and it might seem as if your opinion matters more than a multitude of community member opinions when an issue is closed quickly, generally speaking.
It would be nice for it to be open (and bonus if we could somehow get JS community members more involved in the discussions).
How can we garner more community involvement?
(NOTE This first post was originally about having both private and protected features with symmetrical syntax, but the next comments evolved into the concept of "access mode" and "reference mode")
Personally I like the more poetic/verbose/meaningfully-clear
private
/protected
keywords, and my editor's autocompletion is fast enough (if my typing speed can't beat it).But if we're going to stick with the
#
sigil, can we at least figure howprotected
will be allowed the possibility of entering the picture later (if it ever does), without it being asymmetrical? (f.e.#
for private andprotected
for protected wouldn't be symmetrical)What about having both
#
and##
?I feel like
#
for protected and##
for private would be nice. The longer one with more symbols is for private becauseprivate
is more hidden thanprotected
.Personally I would be fine losing the
.
symbol when accessing protected or private:For dynamic string access, and alternative could be the
#
symbols go outside:or
But that last one is the most awkward. If we want to preserve the notion of accessing props on an object with
.
, then another possibility is:which seems to be clear:
this
,this#
, andthis##
are all treated like objects, and the.
and[]
patterns stay the same as they have always been.Other objects:
Is it possible to leave space in the spec for
protected
, in case it is added later?