Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

Make # an object on the instance #75

Closed
shannon opened this issue Jan 20, 2018 · 77 comments
Closed

Make # an object on the instance #75

shannon opened this issue Jan 20, 2018 · 77 comments

Comments

@shannon
Copy link

shannon commented Jan 20, 2018

Update at bottom of description

After reading through the FAQ and issues on the various related repos regarding the sigil I just wanted to run something else by the champions of this proposal.

Please forgive me if this has already been discussed but as I said there are various repos and lots of threads so I may have missed it.

I'm wondering if just making # be a an object property of this makes this proposal more consistent with existing paradigms in Javascript.

For example:

class {
  private x = 'x';

  method() {
    console.log(this.#.x);
  }
}

Would be sugar for:

(function(){
  const __private__ = new Weakmap();
  return class {
    constructor(){
      __private__.set(this, {
        x: 'x'
      });
    }

    method() {
      console.log(__private__.get(this).x);
    }
  }
})();

Why?

Well I get the need for the # sigil for assesor efficiency but there are a couple of points of this proposal that I find unnessarily restricitve.

  1. No variable accessors (e.g. this['#x'] or this[somevar]). This would seem like any use case where you would need this for public properties could also be applied to private properties.
  2. No private property iteration. To me there shouldn't be any reason I shouldn't be able to iterate over private properties without breaking encapsulation. I can think of one use case in particular for a ECS game engine I am working on.
  3. No destructuring. To avoid having to type this.# repeatedly it would be nice to be able to use destructuring at the beginning of a method to access private properties.

With this adjustment all of these should be possible without losing any encapsulation.

Variable accessors:

class {
  private x = 'x';
  private y = 'y';

  method() {
    const prop = condition ? 'x' : 'y';
    this.#[prop] = somecomplexcalc(this.#[prop]) ; 
  }
}

vs

class {
  #x = 'x';
  #y = 'y';

  method() {
    if(condition) {
      this.#x = somecomplexcalc(this.#x);
    } else {
      this.#y = somecomplexcalc(this.#y);
    }
  }
}

Private property iteration:

class {
  private x = 'x';
  private y = 'y';
  private z = 'z';

  method() {
    for(const [prop, value] of Object.entries(this.#)){
        this.#[prop] = somecomplexcalc(value);
    }
  }
}

vs

class {
  #x = 'x';
  #y = 'y';
  #z = 'z';

  method() {
    this.#x = somecomplexcalc(this.#x);
    this.#y = somecomplexcalc(this.#y);
    this.#z = somecomplexcalc(this.#z);
  }
}

Destructuring:

class {
  private x = 'x';
  private y = 'y';
  private z = 'z';

  method() {
    const { x, y, z } = this.#;
  }
}

vs

class {
  #x = 'x';
  #y = 'y';
  #z = 'z';

  method() {
    const x = this.#x;
    const y = this.#y;
    const z = this.#z;
  }
}

Why private in declaration? (I know it's been talked about but I have to try)

First consistency and readability:

class {
  #x = 'x';
  @protected #y = 'y';
  static method (){}
}

vs

class {
  private x = 'x';
  protected y = 'y';
  static method() {};
}

Secondly, with # being a property of this instead of part of the name of the property it makes more sense here.

Conclusion

To me, this makes this proposal seem a little less strange compared to the rest of Javascript. It's a simple mental model. All private properties are stored on this.# which is only accessible from within the class methods.

Keeping the sigil for accessors means it's non breaking for any existing code so I'm ok with it there but I really hope we reconsider using private in the declaration just to keep the syntax consistent and readable.

Edited: typo and broken formatting in github

Updated proposal

I wanted to provide an update to my proposal based on the discussions in the thread. I believe that the # should be moved to before this to make it clearer to developers that #this is a lexically scoped variable.

class {
  private x = 'x';

  method(obj) {
    console.log(#this.x, #obj.x);
  }
}

Would be sugar for:

(function(){
  const __private__ = new Weakmap();
  return class {
    constructor(){
      __private__.set(this, {
        x: 'x'
      });
    }

    method(obj) {
      console.log(__private__.get(this).x, __private__.get(obj).x);
    }
  }
})();

I have also taken the comments of @ljharb and @bakkot into consideration and I have a further addition that will prevent any leakage to mutations.

To avoid any leakage we can make it a syntax error to assign #this to any variable, property, return value, or function parameter. This is actually a really simple syntax check. # must be immediately followed by variable identifier which must be immediately followed by a . or [. With the only exception being for destructuring.

Syntax error example:

const x = #this;
const x = { #this };
const x = { x: #this };
const x = [#this];
somefunction(#this);
return #this;

I'm sure there tons I have missed here but as long as they follow those rules there shouldn't be any leakage.

Valid syntax examples:

const x = #this.x;
#this.method();
#this.x++;
#this['x']++;
const { x } = #this; // exception to the rule but doesn't cause leakage
const x = { ...#this }; //exception to the rule but doesn't cause leakage

This prevents any leakage for mutations.

This doesn't fully address the issue with private keyword implying this.x access but I think it comes pretty close.

Edit: I accidentally left out or [ syntax check that I had in my notes here

@ljharb
Copy link
Member

ljharb commented Jan 20, 2018

What would the [[Prototype]] be for this object? Would i be able to pass it around and expose private values to mutation later, perhaps by accident as the receiver with this.#.foo()? Would i be able to install getters and setters on this.#?

@bakkot
Copy link
Contributor

bakkot commented Jan 20, 2018

See this recent thread on computed property access and iteration.

Re: the syntax for declaration, I am strongly opposed to any proposal which has private x as the declaration and does not have this.x for access for the reasons given in the FAQ.

There's some other issues with this suggestion which I can go into detail about if you'd like, but these are some of the major points.

@shannon
Copy link
Author

shannon commented Jan 20, 2018

@ljharb

What would the prototype be for this object? Would i be able to pass it around and expose private values to mutation later, perhaps by accident as the receiver with this.#.foo()? Would i be able to install getters and setters on this.#?

Sure just as you could break encapsulation now by accidentally passing any enclosed object around. Accidentally passing this.# seems pretty unlikely to me. Is there some reason that I'm not seeing as to why we wouldn't want to allow getters and setters?

@bakkot

See this recent thread on computed property access and iteration.

I've read that thread, I'm not sure I understand completely the opposition in the context of my suggestion. this.# should only be able to be referenced from within the class methods. If the suggestion in that thread is to place the private properties into a private map, how is that any different than my suggestion?

As you can see from my examples in the description, there are very common use cases for being able to reference a private property from a variable. If this is prohibited then it's going to lead to a lot of ugly
and redundant code.

Re: the syntax for declaration, I am strongly opposed to any proposal which has private x as the declaration and does not have this.x for access for the reasons given in the FAQ.

I've read the FAQ but it doesn't really give a convincing argument. The same could be said accidentally using this.x instead of this.#x. As many times as it's been said throughout these threads that other languages shouldn't be used as a measurement for this proposal I have to say assuming this.x access of a private field because other languages do fails under the same logic.

@shannon
Copy link
Author

shannon commented Jan 20, 2018

@ljharb Not sure why you would but if you want to prevent getters and setters from being added later you can use Object.seal.

(function(){
  const __private__ = new Weakmap();
  return class {
    constructor(){
      __private__.set(this, Object.seal({
        x: 'x'
      }));
    }
  }
})();

@bakkot
Copy link
Contributor

bakkot commented Jan 20, 2018

@shannon

If the suggestion in that thread is to place the private properties into a private map, how is that any different than my suggestion?

The suggestion is that you should use a private property with a Map if you for some reason need a private map, not that you should always do this. I would prefer to encourage the mental model that private properties are record-like.

As you can see from my examples in the description, there are very common use cases for being able to reference a private property from a variable.

I do not share your intuition that these are very common uses cases for private fields.

If this is prohibited then it's going to lead to a lot of ugly and redundant code.

I don't share this intuition either - I don't think putting an object on a private field is all that ugly, and it seems to me it would satisfy your use cases without particular redundancy. In fact, if you had #_ = { x: 'x', y: 'y' } in the class body under the current proposal, you could use this.#_.x or this.#_['y'] for access to get almost identical syntax to what you're asking for without requiring reifying and reasoning about an actual object for the more common record-like use case.

I've read the FAQ but it doesn't really give a convincing argument. The same could be said accidentally using this.x instead of this.#x. As many times as it's been said throughout these threads that other languages shouldn't be used as a measurement for this proposal I have to say assuming this.x access of a private field because other languages do fails under the same logic.

I'm sorry you don't find the argument convincing. I do. I am less concerned (not unconcerned, but less) about this.x vs this.#x because of the symmetry between declaration and use, which is shared by public fields: class C { x = 0; #y = 1; m(){ return this.x + this.#y; } } is very consistent, to my eye.

Also, we do care what happens in other languages and would prefer to avoid misleading people with a background elsewhere, especially in cases where there is very similar syntax which has significantly different semantics in another language. We don't necessarily want to replicate other languages identically (and often are not in a position to in any case), but that doesn't mean we feel free to totally ignore them.

@shannon
Copy link
Author

shannon commented Jan 20, 2018

@bakkot

I would prefer to encourage the mental model that private properties are record-like.

If we want a record-like syntax than it shouldn't be accessed via this at all (i.e. this.#x or this.x or this.#.x). It should just be blocked scoped and accessed via bare x. A mental model that all javascript developers are familiar with. Yes, I have read the FAQ on bare x but it seems to me that we've decided on trying to pick and choose from two different paradigms and ended up with something that is pretty inconsistent with the rest of javascript.

In fact, if you had #_ = { x: 'x', y: 'y' } in the class body under the current proposal, you could use this.#.x or this.#['y'] for access to get almost identical syntax to what you're asking for without requiring reifying and reasoning about an actual object for the more common record-like use case.

Having to do #_ = {...} is not exactly pretty. But more importantly without a proposal implementation to sort out context for private methods it becomes really messy really quick.

class {
  x = 'x';
  #y = 'y';
  #_ = {
    method () {
      //how do I even reference x and y?
    }
  }
}

vs

class {
  x = 'x';
  private y = 'y';

  private method () {
    return this.x + this.#.y;
  }
}

Being sugar for something like:

(function(){
  const __private__ = new Weakmap();
  const __methods__ = {
    method: function() {
      return this.x + __private__.get(this).y;
    }
  }
  return class {
    constructor(){
      __private__.set(this, {
        y: 'y',
        method: __methods__.method.bind(this)
      });
    }
  }
})();

@EthanRutherford
Copy link

@bakkot it seems your biggest complaints against this proposal are this:

  1. inconsistency between declaration and access.
  2. the ability to accidentally expose private parts.
  3. private access should always be thought of as "record-like"

I personally think that (with some small tweaks) this proposes a much less complicated, easier to understand implementation of privates, without really deviating too far from what's already in the main proposal.

This would essentially be the same as the current implementation, except with only one private member, named simply #, as opposed to infinitely many prefixed with #.

To address concern # 1, the following would provide consistent declaration and access:

class Foo {
    x = "I'm pubic"
    # = {
        y: "I'm private",
        method = () => this.#.x + this.y;
    }
}

This to me seems much easier for the uninitiated to comprehend. Rather than explain "Oh yeah, well a member prefixed by # means that the member is private. Except that really, it's not a member at all anymore. You can write a new member at any point, but privates all have to be declared up front in the class body. Also the name isn't #foo, it's actually just foo, but you have to use the # sigil to differentiate between public and private members. They're also not dynamically accessible, or iterable...."
You get the point. It's a drastic break from what we're used to with js classes (or js anything, for that matter), and takes some effort to explain.

With this, the explanation is a lot simpler. "this.# is the private object. Put anything you want to keep private in there.". It intuitively makes sense. Sure, there's this one special property that behaves different, but it's just one, and it gets to be special because it's the private object. And you can write whatever you want into it, iterate it's properties, etc.

(This has another nice parallel to how react state is usually declared and used, just to put that out there.. state = { foo: 1 }, method = () => this.state.foo + 3)

For concern # 2, I don't know that this is any more potentially dangerous than what is already possible. Sure, you could accidentally leak some information, but any potential for leak would also be present in the current implementation, via this.#_ = {}.

As for # 3, I really don't think correct to say that privates should only be record-like, or even to encourage users to only think of privates as record-like. Sure, the use cases for dynamic access and iterability are not the most common cases, but they certainly aren't non-existent. I don't see any value to treating private values as record-only. To the contrary; I think that having them be so, when the reasons why they are are so technical and non-obvious, creates a very strange limitation that breaks the user's intuition about how the language should work. When a user asks "why can't I iterate over private properties?" or "why can't I access this dynamically?" the discussion that will follow will be lengthy, and a tad unconvincing. The sheer number issues (and comments) coming up in this repo to the tune of "I read all the FAQ, and much of the conversation, but I still think it would be better to do ___" should be proof enough of that. Despite there being logical explanations for the choices made thus far, the community at large finds the reasoning dissatisfying. There's a general air of "I get it, but that doesn't mean I have to like it."

As such, I feel like it's much easier to swallow having only one private member (an object named #). Just one exception to the rule of property access, as opposed to a giant bag of arbitrarily named exceptions. You don't ever have to worry about what types of access are available for something private. With this.#foo, it almost feels like this is two separate objects, from different languages entirely. You put the funky sigil in there and you're whisked away to a foreign land where property access is completely different than everything you know.
But with this.#.foo (or perhaps even #.foo, why even need the this at this point?) you're just accessing a property of a plain old javascript object. It's comfortable and familiar. It behaves the way you expect it to (the way all javascript objects behave).

I apologize if any of this comes off as combative or stubborn. I don't really have any major technical arguments against the proposal as-is, other than the aforementioned feeling of "I get it, but that doesn't mean I have to like it". For better or for worse, whatever design is eventually accepted into javascript, we will be stuck with forever. I just want to make sure that we truly exhaust all our options before settling on a design. It would be a shame if a feature as highly desired as private data finally arrived, but was generally found to be unpleasant to work with, and confusing to new users. Before committing to any one design, we need to try to be absolutely sure that there truly is no better option.

@shannon
Copy link
Author

shannon commented Jan 26, 2018

@EthanRutherford I'm ok with this adjustment. Looks clean and simple to me. I also prefer just #.x. The only ambiguous part is the arrow functions. I'm not entirely sure how they behave with class properties to begin with.

I would have written it like this:

class Foo {
    x = "I'm pubic";
    # = {
        y: "I'm private",
        method() { return #.y + this.x; }
    }
}

How will this behave with the proposal as it is?

class Foo {
    x = "I'm pubic";
    y = () => { console.log(this.x);  }; //what is the context of this here?
}

*Edited for clarity

@EthanRutherford
Copy link

That one is simple. Arrow functions are lexically scoped, so the this of method y in your post would be the instance of the class.

class Foo {
    bar() {
        const test = () => this;
        console.log(test());
    }
    baz() {
        const test = {
            me: () => this,
        };

        console.log(test.me());
    }
}

new Foo().bar(); // output: Foo {}
new Foo().baz(); // output: Foo {}

Just as in the functions above, arrow functions declared this way would have a this lexically bound to the class instance. So, the following methods would both work intuitively.

class {
    x = "I'm public";
    y = () => this.x;
    # = {
        z: () => this.x,
    }
}

@littledan
Copy link
Member

This proposal is very interesting, though I share many of @bakkot's concerns. It seems to me like it could be added on top of the existing proposal as a follow on, driven by eexperimentation in transpilers. What do you think, @shannon?

@shannon
Copy link
Author

shannon commented Jan 26, 2018

@littledan I would agree with that. It almost feel like a special case for a blank '#' property. It removes the need to use #_ and if it handles the context within methods that would solve all my requirements.

I prefer the syntax

class Foo {
  # = {
    methodX() { ... }
    async methodY () { ... }
  }
}

over

class Foo {
  # = {
    methodX: () => { ... },
    methodY: async () => { ... }
  }
}

But if it's easier because of the lexical scoping as @EthanRutherford described then I would be ok with it.

@bakkot
Copy link
Contributor

bakkot commented Jan 26, 2018

@EthanRutherford, I don't think your design would work the way you wanted to, unless it actually did mean something very different from what it appears to.

In particular, I think that in

class C {
  # = {
    getThis() {
      return this;
    }
  };

  receiverIsPrivateObject() {
    return this.#.getThis() === this.#;
  }

  receiverIsObject() {
    return this.#.getThis() === this;
  }
}

(new C).receiverIsPrivateObject();
(new C).receiverIsObject();

it must be the case that receiverIsPrivateObject holds and receiverIsObject does not.

There's a few reasons for this. The most important is that this is how it would work if you used a public field (e.g. if you s/#/_/g above), and if it works differently for #, then it is not just a regular object: it's this weird other thing that looks exactly like an object but has different semantics. Less significantly, but also important, if methods in the object came pre-bound it would mean they couldn't be rebound. Plus it creates a weird asymmetry between # = { m(){} } and # = { m: function(){} }.

So I don't think you can really get the semantics you want, here.

But that means that it's pretty awkward to use: you can't readily refer to a public field from the private object, etc.

I really do not think that reifying an actual object distinct from the instance where private state lives is a good design. I think it's a lot nicer all around to have private state live on the instance, just as it does in other languages.

This to me seems much easier for the uninitiated to comprehend. Rather than explain "Oh yeah, well a member prefixed by # means that the member is private. Except that really, it's not a member at all anymore. You can write a new member at any point, but privates all have to be declared up front in the class body. Also the name isn't #foo, it's actually just foo, but you have to use the # sigil to differentiate between public and private members. They're also not dynamically accessible, or iterable...."

See, this hasn't been my experience at all. I explain to people "to declare a private field, begin its name with #". Sometimes I add that it's like how they might previously have begun its name with _ to suggest privacy, except the language will enforce it. And that's pretty much the whole explanation! People get it just fine. Yes, there's difference about iterability and so on, but these really don't come up that much and tend to be covered pretty well by "oh, right, it's private". I think the whole "private object" thing would be a lot more confusing.

As an aside, I'm not sure what you mean by "also the name isn't #foo, it's actually just foo". I would encourage thinking of the # as part of the name; this comment is certainly not something I would include in my explanation. The only way the language has any notion of what the name of a field is apart from how it's accessed is by function name inference, but #f = function(){} will create a function with .name "#f".

@EthanRutherford
Copy link

I think you misunderstand me. No, I don't intend for this object to have any exotic behavior: it will behave exactly the same as an object assigned to #_ under the current proposal. And I entirely disagree: it is not particularly awkward to access other fields from the object. Use an arrow function.

class {
    x = 5;
    # = {
        y: 10,
        // accessing both the public and the private members,
        // in exactly the way that intuitively makes sense
        z: () => this.x + this.#.y,
    };
}

In this way, there is absolutely no confusing behavior or usage: it's just a plain javascript object. I don't see any way this is more confusing, and in fact is largely identical to the way people have been making their own implementations of private data using WeakMaps.

// some-module.js
const privates = new WeakMap();

class Foo {
    constructor(x) {
        privates.set(this, {x}); // initialization of private data
    }
    method(that) { // access of private data, including from another instance of Foo
        return privates.get(this).x - privates.get(that).x;
    }
   // it is trivial to show that I can dynamically access, write, and iterate over private data as well
}

Also, you could just say "to declare a private field, begin its name with #", but you'd also be lying (or at the very least, withholding the truth). Because the truth is, it doesn't behave like like other fields. Calling these private fields, without also being up front about the fact that their access and write semantics are entirely different is deceptive. We're going to end up with people trying to do things like iterate through the private members, access private members dynamically, write new private members at will within methods, and even dynamically write new private members and these things won't work, and it won't be immediately obvious why. In order to explain how private members work, you need to explain all of it.

And I really doubt anyone is going to encounter not being able to iterate through or write new private members and think "ah yes, that makes sense, because they're private". There's no immediately obvious reason that there can't be a private way to iterate through private members. There's certainly no obvious reason I can't add a new private member in a method, either. I can understand the technical reasons, having read much of the discussion here, but I highly doubt that anyone unaware of the implementation details of private members will be able to make those connections.

As to the naming comment, it was my assumption that the sigil would not be parsed as part of the identifier token (# is not a valid identifier character, after all) and would be parsed instead as a symbol or operator token, indicating that the following identifier should be looked up from the private slots, rather than using the standard property lookup.

But the biggest issue with the name is that this would be an unprecedented change to identifiers. There is nowhere else in all of javascript where the way you name a value impact behavior or meaning. This is not only adding a new concept to javascript, but a concept that fits in with nothing else in the language.
Using the lone #, however, we could even treat the sigil as an operator. .# would be an operator meaning "get the private object for this object", whit #.foo still being shorthand for this.#.foo. This would also make a babel transform fairly simple to implement, using an object stored in a WeakMap, providing a far less compicated parallel to what is already possible in JS. Having a reference to the pre-privates-javascript way to do private data greatly simplifies the explanation.

Is there even a parallel to the proposal as-is in JS? Is it even possible (without excessive overhead) to transform a class with private members into current-JS, with full spec compliance?

@bakkot
Copy link
Contributor

bakkot commented Jan 28, 2018

I think you misunderstand me. No, I don't intend for this object to have any exotic behavior: it will behave exactly the same as an object assigned to #_ under the current proposal.

Ah, yes, I was conflating your proposal with @shannon's, sorry.

And I entirely disagree: it is not particularly awkward to access other fields from the object. Use an arrow function.

Since classes as they exist currently only allow methods, not arrows, I disagree with you about how awkward this is.

In this way, there is absolutely no confusing behavior or usage: it's just a plain javascript object. I don't see any way this is more confusing, and in fact is largely identical to the way people have been making their own implementations of private data using WeakMaps.

Again, I disagree about how confusing it is to have an object's private fields be properties of a different object.

Also, you could just say "to declare a private field, begin its name with #", but you'd also be lying (or at the very least, withholding the truth). Because the truth is, it doesn't behave like like other fields.

Mm. I disagree that failing to explain every piece of the semantics of something constitutes lying, and have not found most people to be confused by the behavior of private fields. This because private fields behave a great like other fields except that they are private. There are of course details to what that means exactly, but that alone seems to give people an extremely good idea of how they work and let them get on with writing code. If they're interested in how privacy is enforced, I will tell them that it's enforced by the name of the field only being available within the lexical scope in which it was declared, and this name being the only way to refer to the field.

People have been teaching a pattern of creating "private members" with privacy enforced by lexical scope for a decade. These likewise do not show in iteration, cannot be created dynamically, etc. I don't think I would call Crockford a liar, here. And the expectations to which you allude don't seem to have meant people avoid teaching this pattern, even to absolute novices.

I do not share your beliefs about how people tend to expect private fields to behave.

I highly doubt that anyone unaware of the implementation details of private members will be able to make those connections.

Well, quite a few of the people to whom I have explained private fields as above have made those connections, so "anyone" is overstating your case, I think.

As to the naming comment, it was my assumption that the sigil would not be parsed as part of the identifier token (# is not a valid identifier character, after all) and would be parsed instead as a symbol or operator token,

Yes, it's not a valid identifier character. I don't understand why this means it isn't part of the name from the point of view of the programmer. If your concern is people thinking about implementation details - well, in the spec there is a production called PrivateName, which includes the #.

There is nowhere else in all of javascript where the way you name a value impact behavior or meaning.

__proto__ and constructor spring immediately to mind, but that aside: yes, this is new syntax. In my experience it has been very easy to explain its behavior, so while the lack of precedent is a concern, it doesn't seem to me that it ought to be a blocking one.

Having a reference to the pre-privates-javascript way to do private data greatly simplifies the explanation.

I contend that to the extent there is a "the" pre-privates-javascript way to do private data, it is the pattern based on lexical scope I refer to above. I further contend that said pattern is much closer to this proposal than to a new notion of a "private object".

Is there even a parallel to the proposal as-is in JS? Is it even possible (without excessive overhead) to transform a class with private members into current-JS, with full spec compliance?

Yes, with WeakMaps very much like what you're doing. It's awkward and you might debate what counts as "excessive overhead", but it's doable and matches the spec precisely, as long as you're careful about receivers. You can either use a single WeakMap, as you have, or have one per field, as babel is doing.

@shannon
Copy link
Author

shannon commented Jan 28, 2018

@bakkot if # is part of the name can I just use a lone # as the private field? In other words, is there any reason I can't just do as @EthanRutherford has described with the current proposal?

@shannon
Copy link
Author

shannon commented Jan 28, 2018

Now that I have rethought about it, I think the # = { ... } or #_ = { ... } workaround is still somewhat lacking.

x = 5
# = {
  y: 10,
  z: () => this.x + this.#.y,
}

My concern is that lexically scoped functions are not optimized for this use case. Each instance would create new functions. This is just antithetical to the idea of prototypes in JS and by extension classes. Needing to iterate/dynamically access/destructure methods is far less common so if this proposal goes through as it is then I will just learn to live with it I guess.


Now that I have reread this thread I'm not sure if the comment from @littledan was about my original proposal or the modified version from @EthanRutherford.

This thread is such a confusing mess at this point so let me see if I can get back to my original proposal.

@bakkot

People have been teaching a pattern of creating "private members" with privacy enforced by lexical scope for a decade. These likewise do not show in iteration, cannot be created dynamically, etc.

The difference here is this.#x doesn't look like lexical variable access. It looks like a property access. The two normally behave very differently. I understand after reading through all this that it is in fact meant to behave more like a variable access. This is a somewhat easy concept to explain but doesn't resolve the fact that it doesn't look right. Yes yes, we will all get used to it. But it still won't look like anything else in JS so it does require further explanation.

Say I'm a new JavaScript developer and I have never heard of this proposal and I have no idea # is normally not a valid character for a variable. I've seen $ so why not #. Then I see this in someone's code and I can see they are just accessing it like any other property, this.#x, so it must just be a property. I won't say to myself, oh this is a private variable. I would just be confused when I get errors trying to access the property from outside the class or trying to name a variable with #. This initial confusing behavior is what discourages people from using different langauges.

Look at this simple example from the perspective of an inexperienced JS developer:

class Foobar {
    $x = 'x';
    #y = 'y';
    method() {
        return this.$x + this.#y;
    }
}
  1. There is nothing that says private.
  2. There is nothing that says this.#y and this.$x are any different from eachother

It would have made more sense to do something like #this or #obj which would at least look like variable access for something lexically scoped. As a new developer I would say, what is #this, since I can clearly see it's not defined anywhere.

In my original proposal here, by not using # in the property declaration it's somewhat more clear that this.# isn't user declared and it's not just a normal property. This would be even clearer by doing #this and I think I will edit my proposal to include this. However I still think #this should be an object that contains all the private properties and methods bound to this. It would behave like a simplified Proxy of this.

The use cases for iteration, dynamic access, destructuring may not seem common but it's important to keep the language consistent. If I can iterate over public properties I would expect to be able to iterate over private properties. If I can dynamically assign/access public properties I would expect to be able to for private properties. If I can destructure for public properties I would expect to be able to for private properties. The key here is they aren't variables, they are properties. They should behave like properties first and foremost. When they don't it's a major diversion from what is expected.

@EthanRutherford
Copy link

I have an alternative proposal, which while it unfortunately does not address the last issue @shannon referred to, would hopefully address most of the concerns presented by both sides.

The syntax would be largely the same as the proposal as-is, but we augment with a few things.
Firstly, only use one internal slot. This contains a pseudo-object who's this parameter refers to the instance of the class, so that private methods need not be bound or arrow-ed. Accesses and declarations are as proposed, #foo = "bar", this.#foo | #foo. However, this internal container also has a few methods in its prototype chain, such that (names up for debate) this.#getProperties() returns an iterator over all private properties, this.#getProperty() provides dynamic access, and this.#foo = ... is allowed for adding new private values (for completeness, there should probably also be a this.#setProperty() equivalent). These methods should probably be non-writeable. Perhaps a ## prefix for these would be appropriate, but I'm not attached to any particular name scheme.

This provides the same protections against accidental leakage of data and again, is mostly identical to the proposal as-is, but also provides capability to dynamically read/write and iterate over private data (but importantly, only for instances of the class). The syntax, and therefore the conceptualization, remains the same, but the usage of a internal object-like container allows straightforward avenues for providing dynamic access and iterability. By providing access to these features through the same private interface (this.#name), we also maintain the strict privacy of these properties. One could still, of course, leak out information, but one would have to be pretty intentional in writing such a leak.

foo(name) {
    // clearly not an accident
    return this.#getProperty(name);
}

@shannon
Copy link
Author

shannon commented Jan 28, 2018

@ljharb @bakkot @EthanRutherford I have provided an update in the description that I think may address some of the concerns with my proposal.

@bakkot
Copy link
Contributor

bakkot commented Jan 29, 2018

@EthanRutherford

Firstly, only use one internal slot.

Internals slots are purely a spec mechanism; I don't know what observable semantics you intend for this to imply.

This contains a pseudo-object who's this parameter refers to the instance of the class, so that private methods need not be bound or arrow-ed.

I'm sorry, I don't know what this means.

Accesses and declarations are as proposed, #foo = "bar", this.#foo | #foo.

The #foo shorthand syntax for property access is not currently a part of this proposal, in case that wasn't clear. If there's still a reference to it lying around somewhere, let us know and we'll try to get it cleaned up.

However, this internal container also has a few methods in its prototype chain, such that (names up for debate) this.#getProperties() returns an iterator over all private properties, this.#getProperty() provides dynamic access,

We could add some method of dynamically iterating over private properties in a follow-on proposal, but I am really not convinced it's worth doing. I just don't get why you expect or want this.

and this.#foo = ... is allowed for adding new private values

This is the sticking point for me. There's a few different problems:

  • Could you only create #foo on instances of the class? Remember, you can .call a public method on any random object. How would that be enforced? And what does "instances of the class" even mean, recalling that you can manipulate prototypes at runtime? In the current proposal, all of the private fields are installed onto instances when they are constructed, so that attempting to access a field on an object which lacks it is an error, so this has a clear definition, but what definition are you going to use if new fields can be created after the class has been defined?

  • I am uncomfortable creating new names dynamically, especially when they may already be in scope (see below).

  • It doesn't play well with nested classes. In the current proposal, this works:

class List {
  #nodes = [];
  static Node = class Node {
    #owner;
    constructor(owner) {
      this.#owner = owner;
    }
    delete() {
      this.#owner.#nodes = remove(this.#owner.#nodes, this); // this is the interesting part
    }
  };
  // etc
}

That is, a nested class can refer to private fields defined on its parent, since it has visibility of the field name.

But that means it is not clear - even to the spec - whether obj.#foo = whatever is an attempt to create a new field on an instance of the current class or to manipulate an existing field of an instance of an outer class (or to create a new field on such an instance).

I continue to feel that a Map (or plain object!) in a private field is a strictly better option than creating new private fields at runtime, in the case that you want dynamically named private state. It's much clearer what you're doing and avoids the issues above. I also fundamentally do not understand why you feel it is so important to support dynamically creating fields.

@bakkot
Copy link
Contributor

bakkot commented Jan 29, 2018

@shannon

This doesn't fully address the issue with private keyword implying this.x access but I think it comes pretty close.

I don't think it gets close enough - like I say, anything which has private x for declaration and not literally this.x for access seems way too dangerous to me.

It also has some problems with chained access:

class Node {
  #data;
  #next;
  constructor(data, next) {
    this.#data = data;
    this.#next = next;
  }
  lookaheadType() {
    return this.#next.#data.type; // works in the current proposal
  }
}

let head = new Node({type: 'eos'}, null);
head = new Node({type: 'token'}, head);

How would you do the equivalent of this.#next.#data.type under your proposal?

Similar ideas have been proposed a number of times in the existing extremely lengthy previous discussions of alternate syntax, and have been rejected for the similar reasons.

@shannon
Copy link
Author

shannon commented Jan 29, 2018

@bakkot

I don't think it gets close enough - like I say, anything which has private x for declaration and not literally this.x for access seems way too dangerous to me.

This may be true but this.#x implies property access when it isn't. this['#x'] = 'x' will silently create a public property as well. I'd say they are equally dangerous.

How would you do the equivalent of this.#next.#data.type under your proposal?

I hadn't thought about that use case but with a slight change to the syntax verification you could do something like this:

class Node {
  private data;
  private next;
  constructor(data, next) {
    #this.data = data;
    #this.next = next;
  }
  lookaheadType() {
    return #(#this.next).data.type;
  }
}

let head = new Node({type: 'eos'}, null);
head = new Node({type: 'token'}, head);

I think of # as a keyword itself that gives me the private properties and methods of any object that is an instance of this class.

@bakkot
Copy link
Contributor

bakkot commented Jan 29, 2018

This may be true but this.#x implies property access when it isn't. this['#x'] = 'x' will silently create a public property as well. I'd say they are equally dangerous.

It is property access. It's just accessing a private property.

I agree that there's a risk with the asymmetry with dynamic access (it's called out in the FAQ), but strongly disagree that it is of even remotely the same level of risk.

I hadn't thought about that use case but with a slight change to the syntax verification you could do something like this:

Technically, yes, but #(#this.next).data.type is significantly harder to read, for me, than this.#next.#data.type. I don't really understand why you'd prefer this syntax.

Very similar proposals have been raised and rejected a number of times previously in the main syntax discussion thread.

@EthanRutherford
Copy link

It may have been from old comments on issues, but I believe it's in the faq: "why not this#foo?: asi issues due to the shorthand #foo".

As to the code example in your post, I don't see how this is enforced even as is. Who's to say I don't pass in something completely different than a List as the "owner" of a node? How would the access violation in the delete method be detected at any point before runtime? Is the parser going to walk through every possible code path to make sure the wrong thing isn't passed in?

I don't really see a difference in enforceability whether private fields are preinstalled or not, we still don't know the type of what could possibly be being passed in at runtime until runtime.

The answer to the "why" you keep asking is fairly simple: consistency with public fields. As has been stated by both @shannon and myself, what we find objectionable is primarily that these look like fields, but behave more like lexically closed-over variables. What makes it even more complicated is that they're actually even a mashup of the two. In order to allow two instances of the same class to access each other's private fields, they go from lexically closed over fields to more like lexically closed-over keys for entries stored in weakmaps.. or something to that effect.

Additionally, there's a concern that, to the unaware, it is not clear that this._foo and this.#foo imply different semantics. The general readability (particularly to outsiders or beginners) is reduced. I feel like it could be significantly better if we could have something like

class Foo {
    private field = 0;
    bar(other) {
        return private.field + other.private.field;
    }
}

But I understand the potential for backward compatibility issues.

However, I don't think the argument that private foo implies access would be this.foo is all that strong, because static foo does not imply access would be this.foo.

@shannon
Copy link
Author

shannon commented Jan 29, 2018

@bakkot

Technically, yes, but #(#this.next).data.type is significantly harder to read, for me, than this.#next.#data.type. I don't really understand why you'd prefer this syntax.

I'd prefer this syntax because it's closer to anything else we have in JS. And it behaves as I would expect once I know what the # means. It's easier to go from no knowledge to complete understanding, instead of having to know all sorts of trivia about how private properties work in JS.

@shannon
Copy link
Author

shannon commented Jan 29, 2018

@bakkot

Very similar proposals have been raised and rejected a number of times previously in the main syntax discussion thread.

Sorry it is long but I don't see anything quite like what I am suggesting in there. private.x or obj.private.x is very different from #obj.x. These would actually go against my arguments here for moving # to the instance reference instead of the property reference.

Edit* nevermind I see your comment in there but I'm not exactly sure why it would be fatal with the # shorthand

@bakkot
Copy link
Contributor

bakkot commented Jan 29, 2018

@EthanRutherford

As to the code example in your post, I don't see how this is enforced even as is.

Attempting to access a private field of an object which isn't present on said object throws an error. If you pass something else in, the delete method would throw. (In real code, you'd do this check in the constructor.)

I don't really see a difference in enforceability whether private fields are preinstalled or not, we still don't know the type of what could possibly be being passed in at runtime until runtime.

You're proposing that private fields could be created at runtime, presumably in code outside of the constructor. Currently private fields can only be installed during construction, when there is no ambiguity about what it means for something to be an instance of the class - private field creation is tied to instance creation.

The answer to the "why" you keep asking is fairly simple: consistency with public fields.

#getProperty is not especially consistent with public fields. I'm still confused.

I also don't understand why this particular kind of consistency is so important. I do not remotely share the expectation that being able to for-in over (enumerable) public fields implies there must be some way to iterate over private fields, for example.

that these look like fields, but behave more like lexically closed-over variables

Again, they behave a great deal like fields. They are per instance, they are installed by the constructor at the same time as public fields, they are accessed with obj., they establish the right receiver if you do this.#method(), etc. In particular, they behave almost exactly like fields whose name is only available within a particular scope.

In order to allow two instances of the same class to access each other's private fields, they go from lexically closed over fields to more like lexically closed-over keys for entries stored in weakmaps.. or something to that effect.

The name is the thing which is closed over. I'm not entirely sure what it would mean for a field to be closed over.

Additionally, there's a concern that, to the unaware, it is not clear that this._foo and this.#foo imply different semantics.

Yes, that's a concern. It doesn't seem that major, to me. Because attempting to write #foo anywhere outside a class which declares #foo throws a syntax error, presumably with a note implying what the syntax means, I think people will generally learn pretty quick.

However, I don't think the argument that private foo implies access would be this.foo is all that strong, because static foo does not imply access would be this.foo.

As far as I'm aware static foo doesn't work that way in other languages, and does not create a per-instance thing. (And it is accessible with this.foo in static contexts!) private foo very much does work that way in other languages, and does create a per-instance thing.

@bakkot
Copy link
Contributor

bakkot commented Jan 29, 2018

@shannon

I'd prefer this syntax because it's closer to anything else we have in JS.

I just don't get this. this.#foo is very similar to something we have in JS: namely, public property access.

It's easier to go from no knowledge to complete understanding, instead of having to know all sorts of trivia about how private properties work in JS.

It really, really is not that hard to explain how private properties work in this proposal. I have done it many many times to people of varied levels of experience.

Sorry it is long but I don't see anything quite like what I am suggesting in there.

People have proposed private(obj).x at least twice to my recollection. Possibly not #(obj).x precisely, but similar concerns apply.

@shannon
Copy link
Author

shannon commented Jan 29, 2018

@bakkot

I just don't get this. this.#foo is very similar to something we have in JS: namely, public property access.

Is this.#foo somehow more similar than #this.foo? Because every way that it should be (more similar), it actually isn't in implementation.

class Foobar() {
  #x = 'x';

  method(obj) {
    this.#x !== this['#x'];
    this.#y = 'y'; //runtime error
    const { #x } = this; //syntax error 
    for(const prop in this){ } //#x doesn't exist even though I have access here
    
  }
}

At least with #this it's immediately clear that these would not work with this and you would need to reference #this instead. This would be true if # was not a special character and #this was just a lexically scoped variable. There's nothing strange about it once you know where #this comes from with no other prior knowledge. This is what is closer to the rest of javascript. Any syntax errors thrown as I have described in my proposal would come from accidents. Not anything useful. You don't need to pass #this around since it's available everywhere internally.

It really, really is not that hard to explain how private properties work in this proposal. I have done it many many times to people of varied levels of experience.

You have explained. They didn't come to the conclusion on how it works based on their knowledge of the rest of JavaScript. I can explain it too. You prefix private variables with # and then you can't access it this way, or this way, or this way, or this way, good luck. No logical explanation is to why all of these restrictions. If I'm in the class I should be able to access it in every way that I can access a public property. Any other restrictions seem like they just weren't thought out enough.

This proposal is putting a lot of restrictions in the wrong place. It seems that all the negatives are against the class developer just so there's no chance to accidentally leak. When, as I have described, you can still keep leaking from happening without removing any functionality from the class developer.

People have proposed private(obj).x at least twice to my recollection. Possibly #(obj).x precisely, but similar concerns apply.

It seems the only real concern about this is the syntax for chaining. This seems an acceptable compromise to me to regain all the functionality we are used to. Plus I don't think you really need the parenthesis around obj (e.g. #(obj)) unless you are chaining.

@ljharb
Copy link
Member

ljharb commented Jan 29, 2018

@shannon Why would this.#y be a runtime error? You can statically determine, at parse time, that that’s referring to a private field that doesn't exist. I would expect that to be a syntax error.

Separately, for..in only enumerates enumerable properties, so “i have access to it” in no way guarantees “i can find it in an enumeration loop” - I’m not sure why you have that expectation.

#this would make, as has been demonstrated, accessing private fields on non-receiver objects very unergonomic. This is not the primary use case, surely, but it’s relatively common - including on any comparison or brand-checking method, both of which are common in userland. In other words, this is not an acceptable compromise - it’s an important use case.

@ljharb
Copy link
Member

ljharb commented Mar 17, 2018

@MichaelTheriot it is indeed (conceptually) sugar for WeakMaps; you can put any this into the WeakMap upon construction, and then if you had, say, equals(obj) { return this.#secret === obj.#secret }, it would work fine. This isn't an "entirely new feature" in the sense that you can already do this with WeakMaps; only in the sense that it's ergonomic and straightforward to use.

@shannon
Copy link
Author

shannon commented Mar 17, 2018

@ljharb I assume you mean this.#secret === obj.#secret and not this#secret === obj#secret or has this changed? This is explicitly called out in the FAQ as an ASI hazard. I would much prefer this#secret but as we have been repeatedly directing users to the FAQ for any issues regarding the sigil I don't want to be adding to the confusion here. You last two comments have included this syntax though.

@ljharb
Copy link
Member

ljharb commented Mar 17, 2018

@shannon yes, my mistake, just typing too quickly :-) I've updated both of the comments you mentioned.

@MichaelTheriot
Copy link

MichaelTheriot commented Mar 17, 2018

@bakkot I don't expect engines to use a WeakMap under the hood, but if we agree it's semantically equivalent we should take advantage of it. Can we preserve what this offers? You can access private properties via bracket notation using a semantically equivalent WeakMap as such:

const Person = (() => {
  const __private__ = new WeakMap();

  return class {
    constructor(name, ssn = null, cc = null) {
      this.name = name;
      __private__.set(this, {ssn, cc});
    }

    get hasSSN () {
      return !!__private__.get(this).ssn;
    }

    get hasCC () {
      return !!__private__.get(this).cc;
    }

    hasSecret (key) {
      return !!__private__.get(this)[key];
    }
  };
})();

const joe = new Person('joe', '123');
joe.hasSSN; // true
joe.hasCC; // false
joe.hasSecret('ssn'); // true
joe.hasSecret('cc'); // false

and I would hope the equivalent would be

const Person = class {
  constructor(name, ssn = null, cc = null) {
    this.name = name;
    #this.ssn = ssn;
    #this.cc = cc;
  }

  get hasSSN () {
    return !!#this.ssn;
  }

  get hasCC () {
    return !!#this.cc;
  }

  hasSecret (key) {
    return !!#this[key];
  }
};

const joe = new Person('joe', '123');
joe.hasSSN; // true
joe.hasCC; // false
joe.hasSecret('ssn'); // true
joe.hasSecret('cc'); // false

I only favor this because it is intuitive to me. This is confusing:

  • Brackets do not work
  • Do Symbols work?
  • Does destructuring work?
  • Can I enumerate private properties?
  • Can I add properties that are private outside of the constructor? (Like the rest of JS!!)

All of these work with #this notation and I do not see a drawback of simply prefixing the #. Are these are being left out because of syntax or a perspective on how private properties should behave?

I expected private properties to behave identical to regular properties, just with their reference private to the class internals. What I'm coming away with is something else. This seems more like statically typed variables that can be accessed by <instance>.# syntax. That's all. It certainly does not seem like this.#a is a hidden property on the this object, just that it is masquerading as one and leading to a lot of confusion when the reality sets in that it isn't.

I would not expect private properties to be statically typed after a decade of experience, and it feels very inconsistent given that this is not a statically typed language. If this is the intent then private "properties" is misleading in this context and I would suggest a rename at least to convey something more accurate to what these actually are.

Sorry if I'm late to the show and what I'm saying is less valuable now, but I have to think there are merits here that need consideration given the small cognitive burden (#this.a instead of this.#a). It'd be nice to gauge interest between the two. I hope the questions I'm raising help get a better feel to what the reception will be.

@bakkot
Copy link
Contributor

bakkot commented Mar 19, 2018

@MichaelTheriot

if we agree it's semantically equivalent we should take advantage of it

... Why? It's not always the case that more powerful is better.

Are these are being left out because of syntax or a perspective on how private properties should behave?

The latter. There's some more discussion here and here, though the major thing for me is this:

JavaScript currently has exactly one kind of strong privacy, and that is the kind offered by closures. The privacy model of this proposal is pretty much identical to the privacy you get there (i.e., you have access to this.#x anywhere where the #x name is lexically in scope), and I think that's a good thing. I would not want to have to teach several different radically different models. Given this intuition or understanding, the answers to your questions are immediate, I think.

If I were to think of this as analogous to WeakMaps, I would want to think of it as having one closed over WeakMap per private field, not a single WeakMap storing a "private object".

It certainly does not seem like this.#a is a hidden property on the this object, just that it is masquerading as one and leading to a lot of confusion when the reality sets in that it isn't.

Can I ask why not? It's certainly intended to: for example, this.#foo() will call the resulting function with this as the receiver, while (0, this.#foo)() will get undefined; values are associated with individual objects rather than there being a single value shared across all objects; etc. They are not dynamic, but they're still very much properties, to me.

I would suggest a rename at least to convey something more accurate to what these actually are.

There's some discussion of this over here, but personally I don't expect it to matter what we choose to call this. People are going to think of it as private fields no matter what we do. And I don't think that's a bad thing, because it seems like it tends to produce pretty much the right mental model.

given the small cognitive burden (#this.a instead of this.#a)

I don't want to focus too much on the details of this alternate syntax proposal, though I think I linked upthread to some of the previous times it's come up if you really want to read more. Briefly, though, I dispute that this burden is as small as you think: for example, does #a.b.c map, in this proposal, to a.#b.c or a.b.#c? I claim neither is obviously correct, which is a bad thing.

I have to think there are merits here that need consideration given the small cognitive burden

They are definitely worth consideration! That said, we've considered all the things brought up in this thread a fair bit already, I think.

@shannon
Copy link
Author

shannon commented Mar 19, 2018

@bakkot

Briefly, though, I dispute that this burden is as small as you think: for example, does #a.b.c map, in this proposal, to a.#b.c or a.b.#c? I claim neither is obviously correct, which is a bad thing.

Since '#' would be an operator, I think it's pretty obvious that it's (#a).b.c. So it would be comparable to a.#b.c in the current proposal. Access c from a private object b on a. It's really only confusing when you mix the two syntax. I'm not sure how it could be confused with a.b.#c though, so I'm not sure how you found it ambiguous.

@ljharb
Copy link
Member

ljharb commented Mar 19, 2018

@shannon the proposed :: operator has ::a.b.c as sugar for a.b::c; which would be similar to the a.b.#c option.

@shannon
Copy link
Author

shannon commented Mar 19, 2018

@ljharb Do any other operators behave this way? :: is a stage 0 proposal at this point so I would not consider that to be normal to JS and should not be the cause of confusion when comparing to the rest of JS.

@shannon
Copy link
Author

shannon commented Mar 19, 2018

@ljharb In either case if that behavior is desired if that proposal is considering it then as long as it is well defined then the ambiguity should be gone. But to say it's confusing here is to say it's confusing for ::.

@bakkot
Copy link
Contributor

bakkot commented Mar 19, 2018

@shannon I'm not aware of any operators which behave as you're proposing # would behave. In any case, I think the important point is not that it would be different from ::, which as you say is not at all part of the language, but just that the proposed behavior of :: shows that at least some people have an intuition that a prefix-placed operator which modified member access would bind less tightly than the member access, while you have the intuition it would bind more tightly: hence it is not obviously correct to do one or the other.

Yes, we can pick one and make everyone learn yet another precedence rule, but that's still a cause of cognitive burden which is absent with the current proposal, which is all I meant to claim.

@shannon
Copy link
Author

shannon commented Mar 19, 2018

@bakkot Fair enough, I didn't think it all the way through. Now that I've looked at it again and you're right it should be the way you have described. x + a.b.c would be x + c. So my proposal would have to follow suit with the :: operator and I'm ok with this.

However, I think this is a small price to pay to reduce the cognitive burden in other ways though.

@MichaelTheriot
Copy link

MichaelTheriot commented Mar 19, 2018

@bakkot

Can I ask why not? It's certainly intended to: for example, this.#foo() will call the resulting function with this as the receiver, while (0, this.#foo)() will get undefined; values are associated with individual objects rather than there being a single value shared across all objects; etc. They are not dynamic, but they're still very much properties, to me.

Properties are much more than just plain variables and bounded functions.

  • I cannot delete the property, delete this.#foo
  • I cannot test the property exists, #foo in this
  • I cannot access this.#foo by a variable key, this[#variableKey]
  • I cannot assign this.#foo to another object via Object.assign()
  • I cannot get a property descriptor for this.#foo, Object.getOwnPropertyDescriptor()
  • I cannot declare a getter or a setter private property (AFAICT)
  • I cannot use the same set of valid property names nor even use symbols:
const validObject = {
  [Symbol.iterator]: function * () { yield 5; },
  '#': 5,
  '': 5,
  '#that': 5
};

I do not agree that these can be "properties" if they are not dynamic nor support the above behavior; this is fundamental to properties in JavaScript. At most, these share the access syntax and bind functions to the receiver, but otherwise are just variables.

There's some discussion of this over here, but personally I don't expect it to matter what we choose to call this. People are going to think of it as private fields no matter what we do. And I don't think that's a bad thing, because it seems like it tends to produce pretty much the right mental model.

Thanks for the link; I was not aware of this proposal. To me, this proposal gets closer to the mark with "instance variables" because that is what these effectively are. The mental model is simpler when we stop calling these properties because we lose the expectations that carries (at least, I do).

I think it would make sense to interpret these as private properties in the context of a static language, since these generally do not include the extra behaviors of JS properties, but this is a dynamic language, properties here do exhibit this behavior, and this is unnecessary confusion that should be avoided.

does #a.b.c map, in this proposal, to a.#b.c or a.b.#c? I claim neither is obviously correct, which is a bad thing.

I would at a naive glance say it is obvious (#a).b.c maps to a.#b.c, and don't see how this would be confusing at least in the context of how the language is defined today.

I suppose only the terminology is an itch at this point. It might seem trivial, but I anticipate others will have similar expectations and slip ups when they hear private properties landed. I have however gotten a better grasp of what this proposal aims for and appreciate the discussion!

@bakkot
Copy link
Contributor

bakkot commented Mar 19, 2018

@MichaelTheriot, thanks for expanding.

To me, those are (sometimes) things that it happens you can do with properties, but I don't really think of them as being fundamental to what properties are, and I don't think most other programmers do either. For what it's worth:

I cannot delete the property, delete this.#foo

Not all public properties can be deleted.

I cannot test the property exists, #foo in this

True, though part of the point of the more static shape offered by this proposal is that you shouldn't generally have to.

I cannot access this.#foo by a variable key, this[#variableKey]

True.

I cannot assign this.#foo to another object via Object.assign()

Not all public properties are copied by Object.assign.

I cannot get a property descriptor for this.#foo, Object.getOwnPropertyDescriptor()

True.

I cannot declare a getter or a setter private property (AFAICT)

I expect you will be able to; see this proposal.

I cannot use the same set of valid property names nor even use symbols:

Not really sure what this means.


I also want to note that for in also only gives a subset of properties, and with only introduces a subset of properties into the current scope. All four of these subsets - those seen by for in, those used by with, those which are delete-able, those which are copied by Object.assign - are distinct, and none are the full set. If things which do not have all of the above characteristics are not properties, we have at least five distinct categories of thing going by that name already. (Though you don't mention it, .length on arrays is another kind of property still, being a property which claims per getOwnPropertyDescriptor to be a regular data property but such that changing it has side effects.)

As such, I really don't think that most programmers tend to think of "properties" as things which have all of the above characteristics (and if they do that's a problem in itself); this just seems like a list of characteristics which (some) properties happen to have.

For me the most important characteristic of a "property" is that it is associated one-to-one with a specific object, rather than (as with variables) with a specific scope, and secondarily that invoking a function via a property access binds the function's this (unless it is already bound). These seem much more fundamental to me than how they're treated by Object.assign. This makes #fields seem much more like properties than variables, to me.


I anticipate others will have similar expectations and slip ups when they hear private properties landed. I have however gotten a better grasp of what this proposal aims for and appreciate the discussion!

Some of that seems inevitable, unfortunately. We'd like to do our best to minimize it, of course; I think the current proposal does the best job that reasonably can be done with that without making the feature much less good. Anyway, happy to chat about the design decisions here (and to continue to do so), and I'm glad I could help explain them a bit.

@MichaelTheriot
Copy link

MichaelTheriot commented Mar 20, 2018

@bakkot

To me, those are (sometimes) things that it happens you can do with properties, but I don't really think of them as being fundamental to what properties are, and I don't think most other programmers do either.

Most programmers probably do not; these behaviors altogether are pretty unique specifically to this language. Here are behaviors for properties generally not present in most languages (altogether):

  • operators (in, delete)
  • syntax ([] notation)
  • methods that define / configure properties (Object.defineProperty(), etc)
  • and loose naming rules

Calling something that does not fit this profile a property raises flags to me. If we ignore these and think of JS like a static language, sure, but it simply is not and I anticipate among newcomers and experienced developers alike this will be more confusing than it needs to be.

Not all public properties can be deleted.
Not all public properties are copied by Object.assign.

True, but deleting a public property is never a syntax error like it would be for these. Any mishap in deletion or properties missing in iteration are run-time behaviors explicitly configured by the programmer.

Not really sure what this means.

By my examples I meant to convey:

  • Public properties can use any string value as keys. Private cannot.
  • Public properties can use Symbols as keys. Private cannot.
  • Public properties can be defined and accessed with a variable keys. Private cannot.

Every restriction I am listing is actually already present on variables.


Though you don't mention it, .length on arrays is another kind of property still, being a property which claims per getOwnPropertyDescriptor to be a regular data property but such that changing it has side effects.

That is a quirk that I definitely consider a missed opportunity to catch when Object.getOwnPropertyDescriptor() landed. For what it's worth you can at least implement an Array yourself that exhibits this behavior by using a Proxy. 😕

@bakkot
Copy link
Contributor

bakkot commented Mar 20, 2018

Here are behaviors for properties generally not present in most languages (altogether) [...] Calling something that does not fit this profile a property raises flags to me.

Wait, where does this intuition come from? I have exactly the opposite intuition: the characteristics which are a priori most core to being a "property" are those which are the most in common with "properties" in other languages, whereas details like delete semantics are just JavaScript-specific extensions to what you can do with its properties. That in JavaScript you happen to be able to (for example) use delete with at worst a runtime error for all the kinds of properties currently in the language doesn't really imply that this is a necessary characteristic of anything we might reasonably call a property. It just... happens to be the case currently.

True, but deleting a public property is never a syntax error like it would be for these.

Sure, because you don't have enough information to know ahead of time that the property in question is not configurable. Here you do, and we usually feel it's better to get errors as early as possible.

This does not feel, to me, like it has anything to do with whether it is "a property". It's just a property which the language is able to make more guarantees about than usual.

Any mishap in deletion or properties missing in iteration are run-time behaviors explicitly configured by the programmer.

I don't really buy "explicitly configured by the programmer", especially for things like unscopables, nor why it would matter. The point is just that not all properties have these characteristics: programmers already have the power to make a property which is invisible to for in, for example. That there is now another kind of property with these characteristics which some properties already had doesn't seem like it should be that much of a big deal.

@MichaelTheriot
Copy link

details like delete semantics are just JavaScript-specific extensions

These JS-specific "extensions" are core to the language. In JS, "static-like" properties are an extension. Naturally, something claiming to be a property but being largely inconsistent with properties as already defined in the language comes off confusing.

Sure, because you don't have enough information to know ahead of time that the property in question is not configurable. Here you do, and we usually feel it's better to get errors as early as possible.

I'm an outsider here; I do not know what to say to "we usually feel..." or what weight it carries. All I can say is this is a duck-typed language that is constantly guarding against run-time errors, and this would be an exception to the rule. For what it's worth an avenue does exist to check if a property can be deleted at run-time.

That there is now another kind of property with these characteristics which some properties already had doesn't seem like it should be that much of a big deal.

I think it is potentially a big deal to call these private properties, give it a syntax similar to properties, and have no property operators, bracket notation, or descriptor methods work here. The next question is naturally, why is the rest of JS not like this? Why this oddball in an otherwise dynamic language?

@bakkot
Copy link
Contributor

bakkot commented Mar 20, 2018

These JS-specific "extensions" are core to the language. In JS, "static-like" properties are an extension. Naturally, something claiming to be a property but being largely inconsistent with properties as already defined in the language comes off confusing.

I really do not agree, but I doubt we're going to be able to convince each other here. They are inconsistent in a few ways, but there are already many inconsistencies in how various things called "property" work, and people seem to be able to work with that just fine.

All I can say is this is a duck-typed language that is constantly guarding against run-time errors, and this would be an exception to the rule.

There are dozens of other early errors in the language. For example, delete x is a syntax error in strict mode (even though it sometimes has effects in sloppy mode), calling super outside a constructor is a syntax error, importing a name which does not exist is a syntax error, 0++ is a syntax error, etc.

Any of those could have been runtime errors, but because there is enough information to statically detect them they're instead syntax errors. I don't think this rule you refer to exists.

I think it is potentially a big deal to call these private properties, give it a syntax similar to properties, and have no property operators, bracket notation, or descriptor methods work here.

Why is it any more of a big deal than to have properties which don't show up in for in, which are not copied by Object.assign, or which with does not introduce as bindings?

@trusktr
Copy link

trusktr commented Mar 22, 2018

existing paradigms in Javascript.

Yes, please, let's not forget this!

If we're in the scope of the private or protected instance, we should be able to use any existing language features, f.e. for..in or Object.keys.

Personally, i like the prefix notation, f.e.

this.foo
@this.bar // protected
#this.baz // private

because, then

Object.keys(#this)
for ( const key in #this ) {}
const { bar } = @this

which is similar to my inheritance implementation, lowclass, where you can do

this.foo
protected(this).bar
private(this).baz

Object.keys( private(this) )
for ( const key in private(this) ) {}
const { bar } = protected(this)

We should be able to do literally anything we can do nowadays with public properties.

More examples, using notation:

const key = makeSomeString()
#this[ key ] = 'foo'

The same with Lowclass:

const key = makeSomeString() // for example, a UUID, or literally anything
private(this)[ key ] = 'foo'

With notation:

console.assert( 'bar' in @this )

With Lowclass:

console.assert( 'bar' in protected(this) )

At bare minimum, let's let these new features work just like public does, even if they work separately, where by "separately" I mean we can't use for..in on all of them at once, f.e.:

for ( const key in this ) {} // only public keys

// we have to do it on them separately:
for ( const key in #this ) {} // private keys
for ( const key in @this ) {} // protected keys

With Lowclass:

for ( const key in this ) {} // only public keys

// we have to do it on them separately:
for ( const key in private(this) ) {} // private keys
for ( const key in protected(this) ) {} // protected keys

Let's let people have the same flexibility they do today with public stuff.

In Lowclass, we can also leak protected or private scope purposefully if we want:

// FooBar.js

let fooPrivate

const Foo = Class((public, protected, private) => {
  fooPrivate = private
  
  private.foo = "foo"
})

const Bar = Foo.subclass((public, protected, private) => ({
  test() {
    console.log(fooPrivate(this).foo) // "foo"
    console.log(private(this).foo) // "bar"
  },
  private: {
    foo: "bar"
  }
}))

export { Foo, Bar }
import { Bar } from './FooBar.js'

const bar = new Bar
bar.test()

// output:
foo
bar

That example is here in the lowclass tests.

The following is just a stretch, but maybe it is possible to have even that previous flexibility with the builtin feature:

class Foo {
  private as fooPrivate // makes a variable fooPrivate in the same scope as the Foo variable
  private foo = "foo"
}

class Bar extends Foo {
  private foo = "bar"

  test() {
    console.assert( fooPrivate(this).foo === 'foo' )
    console.assert( #this.foo === 'bar' )
  }
}

const bar = new Bar
bar.test()

or even

let fooPrivate

class Foo {
  private foo = "foo"

  constructor() {
    fooPrivate = #this // expose specific private instance
    fooPrivate = private(this) // lowclass version
  }
}

new Foo
console.assert( fooPrivate === 'foo' )

It would not be possible to do the following:

let fooPrivate
Foo.prototype.hack = function() { fooPrivate = #this }
bar.hack() // maybe throws an error
console.log( fooPrivate ) // or undefined

in lowclass:

let fooPrivate
Foo.prototype.hack = function() { fooPrivate = private(this) }
bar.hack() // error, private is undefined

In lowclass, protected(this) returns the protected prototype chain. private(this) returns the private prototype chain, but it is much different than the protected prototype chain.

this // leaf of the public prototype chain
protected(this) // leaf of the protected prototype chain
private(this) // leaf of the private prototype chain

Any given instance has exactly 1 public prototype chain and 1 protected chain, but has as many private chains as there are number of classes in the instance's inheritance hierarchy.

For example, suppose we have this class hierarchy: Baz extends Bar extends Foo, then this is what the prototypes for an instance of Baz looks like:

Public prototype chain, the `this` we all know (the public object returned from
`new Baz`):

     +------+          +-----+          +-----+          +-----+
     | this |<---------| Baz |<---------| Bar |<---------| Foo |
     +------+          +-----+          +-----+          +-----+


Protected prototype chain, returned from `protected(this)` call in the code of
any class in the hierarchy (Foo, Bar, Baz):

     +-----------------+          +-----+          +-----+          +-----+
     | protected(this) |<---------| Baz |<---------| Bar |<---------| Foo |
     +-----------------+          +-----+          +-----+          +-----+


There are multiple private prototype chains, one per class. The call to `private(this)`
in the code of a given class returns the two-object-long private chain
specifically associated with that class:

     +-----------------+          +-----+
     | private(this)   |<---------| Baz |   // this one is returned if private(this) is called inside Baz code
     +-----------------+          +-----+

     +-----------------+          +-----+
     | private(this)   |<---------| Bar |   // this one is returned if private(this) is called inside Bar code
     +-----------------+          +-----+

     +-----------------+          +-----+
     | private(this)   |<---------| Foo |   // this one is returned if private(this) is called inside Foo code
     +-----------------+          +-----+

In the public prototype chain, Baz, Bar, and Foo in the diagram are the prototypes we all know, accessible on Baz.prototype, Bar.prototype, and Foo.prototype, respectively.

In the public chain of the instance, Baz, Bar, and Foo contain the protected properties and methods that were defined in their respective lowclass Class definitions.

In the private prototype chains, Baz, Bar, and Foo contain the private properties and methods that were defined in the respective class definitions, but instead of connecting them all together in one long chain, a new private instance is made for each prototype associated with each class (instead of just a single instance at the front of the chain like with protected).

Just throwing the idea out there to show what's possible and to show how (conceptually, not literally) it may be possible to "desugar" protected and private sigils to this multi-prototype form.

A current downside of lowclass is that protected(this) returns the leaf of the chain, so it is possible that someone can take advantage of that to modify the prototype of all instances in the entire application that inherit from some base class. This should not be possible with the builtin implementation.

@trusktr
Copy link

trusktr commented Mar 22, 2018

In my above example, maybe & doesn't work well because it is a bitwise operator. Just replace it with some other symbol.

Updated the above to use @. Personally, because I think protected will be more commonly used than private, maybe give protected the better symbol and private the at-sign:

#this.bar // protected
@this.baz // private

The & symbol is nicer than @ though. Could it be used? Not sure if it has any conflicts with the bitwise operator in some case.

Or maybe just

#this.bar // protected
##this.baz // private
// hmm, I think I like this.

With prefix form, the . operator isn't so awkward, making #this seem like the "private version of this", then accessing some property like normal. In fact, many devs I know say "this dot that", and placing the # after the . gets in the way of that concept.

@seansd-zz
Copy link

seansd-zz commented Apr 9, 2018

I'm super excited just to have class fields . . . but with that said (yeah poo sandwich i know ;-) ). . .

My biggest problem with the field proposal (especially for private), is in 'class' syntax being forced to use the 'this' key word which is completely unsafe. . . as an example:

'use strict';

class Point2D {

    #x = 0;
    #y = 0;
    #_str = '';

    constructor(x, y) {
    
        this.#x = x;
        this.#y = y;  // here this is safe to use b/c we know it has to be a 'new' object
    }
    
    get x() { 
        return this.#x; 
    }
    
    set x(v) {
        if (typeof v === 'number') {
            this.#x = v;
        }
        return false;
    }
 
   toString() { 
       return this.#x + ', ' + this.#y;
   }
    /* in both the getter/setter methods and toString method above, 
     * there is NO GUARANTEE that 'this' is defined
     * or that 'this' is an instance of Point2D
    */
       
    /// . . . 
    
}

I'd be fine with requiring '#x', but not 'this.#x'. The previous sytnax would basically for the engine to assume an implied 'this' pointer / object and do the validation for the developer. . it could thrown an error automatically for us when 'this' isn't defined or isn't a direct instance of an given class. . . othewise as developers in order to ensure safety we'd have to do that kind of check ourselves in EVERY single method call. . .which is super cumbersome and painful. . .

@bakkot
Copy link
Contributor

bakkot commented Apr 9, 2018

@seansd, the currently specified semantics are that if the engine executes obj.#x and finds that obj does not have #x - that is, obj was not produced by the class - then a TypeError is thrown. You don't have to do that check yourself.

@littledan
Copy link
Member

OK, seems like @shannon and I agreed in #75 (comment) that it would be fine to pursue this as a follow-on proposal. At the same time, I'd like to keep the current class fields proposal minimal; it's already at Stage 3, so it's somewhat late to go adding additional features. See CONTRIBUTING.md for some details on how to get started. Closing this issue, but feel free to follow up here for help getting started.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants