-
Notifications
You must be signed in to change notification settings - Fork 113
[Open discussion] What would be for me the perfect class in js #15
Comments
It's convinced the committee. Have you seen the FAQ? Do you think there's something it doesn't address?
This isn't the right place to discuss those topics. You might be interested in this proposal. That said, we are extremely unlikely to introduce significant breaking changes, especially to the inheritance model. |
Yes, everything read. For me the private proposal is currently more "theorical" but doesn't really solve deeply daily developers problems :
/**
* In this example, the class B is exported but its `attr` is hidden when importing B
**/
class A {
readB(bInstance) {
// class A could perform some complex algorithms on B private members
console.log(bInstance.attr);
}
}
export class B {
friend A
private attr = 'B';
}
class A {
#data;
setData(data) {
// some super slow type checking, data conversion, etc...
// ex:
for(const key in data) { /* whatever */ } // imagining it takes 1s
this.#data = data;
}
} Knowing that When accessing a property, the browser knows the context and could easily determine if the property could be accessed or not. If others languages can do this (and fast), ecmascript can do this too. For me the proposal is not wrong, but don't go enough far on the expectations a developer could have for modifiers. ES6 introduced big features with new syntax, so we should not be afraid. If it solve a problem (and it will) the community will accept it fast (and polyfill/transpillers exists for old browsers). |
Friend classes are really important. My current thought is that this capability would be provided by decorators. There's a hard problem where it would be impossible for two classes to declare themselves to be mutually friends if declaring friends is set up by including a declaration which references the other class, since classes are always defined one after the other. Decorators can get around this by writing the friend information to a third location, wherever they want. |
I wrote a fast POC to demonstrate the feasibility using decorators => I use the Error stack trace to determine if called from a friend class/function. The problem by using decorators instead of native : it's far more slower (~1500 times slower, which result in "only" 50k calls/s). Moreover, the current private proposal doesn't allow to modify on the fly the property access, so they are not editable by decorators.
Well in fact with the new |
I don't think using the error stack is a good idea. Not only is it slow, but it can also be spoofed in strict mode code (e.g., class bodies) because you don't have access to function identity in the V8 stack trace API. It's not clear when "after all classes are defined" is, in a world with eval, dynamic module import, and dynamic script tag insertion. Code is always coming into a JavaScript system. |
Well, it's a POC, so far far from being perfect.
What I mean is : it's the responsibility of the developer to know if the class exists (has been parsed by the browser). If he knows it's fine (ex: in a constructor of a class, after a window.onload for example, class B exists), he could create the private property and the friendship on the fly with |
@claytongulick Well at the moment, the big drawback I see is the fact that we can only access the stack with Error().stack because function.caller is not available into es6 modules :'(. It's far from being perfect. So for a proper polyfill of private/protected decorators the function.caller should be allowed inside modules, properly defined in the specs (because it seems not being a standard), or some kind of stack() function should exsists. |
@lifaon74 I think your proposal for having modifiers on the descriptor makes way more sense than the dynamic run time check via caller or an enhanced Proxy (though I do think adding caller info to Proxy is a worthy endeavor on it's own). It's sort of the best of both worlds there - it's straight forward for library space to manipulate, expandable, and achieves most of the goals that @littledan and @bakkot laid out. One of the problems I'm having is that the justification for
I really don't understand this. Having a private and public member with the same name isn't allowed in other languages, why are we trying to do it? Java and C++ certainly don't allow this. If I have class Foo {
@private //modify the descriptor to make 'a' private
a=0;
}
(new Foo()).a = 5; //why not throw here?
That's how it works now, but the whole point of adding a concept like private implies that this behavior would change. Given that the convention in a lot of OOPy languages is to prefix privates with an underscore anyway, I don't see that causing a problem with naming conflicts. If someone felt really strongly about adding a public to an existing class that has the same name as a private, there's always
This is another point that I don't understand well. class Base {
@private //set access modifer in the descriptor to 'private'
x = 0;
}
class Derived extends Base {
x = 0;
foo() {
x = 3; //spiffy
super.x = 3; //error - the same way trying to do this to a writable: false prop would
}
}
(new Base()).x = 3; //error - throw
(new Derived()).x = 3; //fine and dandy That seems like natural and straight forward behavior, and what I think most folks would expect from classes coming from other languages. If we can't figure out a graceful syntax for privates, I think it's better to wait to add them rather than pulling the trigger on |
@claytongulick, the reason a private and public field of the same name must be allowed, and the reason that This is covered in the FAQ, but to recap: Encapsulation is a core goal of the proposal because other people should not need to know about implementation details of your class, like private fields. (Indeed they should not be able to know about them, without inspecting your source, since otherwise those details are part of your public API and will be depended upon.) For example, if I am a library author providing class Your example cannot work as described, because class Base {
@private
x = 0;
static m(obj) {
return obj.x;
}
}
class Derived extends Base {
x = 1;
}
Base.m(new Derived()); // does this return 0 or 1? How could it know? |
@bakkot that's a great response and really helps me understand your point, the FAQ just sort of says "encapsulation" but doesn't go into much detail (there are several ways to achieve this without I'm no Java guru, but when I do this: public class HelloWorld
{
public static void main(String[] args)
{
OtherClass myObject = new OtherClass("Hello World!");
System.out.print(myObject);
}
}
public class OtherClass
{
private String message;
public String message;
public OtherClass(String input)
{
message = input;
}
public String toString()
{
return message;
}
} I get:
Am I being dense here? Agreed that this would work on a subclass defining a Your example really demonstrates a great point about how that behavior would be currently undefined, but at the risk of gross oversimplification, couldn't we define priority rules to solve that? I.e., check if there's a public property first, if not, check for a protected, if not check for a private, if not, throw... |
Sorry, when I say Java "allow classes to have both a public and private field of the same name", what I really mean is that it allows instances to have a public and private field of the same name, by being instances of both a superclass which a private field and a subclass which has a public field of that name. That's the case the FAQ intends to get at, though in JavaScript it's a little more complicated because code external to a class can and often does add properties to instances of a class without actually subclassing said class.
We could, in theory, though I don't think it would actually solve the problem: if a class has a private field, and writes methods operating on that field, those methods shouldn't break just because a consumer of that class is adding a public field of the same name to instances of the class. But even if it would work, we would strongly prefer to avoid complicating property access semantics. |
@bakkot thanks again, I see your points and I think they are strong. I guess it comes down to whether we're willing to trade a bit of complexity with property access semantics for a cleaner syntax and avoiding a magic character like I'm not sure I have much more from a technical standpoint to contribute, I'm curious how others in the community feel. I do deeply appreciate the time you've spent discussing this with me!! |
@claytongulick I fact, the "private" field with # are "class scoped variable" it means the property can only be accessed from the context of the class (it's some king of the exact same behavior than WeakMap with classes). What I reproche is : that it's a different meaning of private keyword from other languages (maybe the name should be changed as "class scoped properties" to avoid confusion for other developers), the missing of "friend" because it's class scoped only and can' be modified afterward, the missing of "protected" which is really important, this missing of modifications in case a developer wrote a "bad" classe. @bakkot The fact what a programmer should be allowed to have a private and a public property with the same name is for me irrelevant => with a |
Keep in mind too that it's not necessarily the case that the person whose code breaks is the person who has to deal with that problem: suppose C depends on B which depends on A. If A adds a new private field in a patch update and in doing so breaks B, then C will update A and find that their project is now in a broken state because of code they do not control. They can't actually change the name of the thing, because the conflict is in B. The whole point is that the names of private fields are something which consumers of your code should not have to know. Also, I'm not sure it actually is all that different from other languages. What difference do you mean? |
Well, let's compare with java which is pretty good for poo :
So # is not really a "private" but more a "class scoped". It's not a modifiers but only a property only available in the class context. So yes, # makes sense in some situations, but in this case it should not be called "private" to avoid confusion for others. This is like the below example but for es6 classes : function A() {
let a = 1; // only visible in A
this.method() {
console.log(a);
}
} So the need for real private, protected and friend is still required and complementary with #. On your previous argument, you rely on the fact a dev would extend an external lib (this is really rare but could append). If he updates a lib and see that it doesn't work anymore, it's simply what appends for most of the libs updates... he has 2 solutions => rollback or edit it's own classes. |
It's funny you should mention Java, because they're currently in the process of adding a major new feature to the language (modules) in part because they found that developers needed a way to prevent reflection from accessing private fields.
I'm not sure why "some languages also have protected fields and friend classes" means that their private fields work any differently. (And of course not all languages do have friend classes - like Java, for example.) We're not calling this "general access modifiers"; we're calling it "private fields". And as far as I can tell, it pretty much works how private fields work in most other languages (plus or minus reflection, anyway). Why do you think this proposal doesn't count as "real private fields"?
I don't think this is an acceptable cost. |
Well it's strange because you seems to be in a total deny of the importance of "protected", "friend" and reflection : if they were implemented on many languages it was for a good reason.
Modifiers are more metadata to inform the developer (for other languages, the access check is done at compilation time only and not put into the binary code). In js the check could only be done at run time. The # here has not the same "definition/implementation" than in other languages, it's a "class scoped property" and has of course importance for some usecases (so the proposal makes sense), but I would be strongly disappointed if some external lib was "limited/restrained" just because they abuse of # fields (imagine a jQuery without extension, etc...). I just see one bypass way : import { A } from 'A';
A.prototype.getPrivateX() {
return this.#x; // pretty ugly according to me...
} What I want to point is that we need more (as I repeat since the beginning), see further to have real class and solve concrete daily problems. It's not all about modifiers, its too about multiple inheritance and abstract classes : things that exists from ages and could be a real improvement for js. |
+1 for thinking But, the swap example in #14 is also a big problem for using In typescript, the I have no idea about other languages which have private as their private class field, maybe these languages have a well-designed mechanism to avoid this problem. |
@Jack-Works did you reference the correct issue ? |
I'm sorry, I cannot find out which issue I want to refer.
But I laterly find that problem is already described in the FAQs
…On Tue, 18 Jul 2017, 16:38 valentin, ***@***.***> wrote:
@Jack-Works <https://github.com/jack-works> did you reference the correct
issue ?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#15 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AFJBf7BfDiqyPzWocQjfSrEq8o1cYwdLks5sPG8YgaJpZM4OT5Pp>
.
|
private xOne possibility to support For example, we could modify the behavior of GetValue(V):
It's not terribly expensive, though it expands the definition of [[HomeObject]] a bit. It then aligns more closely with how other languages treat private. With those rules, we can easily expand them to support I'm not advocating for this, just pointing out the possibility. I do feel it calls into question the validity of this entry in the FAQ that calls out "friend" accessI imagine you can emulate "friend" access, at least within the same file: let xAccessor;
class A {
#x
constructor() {
// set up "friend" accessor for #x
if (!xAccessor) xAccessor = { get: (a) => a.#x, set: (a, v) => a.#x = v };
}
}
class AFriend {
constructor(aInstance) {
console.log(xAccessor.get(aInstance));
}
} |
How so? Adding steps to every [[Get]] is exactly the concern that part refers to. |
Yes, but without metrics that statement is pure conjecture. I could understand the argument that it would make [[Get]] slow if it was expensive to correlate the caller to the receiver. The changes to the algorithm steps needed to accomplish this seem small enough that the only way definitively state the performance cost is with actual data. We shouldn't make a blanket statement that Again, I am not necessarily advocating for |
The FAQ entry just says we want to avoid further complicating property access semantics, which is true, and that adding a runtime typecheck on property access would slow down property access, which I believe is also true. The FAQ entry doesn't say it would be slow for some objective definition of slow, or include any superlatives that I can see. It certainly doesn't definitively state any particular performance cost. What phrasing would you prefer? |
I don't have better phrasing to offer at the moment, but would be interested to see what, if any, performance impact such a change would have. I just balk a bit at (paraphrased) "we don't want to do X because its slower" without showing that it's slower when that impact is non-obvious. |
Well, for me if the field is public the GetValue should be as fast as before, because only private/protected requires to check the context, Moreover, a private/protected check should not be very slow (probably only 2 times slower than public) because the context (stack) is already known by the environment. |
I think @rbuckton 's point makes a lot of sense to me in the context of high performance js. What is an acceptable level of performance penalty v/s flexibility, power and future proofing? The performance concern is certainly something to be aware of, but not necessarily something that should call an absolute halt to alternate proposals like Given that we already have to work around js performance issues in tight code anyway. function() {
var x = 1;
function() {
function() {
function() {
var i=0;
var inner_x = x; //we always have to do this in tight code today
var calculated = 0;
//slow, because of scope chain navigation
for(i=0; i<1000000000; i++) calculated = i + x;
//fast, because we've brought the variable into scope
for(i=0; i<1000000000; i++) calculated = i + inner_x;
}
}
}
} Pretty much everyone doing high performance js is aware of the above issue. And of course the same thing applies to the prototype chain lookup, and ES6 doesn't really change that nor anything I've read about class fields: class Foo { x=1; ... }
class Bar extends Foo {...}
class Baz extends Bar {...}
b = new Baz();
for(let i=0; i<1000000000; i++) b.x + i; //slow because of prototype chain navigation Again, when you're really concerned about squeezing performance, you always cache the variable you're accessing. Given the penalty of prototype and scope chain navigation, @rbuckton makes a strong point about quantifying the performance impact of alternate approaches - we already have to deal with performance penalties for having the power of prototypal inheritance and closures, and we gladly accept them and work around them when necessary. The same patterns and workarounds would still apply to access modifiers, i.e. local value caching, so I don't really see the problem. |
@claytongulick, performance is not the only consideration mentioned even in that single FAQ entry. At least as serious is the other issue raised, i.e., that a given piece of code might act on a either a private field or a public field depending on the type of the object passed in. |
Using decorators for protected fields doesn't feel right to me. If this is still in the proposal stage, then why wouldn't the goal be to get this right the first time around instead of waiting for another proposal to hopefully improve/fix things later on? That said, I'd prefer something along the lines of: class A {
##privateField = 0;
#protectedField = 0;
publicField = 0;
constructor() {
##privateField++; // ✅
#protectedField++; // ✅
publicField++; // ✅
}
}
class B extends A {
constructor() {
super();
##privateField++; // 🚫
#protectedField++; // ✅
publicField++; // ✅
}
} The #-prefixes could be swapped for private and protected to match the current proposal, but I think it looks more logically consistent this way. It could be argued that As for whether access to the protected field is controlled through something like the private fields' internal slots list, or by simply checking the prototype chain, I leave that entirely up to others. |
@spencerhakim, why? There are many possible levels of access control between "fully public" and "fully private", including probably some which we haven't even thought of; I don't understand the desire to give language support to |
(Forgive me if I'm missing something here, did my best to read through this thread, but there's quite a bit) @bakkot I understand and agree that many intermediate levels of access control can and do exist, but from what I've read, the ones discussed here can generally be grouped together as follows (with keywords loosely based on your examples):
I believe 2 is out of scope for this proposal, and 3 is likely out of scope for the language (considering the various module systems currently in use), and both of them could have solutions that apply not just to class fields, but to methods and classes/functions/variables (on a module/package level), too. Even if an implementation for 1 can't make guarantees in situations where the instance's If @ljharb / the committee decides that this proposal should only deal with the object-oriented aspect of JS and not the class/prototype-oriented aspect of it and continues with this proposal as-is, then so be it, but (considering this proposal is about Class Fields, after all) I'd be disappointed to see developers left with only an all-or-nothing approach to field access until some indeterminate point in the future when a proposal that likely deals with a much wider span of access levels is standardized. |
I too would like to see a more complete native solution for access control that doesn't rely on decorators. But that's a much more complex problem than just private fields, and I think it should be done with more community feedback than has been obtained so far or could be obtained just from this github thread. And I agree with what @bakkot said earlier in this thread:
Allowing people to play with private fields and decorators would be a great way to get more feedback and real-world experience with other access levels. Designing a native solution right off the bat for every access level deemed necessary would certainly slow down the ratification of this proposal, and people have been asking for private fields for a long time. I do think it's important that we not paint ourselves into a corner by introducing private fields in such a way that would limit us in the future, but we discussed that already and I think the current proposal leaves many options on the table for future native access control features. I might be more inclined to agree with @spencerhakim if |
@littledan
If I mark field as private how can it silently become public? Can you give some lower level details (V8 js engine) what is wrong with using private keyword and this? class Foo {
private _name = 'bar';
get name() {
return this._name;
}
}
const foo = new Foo();
foo._name // browser should throw error
foo.name // ok
Why we need to have private and public fields named the same x (it is not possible). Just name you private field with underscore
Thanks. |
Well, I'll try to resume :
// module A.js
const _private = 'private';
export class A {
method() {
console.log(_private);
}
} In other languages, private are not scoped like this and can be accessed through reflection. So the term "private" for this proposal is wrong. This should be called: "scoped properties" instead. |
@lifaon74 Yes, this proposal is for what people have been calling "hard private" instead of "soft private" - so private properties would be truly inaccessible from outside the class (not even via reflection). To be clear, unlike your code example, private properties would still be specific to each instance (except for private static properties). @anjmao the Typescript Regarding protected access, yes that is an important use case (or at least something similar to it), and this proposal leaves open many possibilities for introducing protected in the future. It will also be possible to implement it using decorators. Protected/friend/etc were discussed at length earlier in this thread. |
I forget to mention it but YES, #private (hard private) makes sense allowing cool stuff. I just hope developers won't abuse of it: "with great power comes great responsibility". The term "private" in this proposal add just many confusion vs other languages private meaning. A "class scoped property" makes more sense. |
Yeah, perhaps it would be worth starting a new thread to discuss whether or
not "private field" (or "private property" more generally once we have `#`
methods too) is the best name for this.
|
@lifaon74 @mbrowne Thanks for your answers. I still need few more to be 100% clear.
class Foo {
#name = 'nooo - this is hard private';
}
const foo = new Foo();
foo.#name; What will happen then I run this script in the browser at runtime? Will it just throw SyntaxError or something like OhNoPrivateWasUsedOutsideClassError?
class MyClass
{
public $public = 'Public';
protected $protected = 'Protected';
private $private = 'Private';
}
$obj = new MyClass();
echo $obj->private; // Ups and at runtime you will get error
So what is the reason we can't simply have public, private, protected in ES class class MyClass {
public publicVar = 'Public';
protected protectedVar = 'Protected';
private privateVar = 'Private';
}
const obj = new MyClass();
console.log(obj.privateVar); // Ups. Should throw error at runtime Normal library user will not be able to access any internals because tooling will show errors and JS runtime will throw error at runtime so we are all good and safe in 99% cases. I still want to access all private properties if I want using reflection (I'm advanced user), but this should be my own responsibility.
class A {
private privateVar = 'Private';
}
class B {
giveMeA(a) {
a.privateVar; // will throw error, but I will know it only on runtime because javascript is dynamic language and I need to use Flow or Typescript to annotate argument type.
}
}
const a = new A();
const b = new B();
b.giveMeA(a); // throw error at runtime so by having you private named with |
@anjmao The problem with throwing (was pointed out in some comments around this repo and might also be in the FAQ): It makes it unsafe and a breaking change to add a private field to a class. Which this proposal doesn't consider acceptable. Example:
If we'd allow naming collisions between public and private fields, then the library implementing |
About multiple inheritance,
It can also be achieved with prototype chain branching using Proxies, and a helper that puts it all together. Usage is like this: import multiple from 'path/to/multiple-helper.js'
class Foo {...}
class Bar {...}
class Baz extends multiple(Foo, Bar) {...}
console.assert( ( new Baz() ) instanceof Foo ) // works
console.assert( ( new Baz() ) instanceof Bar ) // works I have a couple non-proxy implementations in these gists:
I hadn't made the Proxy implementation yet, because Proxy wasn't widely supported at the time, but I'd like to revisit it and make that in implementation 3. I'd to add the Proxy implementation as an additional feature for https://github.com/trusktr/lowclass. Just sharing here, as I'm interested in this stuff for fun (and, well, because I also feel that having |
I just realized the flaw in one of the arguments about encapsulation... On July 14, 2017, @bakkot wrote:
As it works out, there's no conflict here. class Base {
static m(obj) {
return obj.x
}
static m2(obj) {
obj = (typeof(obj.__proto__) == "object") ? obj.__proto__.__proto__ : obj;
return obj.x;
}
}
Base.prototype.x = 0;
class Derived extends Base {
}
Derived.prototype.x = 1;
var d = new Derived();
Base.m(d); // returns 1;
Base.m2(d) //returns 0; This is how it has always worked in ES. I don't see why declaring The mistake in your logic is that you applied the reasoning for a language with hard types to ES which is duck typed. There is no confusion precisely because ES doesn't have hard types. In the end, because |
As we’ve discussed elsewhere, class is not just sugar (even if it’s mostly sugar), and that isn’t part of its design. It’s a different construct and it will create a different mental model than the outdated ES5 way. |
If we accepted this, there would be no point in having private fields at all. Public fields are already adequate for duck-typing. But sometimes you want closure-style encapsulation instead. |
I think the above thread adequately covered the issues with the above proposals. To summarize, a design goal of the class fields proposal is to provide a strong encapsulation boundary, but it seems like the above proposals would not be able to provide such a boundary. |
Can you make some bulletpoints of the requirements? |
Hello everybody, after following the private class fied proposal and now this proposal, I wanted to discuss : why we don't go further.
First I need to specify i'm not a java, C++ guy or whatever, but a really involved an ecmascript lover. My motivation is to reach a consistent structure across languages heavily used and which have already implemented all the best for classes (since decades).
So for me what would be the perfect class in ecmascript :
1) attribute/method modifiers
In most of the language we find : public, private, protected and static. Currently only static is supported. For me we should use all of this words (already implemented in many languages) to keep consistency and fast code adaptation from one language to another.
The # for private sound wrong for me and the discussion (tc39/proposal-private-fields#14) didn't convince me (people proposed concrete solutions to every problems...). Moreover the protected is still missing but extremely used in inheritance (sound strongly necessary).
Because we love to have full control of class properties in ecmascript, we could add a new attribute to descriptor when using
Object.getOwnPropertyDescriptor
orObject.defineProperty
(https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Object/getOwnPropertyDescriptor) :The new modifiers attribute could be an array of string representing the access of the method, and could potentially be modified after initialisation (to allow the same power than Reflect provides, allow a "backdoor" on external libraries, and allow descriptor to modify modifiers). This would bring far more power than the actual #field propose.
The public, private, protected should be allowed before static too.
To allow external classes or function to access to a protected or private member, we could use the friend keyword. Something similar to
This means than the class B can access the attribute attr of A even if it's private.
We could then extends the
Object.defineProperty
:friends could be an array of friend functions or classes.
Finally for modifiers, a const keyword could be used to specify constant attributes, only allowed to be initialized into the constructor.
This will be a shortcut for
writable=false
2) Multiple inheritance
Multiple inheritance is something that a lot of developers wants (there is a lot of subject or tutorial on the web) but can only be archived through mixins or factories. I would enjoy that you won't reply : "this is too complex because of diamond structures" or whatever because this is obviously FALSE (other languages like C++ archive it easily).
The following is just an idea how to bring multiple inheritance, it's not a concrete proposal.
First of all, because of the javascript current inheritance system with prototype, we can only inherit from one class. No more.
A.prototype = Object.create(B.prototype);
or in es6class A extends B
.So we should introduce some king of new syntax.
A.prototypes = [Object.create(B.prototype), Object.create(C.prototype)];
A extends B, C
instanceof should then search for all subclasses, so
new A() instanceof C
would return true.The super keyword will need some adjustments:
I propose this king of syntax
super<B>.method
: the super class here is B.To init a class:
Or we could use some C like :
super::B
.Using
super.method
will call the first super class (here B) for retro-compatibility.Some other fancy stuff could be added too :
super<B>()
.3) Abstract classes
Not the most important but really enjoyable, the abstract keyword could be use before the class keyword to define abstract classes. An abstract class may have abstract members and can't be initialized.
This still need more specifications.
So, the discussion is open. My purpose it to bring more in a big step to ecmascript instead of doing small steps and be stuck with retro-compatibility because of too various incremental specifications.
The text was updated successfully, but these errors were encountered: