Giter Site home page Giter Site logo

proposal-private-methods's Introduction

This proposal has been merged into the class fields proposal.

Please see that repository for current status. The rest of this repository is left up as a historical archive. The old readme is available here, but will not be kept up to date. Similarly, while the FAQ in this repository will continue to exist as is, it will not be updated; its canonical location incluing any updates is in the class fields repository.

proposal-private-methods's People

Contributors

antleblanc avatar bakkot avatar bherrero avatar caiolima avatar koba04 avatar littledan avatar ljharb avatar mkubilayk avatar ms2ger avatar nicolo-ribaudo avatar robpalme avatar rwaldron avatar tim-mc avatar tjcrowder avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proposal-private-methods's Issues

Writing the same method value to a private method should work

Example:

class C {
  #method { }
  method() { this.#method = this.#method; }
}

The current spec text would cause a TypeError to be thrown from new C().method() (and that was true before the recent refactoring as well), but I think we should silently accept this, following the way property descriptors work.

Thoughts?

Should private methods be auto-bound?

We "missed our chance" with public methods, but auto-bound methods are a big feature request. Should we make private methods auto-bound? This was suggested on Twitter by codemix, though @thejameskyle noted there,

That's overhead for instantiation and it breaks the pattern already set up. Performance and consistency are valuable

I wasn't there for the ES6 class discussion; do the arguments made there against auto-bound methods apply equally to private methods? cc @arv @dslomov

Using mask, masked or local when declaring private methods

So after reading about this proposed change, and then having a read through various comments in the issues section of this repo, few things have become apparent to me:

  1. There is a substantially larger dislike of using # as a way to define values within a class.
  2. private can occasionally be a misnomer, since other classical languages use it to mean different things.
  3. People (including me) have a hard time being decisive about what they actually want, and are less quick to adapt larger deviations in terms of syntax.

My proposed changes:

I might be a small part of a minority (not sure yet), but as a developer I have no issues accessing hidden scopes using #. This makes more sense to me in terms of being able to know immediately if a method or static method is calling conditionally accessible code.

Since private seems to be the pivotal issue (in terms of syntax), I decided that a keyword masked would fit the bill due to it being more readable and less jarring to existing developers.

Example

class Counter extends HTMLElement {
  masked xValue = 0;

  get masked x() { return #xValue; }
  set masked x(value) {
    this.#xValue = value; 
    window.requestAnimationFrame(this.#render.bind(this));
  }
  
  // potentially even the below
  masked x get()
  masked x set()

  masked clicked() {
    this.#x++;
  }

  constructor() {
    super();
    this.onclick = this.#clicked.bind(this);
  }

  connectedCallback() { this.#render(); }

  masked render() {
    this.textContent = this.#x.toString();
  }
}
window.customElements.define('num-counter', Counter);

Why masked ?

A more obvious keyword like hidden obfuscates the actual syntax required to access the value. Masked makes it intentionally more clear that the value is present but requires specific intentions to access, which in this case would be the # keyword.

An argument regarding syntax parsers.

There is a substantial upside to using a named keyword such as mask or masked or rather any unique keyword, which is the accessibility for less sophisticated parsers to analyze code (including grepping and editor searching). Having the # keyword exist as both the declarative statement and the way to access properties is slightly unintuitive.

To be more specific, I worry about the ambiguity this holds against something more standardized like static, as it allows us to specifically detect the amount of times static values have been declared within a class. The same does not hold true to this specification.

Final thoughts (as an aside)

Like I assume most of the people here do, I use this language daily at my place of work. Being so frequently exposed to a language causes one to make certain expectations about how it looks and the way it feels.

Over the last 4, maybe 5 years there has been a substantial growth in the additions to JavaScript's features, which has enabled use to solve problems using powerful patterns (class, extends, spread operators, and async await come directly to mind).

That being said, this kind of exponential growth can also lead to a ghastly feature creep, or in this case, having proposals pass through multiple stages that feel not in par with the overall design of the language. I might just be another drop in the sea but this sort of feature (and the implications it presents) can have a lasting effect on many workflows, and to be completely honest I do not believe this feature to be ready for stage 3.

I've been wanting private class methods for so long, so I'm looking forward to whatever form it takes when it reaches stage 4, though I'm really hoping it's the one that the majority of us actually want to use.

Editorial: how should private methods and accessors be specified?

In the current specification, the logic is that a private method is like a lexically scoped variable. In particular, the lexically scoped internal "private name" has an internal slot (never mutated) which contains the actual method or getter/setter pair.

If we switch to doing brand checks for methods, I see two ways we could spec this (thanks to @bakkot for discussing this with me earlier):

  • A parallel representation with brand, on top of lexical scoping: In addition to the representation described above, each object could have an internal slot containing a List of Private Names, where each of these is the name of a method or accessor. Private names are added to the list just before field initializers run. When accessing a method or getter/setter pair, check the list first to see if the name is included, and throw a ReferenceError otherwise.
  • Non-writable private instance fields: Here, a private method is just a private field which is non-writable. Maybe private fields would reuse the object infrastructure for property descriptors internally, even if that's not exposed to users. They would all hold the same function identity. For accessors, those would similarly be represented internally as (own) private fields which are accessors in the same list. The non-writability/non-configurability makes these shared-like, even if they are actually own.

Ultimately, the differences are unobservable, but might have implications for what one might expect to happen in future revisions of the language. Either way, all of the private methods have to be added before any of the instance methods, so that the private methods can be called from initializers which come earlier textually. Which is better?

cc @allenwb @erights

Implementability without per-instance memory overhead

I was talking to @gsathya and learned that this proposal seems to bake in semantics that require per-instance memory overhead for each method. In particular, the methods have identity, as exhibited by this code:

class C {
  #m(other) {
    return this.#m === other.#m;
  }
  
  public(x) {
    return this.#m(x);
  }
}

assert((new C()).public(new C()) === false);

This means that using private # methods, as opposed to pseudo-private _ methods of the type we use today, actually causes your code to use more memory. This seems like it will steer library authors and developers away from this new language feature.

In particular, it forces library authors to make a choice: they can get strong encapsulation, but only at the price of making apps that consume that library more memory-heavy. I don't know what I would choose, as a library author.

As an app developer, I'd anticipate that I'd want to use some transpilation tool to remove all # methods from my app's codebase, converting them to _ methods. This seems like it will quickly become best practice for deploying production apps, in the same way that using a minifier is today.


It gets worse though: this has infectious effects on the private fields proposal.

Let's say I want to write a class that has a private field and private method. I start out writing it using new language features

class D {
  #field = 5;

  public() {
    return this.#m();
  }

  #m() {
    return this.#field;
  }
}

Let's also say that I as a library author am very concerned about encapsulation, so switching to _-private methods and fields is not good enough. But, I am also concerned about not burdening my consumers with higher memory usage.

After some thinking, the solution I arrive at is this:

const field = new WeakMap();

class DPrime {
  constructor() {
    field.set(this, 5);
  }

  public() {
    return DPrime_m(this);
  }
}

function DPrime_m(instance) {
  return field.get(instance);
}

I've now ensured that I don't pay a per-instance cost for my method, and maintained strong encapsulation. But the only way I was able to do that was by not only removing usage of #-private methods, but also removing usage of #-private fields. (Since without #-private methods, I can't access the #-private fields.)


This worries me significantly. Assuming there is not some clever optimization trick that will allow the private methods to have different identity without per-instance overhead, I think we should consider a different design for private methods. (Perhaps some kind of private prototype chain, so they can be shared across instances in the same way that public methods are.)

This proposal does not address the actually existing needs for private fields

I'm aware that much of this has been covered in the FAQ, but I still want to chime in:

The only good reason for this (IMO ugly, but that's a matter of taste) syntax seems to be the complications with what is accessible from "this" and the desire to give other objects in the same class access to the private properties.

But this idea that other objects of the same class should have access to the private fields of an object is what is very non-Javascript.

JavaScript objects don't really have classes, they have inheritable prototypes. All it takes for another object to become "of the same class" is to set its __proto__ property to some class's prototype. Which could be considered "dirty", but is also very common across libraries. All it takes to add a method to a "class" is to add a function member to the class's prototype. So protection by classes is ineffective and meaningless.

What Javascript programmers traditionally do is restrict access to data by scope, not by class. This is why there is a very common pattern that goes like this:

function myFactory () {
  var foo = "bar";
  var myNewObject = {
     doSomethingWithFoo() {
        // foo is accessible here, but nowhere outside the myFactory function
        doSomething(foo);
     }
  }
  return myNewObject;
}

This is what Javascript programmers call "private" variables. It is not the same thing as in Java or other programming languages. But it is very useful for our use cases , because it prevents programmatic access to these "private" variables and thus protects the information from other, potentially malicious scripts outside our control, in the same window (if we're talking about browsers).

The pattern is very tedious to write, and cannot be easily (or at all?) converted to using classes. A proposal that would solve this problem, and not the problem of making Javascript classes more like
classes in other languages, would be preferable.

What we really need is this:

class myClass() {
  private foo = "bar";
  doSomethingWithFoo() {
     // foo is accessible here, but not accessible to methods of this or any other object
     // defined outside the class definition 
     doSomething(foo);
   }
} 

Consider prototype chain walk for private get/set

In the September TC39 meeting we discussed issues with private static fields, e.g.:

class Base {
  static #field = 1;
  static get() { return this.#field; }
  static set(value) { this.#field = value; }
}
class Sub extends Base {
}
Sub.get(); // error
Sub.set(2); // error

The reason for the error is that the receiver is Sub, which does not have a #field defined on it. One possible approach is to allow for a prototype walk just as we do for any other property lookup. We could support this by splitting private members into several parts:

  • Space reservation for a private name on an object, indicating whether it is addressable when the object is the receiver.
  • Definition of a private member on an object.

Then we would apply the following semantics, given private name P:

  • non-static private methods/accessors:
    • During class declaration evaluation:
      • Add a reservation on the class prototype for P with the value "reserved".
      • Add a private property definition to the class prototype for P.
    • During construction:
      • Add a reservation on the instance for P with the value "addressable".
  • non-static private fields:
    • During construction:
      • Add a reservation on the instance for P with the value "addressable".
      • Add a private property definition to the instance for P.
  • static private methods/accessors and fields:
    • During class declaration evaluation:
      • Add a reservation on the class constructor for P with the value "addressable".
      • Add a private property definition to the class constructor for P.
      • If the class is a subclass, Add a reservation on the class constructor for each "addressable" private name on the superclass constructor.

Then, when performing a get or set of a private property P on object O, with receiver Receiver:

  1. If O does not have a reservation for P, throw a TypeError exception.
  2. Let ownDesc be the private property definition for P on O.
  3. If ownDesc is undefined, perform this operation on O.[[GetPrototypeOf]]() instead as O and passing P and Receiver as is.
  4. If Receiver does not have a reservation for P, or the reservation for P on Receiver is not "addressable" throw a TypeError exception.
  5. Perform the remaining get or set steps.

This would result in the following behavior:

  • non-static private methods/accessors would be defined on the prototype, but cannot be accessed directly (e.g. C.prototype.#method() is an error). This keeps the observable semantics the same as the current spec.
  • non-static fields are defined on the instance, as per the current spec.
  • static private methods, accessors, and fields are defined on the constructor.
  • static methods on a subclass can use this and have the same semantics for private fields that they do for public fields.
  • Calling Sub.get() above would succeed, returning the value of Base.#field.
  • Calling Sub.set(2) above would succeed, setting the value of Sub.#field to 2, but leaving Base.#field as 1.

[Proposal] Independent private field

e.g: code

const logName = function () {
    console.log(private.name);
};

class A {

    name = 'public name';
    private name = 'private name';
    // private value = 'private value'; [1]

    constructor() {
        private.value = 'private value'; // like [1]
        private.logName(); // `private.` like `this.`, but cannot access by instance.
        logName.call(this); // 'private name'
    }

    private logName() {
        console.log(private.name);
    }

}

const a = new A();
console.log(a.name); // 'public name'
console.log(a.logName); // undefined
console.log(A.prototype.logName); // undefined

A.prototype.logName = function () {
    console.log(`${this.name} + ${private.name}`);
};
a.logName(); // 'public name + private name'

Should private methods be writable?

Users may want to use idioms like this:

class C {
  #callback() { /*...*/ }
  constructor() { this.#callback = this.#callback.bind(this); }
}

Such an idiom will not work in the current private methods proposal because private methods are non-writable. Should this be changed? Being non-writable was intended to contribute to a feeling of being "shared".

Thanks to @bmeck for raising the issue.

What are some use cases for private accessors?

What's the main use case of a private accessor? Most compelling uses of an accessor I've seen is that it helps provide an abstraction over the implementation details (trigger a redraw or setting a dirty bit) from the users. In the case of private accessors, they can only be used inside a class body that declares the accessor.. there isn't a whole lot of abstraction provided here.

If I want to refactor an existing private field into an private accessor, I could, instead, change it to a private method and then changing all the users of the private field is straightforward as they occur within the class body. This wouldn't be true for changing public fields into accessors as their users would be spread out through the codebase.

Private accessors add to already complex mental model of classes (with all the new features). Are class bodies really complex enough to require the extra ergonomics of private accessors?

Are properties defined after private methods?

class Foo {
  publicField = this.#foo()
  #privateField = this.#foo();

  #foo() {
  }
}

I imagine, like prototype methods, private "own" methods are defined before properties are evaluated? So it'd be something like:

class Foo {
  constructor() {
    super();
    // define private methods
    // define properties

    // user code
  }
}

Allowing access without invocation

I believe @allenwb is suggesting here that it should only be possible to invoke a private method, not to dereference it in general.

This is an interesting idea, whether or not it's what he meant to suggest. This is certainly something which could be enforced statically. It would make private methods even more static, which may be a benefit or a downside depending on your perspective. It would also avoid this potential problem where you might expect a brand check in two places.

If we didn't have spread arguments I'd be strongly opposed, because of the need for this.#m.apply(this, args). But as it is I can't see much reason to allow dereference, except for symmetry with the rest of the language (which not a small reason). It does not cause any loss of functionality, since if someone actually wants a function object stored in a private field they are still free to specify that explicitly - const f = function(){}; class A { #m = f; }.

Why not use private keyword instead of #?

I'm sure you have taken this into account somehow, but I can't find discussion or reasoning for it in my research.

Why use the # prefix?
Why not use the private keyword?

I'd imagine you would use it somewhat like this:

class Foo {
  private bar() {
    // ...
  }

  constructor() {
    private.bar();
  }
}

Here we use the private keyword, both when defining the method, and when referencing the it.

As far as I can tell, this would solve the problem of having them being accessible on the instance of the class.

We already do something similar when referencing methods of a superclass, using the super keyword.

I find this to be more expressive, and more familiar.

Clarify behavior of private static methods

The explainer contains no mention of whether or not this proposal would support private static methods (maybe that's a result of the current contention over private static fields), nor was I able to deduce the answer from the proposed spec changes.

This would be a useful thing to make clearer in the explainer.

Getter-only #-name accessors should throw in strict mode

I believe the intention is to align with public properties on this. Currently,

function thisDoesntThrow() {
  class C { get foo() { return 42; } }
  let c = new C;
  c.foo = "bar";
}

function thisThrows() {
  "use strict";
  class C { get foo() { return 42; } }
  let c = new C;
  c.foo = "bar";
};

The current spec doesn't have this behavior for #foo. https://tc39.github.io/proposal-private-methods/#sec-privatefieldset returns false instead of throwing, but the caller PutValue doesn't check the return boolean.

Are private own methods also defined on the prototype?

InitializeClassElements step 5.b defines the classes' methods as follows:

  • If their placement is "static", they are defined on the class
  • Otherwise (if their placement is "prototype" or "own") they are defined on the prototype.

Private methods on the prototype are probably not observable, but using decorators someone could define a public own method: should it be defined only on the instance or also on the prototype?

Should accessing a private name of incorrect type be an early error?

We throw an early error if a private name is accessed but not declared,

class A {
  foo() {
    this.#a = 1; // this throws an early error
  }
}

For private accessors, we don't throw an early error for the following:

class A {
  get #a() {}
  foo() {
    this.#a = 1; // this is a runtime error
  }
}

The engine statically knows whether a private setter or getter is available during parsing so this could be an early error. Should it?

Reasons for why it should be an early error:

  • If it can be an early error, it should be as it helps developers catch these bugs sooner
  • Consistent with private fields

Reasons for not having it be an early error:

  • Not consistent with public accessors

Any other reasons?

Use export keyword

Did anyone consider exporting the public functions instead? Wouldn't that be more consistent with the rest of the language? But i guess since classes already are here we would need some syntax on the class level to say that everything is not public.

closed class Point {
  somePrivate = 0;

  myPrivateFunction() {
    return this.somePrivate;
  }

  export myPublicFunction() {
    return this.myPrivateFunction();
  }
}

A SyntaxError if class contains private method and private static field with same name?

While reviewing tests contributed to Test262, I encountered a test that I initially interpreted as invalid. In order to verify, I revisited the relevant specification and determined that the language there indicates that the test is valid, but I also believe that the language may be unintentional.

The following is a reduced test case, which is expected to result in an early (parse time) SyntaxError:

var C = class {
  static #m;
  #m() {}
};

The spec language that appears to support this:

It is a Syntax Error if PrivateBoundNames of ClassBody contains any duplicate entries, unless the name is used once for a getter and once for a setter and in no other entries.

Found here: proposal-private-methods/#sec-static-semantics-early-errors, which is a modified early error which is initially introduced here: proposal-class-fields/#prod-ClassBody.

The issue being that ClassBody now expands as:

ClassBody :
ย ย ย ย ClassElementList

ClassElementList :
ย ย ย ย ClassElement
ย ย ย ย ClassElementList ClassElement

ClassElement :
ย ย ย ย MethodDefinition
ย ย ย ย static MethodDefinition
ย ย ย ย FieldDefinition ;
ย ย ย ย static FieldDefinition ;
;

FieldDefinition :
ย ย ย ย ClassElementName Initializer opt

ClassElementName :
ย ย ย ย PropertyName
ย ย ย ย PrivateName

PrivateName ::
ย ย ย ย # IdentifierName

...So it seems like this is accidentally including all productions where PrivateName may occur in ClassBodyโ€”however, following Static Semantics: PrivateBoundNames to Static Semantics: PropName the spec defines PropName such that it returns empty for any PrivateNameโ€”and this is where I determined that I needed some clarification ;)

Is it the intention of the authors to disallow the following:

class C {
  static #m;
  #m() {}
}
class C {
  static #m() {};
  #m() {}
}

...and all permutations, but specifically:

Is it a SyntaxError if a class contains both a private method or private field, and a private static field or private static method, with same name?

Using # as part of the name of the field is not javascripty

I can understand that private varaibleName breaks backwards compatibility because private hasn't been a keyword (for some reason) and so people might be using it as a variable name. I can understand using syntax like let #a = 'whatever' in that context, even tho it's super ugly.

But it absolutely makes no sense to make the # mark a part of the identifier itself. Nowhere else in javascript do we do this. This isn't perl folks. The identifier should be just a name. We can all agree the following would look absurd:

class A {
  const x = 5
  setX(value) {
    this.(const x) = value
  }
}

But that's basically what you're doing with the hash mark. Just replace const with #:

class A {
  #x = 5
  setX(value) {
    this.#x = value
  }
}

In my opinion the above is just as absurd as the first example. Why make the # part of the identifier? Why not do the following?

class A {
  #x = 5
  setX(value) {
    this.x = value // No hash mark needed (or wanted)
  }
}

Or better yet, why even use the hashmark when you can do the thing everyone is already familiar with:

class A {
  private x = 5
  setX(value) {
    this.x = value // No hash mark needed (or wanted)
  }
}

The above should not break backwards compatibility because the 'private' is in the top-level of the class, which must have one of the following forms:

  • private name ...
  • private declarator name ...
  • declarator private name ...
  • name(...) ...

So even tho private can be used as a name, it should be entirely unambiguous in this context. So why aren't we doing this?

But number 1, please do not make the # mark part of the identifier. I don't want to have to be doing myInstance.#thing all over the place. Its absolutely fugly and surprising in all the worst ways.

Do we need private accessors?

Private methods have a clear use case--manipulating data held in private fields--but private accessors seem a little more borderline. When everything is in the same class body, and when there are no decorators creating accessors under the hood, are private accessors sufficiently motivated? It seems like you'd often be able to just call a private method instead at not much loss to ergonomics.

Thanks to @ajklein for raising this issue.

Private function and var naming with # looks so odd, can we use __ instead

Simply put, didn't like the # naming at all. Could we use __ instead of #? Not in the favour of too verbose private x or @private x; but some other symbol in front will make it look better IMO. This has been used in angularjs and few other libs before.

class MyClass {
    __xValue = 0;

    get __x() { return __xValue; }
    set __x(value) {
       this.__xValue = value;
    }

    __clicked() {
        this.__x++;
    }
}

Review comments: 2.7 ClassDefinitionEvaluation

This is my first time doing formal review of a sizable spec; let me know if there's a better workflow I should use than just filing issues. I'm currently reading through ClassDefinitionEvaluation, and the below is a variety of issues I found:

  1. When invoking ClassElementEvaluation. step 27.c passes true for enumerable. I believe this should be false.

  2. Step 28 reads: "Let elements be elements with getters and setters of the same key coalesced, with later declarations overriding earlier ones." I think I understand what you mean by "coalesced" (though I think this could be improved by spelling it out), but I don't understand the second part of the sentence ("with later declarations..."), since there should be at most one getter and one setter for a given key, and the two should have all the same descriptor attributes save [[Get]] and [[Set]].

  3. Can the invocation of InitializeClassElements at step 33 fail? I'm having trouble seeing how, yet I see a ? preceding it. I'm likely missing something though.

  4. While on the topic of InitializeClassElements, the Assert in step 3 says "proto is F.[[Prototype]]", but presumably you mean that proto is identical to Get(F, "prototype")?

this.#value vs #value - short form of writing

class Counter extends HTMLElement {
  #xValue = 0;

  get #x() { return #xValue; }
  set #x(value) {
    #xValue = value; 
    window.requestAnimationFrame(#render.bind(this));
  }

  #clicked() {
    #x++;
  }

  constructor() {
    super();
    onclick = #clicked.bind(this);
  }

  connectedCallback() { #render(); }

  #render() {
    textContent = #x.toString();
  }
}
window.customElements.define('num-counter', Counter);

out of sync: PrivateFieldValues vs PrivateFieldDescriptors

this repo is out of sync with the class fields proposal.

One here it uses PrivateFieldDescriptors while the other uses PrivateFieldValues. Other similar names follow the same issue.

A synchronization between both proposals supports comprehension of the intended usage.

I'm not sure if this is related to #40.

Final change suggestion

@littledan This thread will contain the last suggestions I'm going to make. I'm taking into account your issue with [] being "two completely different meanings... punned onto the same token". My new suggestion is a compromise. You've currently got 3 completely different meaning punned onto #. If my approach is bad in your eyes, your's is logically worse. So try this:

  1. Don't use internal slots. Keeping class as syntactic sugar should be a primary goal.
  2. Let # act only as a "private scope access" token. That way it has exactly 1 meaning in the context of a class.
  3. Use private #name for declarations. Since the sigil is a "private scope access" token, it cannot be used alone to declare, hence private.
  4. No shortcut syntax (#x) for access notation. Consider that if a variable is not declared, but used within a particular scope, it is treated as though it extends [[Global]]. Since # cannot declare, by default it would try to access [[Global]].#x which does not exist and cannot be dynamically created, especially since it's outside of any class definition of [[Global]] would need to be, but wouldn't be an instance. Thus, naturally a TypeError.

These 4 changes to your proposal would directly address all but 1 of my objections to your proposal. The last issue (the use of this.#x without allowing either this["#x"] or this[#x]) is indirectly resolved by (2). Since the # is a "private scope access" token, this.#x resolves to this.[[PrivateScope]].x. As such, the key name "x" is on the [[PrivateScope]] which is inconsistent with this["#x"] where the key name is "#x". Further, since with this[#x] the key name is not a string, but an impossible variable name due to (4), it is also invalid. This means the new suggestion is completely in keeping with your proposal and objections while removing the issues I outlined in my previous suggestion.

Weird coalescing behaviors

Summarizing f719e00:

  • If other
    • If method is a getter, assert other has a setter
    • Else if method is a setter, assert other has a getter

That means the following is fine:

class Test {
  get #field() {}
  set #field(v) {}
  get #field(v) {}
}

But this throws:

class Test {
  get #field() {}
  get #field() {}
  set #field(v) {}
}

Yet another approach to a more JS-like syntax

I realize I might be beating a dead horse, but it seems to me that I'm far from being the only one who intensely dislikes the use of # for private fields/methods. So here goes:

  1. Use the private keyword to introduce private field, methods, setters, getters, exactly as with static.
  2. Use bare field and method names without any qualifiers to access private fields and methods within the class's methods, just like we do now in factory functions, i.e. treat them like variables.
  3. Use private.name to explicitly access the private fields of this object, if there are conflicting names, just like we do with super. Also use private["name"] for non-ident and calculated field names.
  4. Use private(otherObject).name or private(otherObject)["name"]to access the private fields of another object of the same class. For symmetry, private(this).x is the same thing as private(this)["x"] private.x or private["x"] or just x if there's no variable x in scope.
  5. In methods, private and private(otherObject) are not valid constructs without being followed by .field or ["field"] and throw syntax errors.

AFAICT, this achieves all the requirements, without introducing a wildly asymmetrical new syntax for private fields, AND gives us a natural way of reading code in human language, solving the naming problem that was brought up on other threads.

A runtime solution for private/protected fields in ES6.

Let me be clear about 1 thing:
This is not a proposed alternative.

The code below represents working ES6 logic to implement both private & protected members around the class syntax. It likely isn't optimal and doesn't have any of the syntax sugar that would be possible with direct engine modification. However, it does work as intended without any need for a cross-compiler.

@bakkot Barring the obvious lack of syntax sugar support, the current inability to declare members static private, and the total loss of the original scope chain within the class, how similar is this to the changes you had to make to V8 to support the current proposal?

If anyone would like to test it, the source is here.

var Protected = Symbol("protected");
var Instance = Symbol("instance");

/**
 * Generates an ES6 class with private and protected scopes.
 * @param {Object} pvtScope - Contains all private/protected members and their 
 * 	initial values.
 * @param {Array} protList - Names of the keys in pvtScope to be shared with
 *  descendant classes.
 * @param {*} pubScope - An ES6 class or constructor function.
 * @returns {function} the constructor for the new Class
 */
function Class(pvtScope, protList, pubScope) {
	var keys = Object.keys(pvtScope);
	var privateNames = {};
	var protectedNames = {};
	var Constructor = Symbol("constructor");

	/* Create a Symbol for every private name declared */
	for (let i=0; i<keys.length; ++i) {
		let key = keys[i];
		privateNames[key] = Symbol(key);
	}

	/* Map the protected keys into another object. */
	for (let i=0; i<protList.length; ++i) {
		let key = protList[i];
		if (key in privateNames) {
			Object.defineProperty(protectedNames, key, {
				enumerable: true,
				get: function getName() { return privateNames[key]; }
			});
		}
	}
	
	/**
	 * Inject a callout into the constructor so we can setup monitoring and
	 * the private scope. Make sure to return the proxy instead of "this",
	 */
	var isExtension = /^class\s+\w+\s+extends\s+\w+/.test(pubScope.toString());
	var defString = pubScope.toString().replace(/(\s*)constructor(\s*\(((?:\s*,?\s*\w+)*)\)\s*{(\s*))(super\(.*?\);?\s*)?/,
		"$1constructor$2$5var retval = initPrivateScope(this);$4retval[Constructor]($3);$4return retval;$1}$1\[Constructor\]$2");
	if (defString == pubScope.toString()) { //Didn't have a constructor to modify
		defString = pubScope.toString().replace(/^(function\s+\w+\(((?:\s*,?\s*\w+)*)\)\s*{(\s*))/, "$1initPrivateScope(this);$3");
	}
	if (defString == pubScope.toString()) { //Wasn't a function declaration
		var replacement = `$1constructor() {$2\t${(isExtension)?"super();$2\t":""}return initPrivateScope(this);$2}$2$3`;
		defString = pubScope.toString().replace(/^(class\s.+?{(\s*))(\w+)/, replacement);
	}
	if (defString == pubScope.toString()) {
		throw TypeError('This class definition makes no sense! Give me a "class" or a "function" definition.');
	}

	with(privateNames) {
		//Ensures the private scope is fully initialized before construction
		function initPrivateScope(instance) {
			instance = createInstanceProxy(instance, privateNames);

			if (Protected in instance) {
				var protNames = instance[Protected];
				if (Object.getPrototypeOf(privateNames) !== protNames) {
					Object.setPrototypeOf(privateNames, protNames);
					Object.setPrototypeOf(protectedNames, protNames);
				}
				delete instance[Protected];
			}
	
			for (let i=0; i<keys.length; ++i) {
				let key = keys[i];
				if (key in pvtScope)
					instance[privateNames[key]] = pvtScope[key];
			}

			instance[Protected] = protectedNames;
			return instance;
		}
		
		eval(`_class = ${defString.toString()};`);
		return createClassProxy(_class);
	}
};

function createClassProxy(_class) {
	var handler = {
		apply(target) {
			throw TypeError(`Class constructor ${target.name} cannot be invoked without 'new'`);
		},
		construct(target, args, newTarget) {
			var retval = Reflect.construct(target, args, newTarget);

			if (target.prototype === newTarget.prototype)
				delete retval[Protected];	//Clean up our mess before returning.

			return retval;
		}
	}
	
	return new Proxy(_class, handler);
}

function createInstanceProxy(instance, privateNames) {
	var handler = {
		slots: {
			type: Object.getPrototypeOf(instance).constructor.name,
			privateScope: {},
			privateNames
		},
		get(target, key, receiver) {
			var retval;
			if (Object.values(this.slots.privateNames).indexOf(key) >= 0) {
				retval = this.slots.privateScope[key];
			}
			else {
				retval = Reflect.get(target, key, receiver);
			}
			return retval;
		},
		isExtensible(target, key) {
			return true;
		},
		preventExtensions(target) {
			Reflect.preventExtensions(instance);
			return false;
		},
		set(target, key, value, receiver) {
			var retval;
			if (Object.values(this.slots.privateNames).indexOf(key) >= 0) {
				this.slots.privateScope[key] = value;
			}
			else {
				retval = Reflect.set(target, key, value, receiver);
			}
			return retval;
		}
	}
	
	return new Proxy(instance, handler);
}

if (typeof(module) == "object") {
	module.exports = Class;
}
else {
	try {
		eval("export default Class");
	}
	catch(e) {}
}

Should private methods and accessors type-check their receiver?

This is the main open question for observable private method semantics: Should private methods act like a lexically scoped function declaration, or like (immutable) fields on the instance/constructor? Or, to give an example, what should the following code do:

class C {
  #sayHi() { alert('hi') }
  constructor() { #sayHi.call(undefined) }
}
new C();

If we treat #sayHi basically like a lexically scoped function, that happens to be in method position, then the alert('hi') should run just fine without any issue. On the other hand, if we treat it as an immutable field, then it would throw a TypeError because the #sayHi field doesn't exist on undefined.

The current spec text does not do the type checking, treating it as a lexically scoped function. If we want to add type checking, I think we'd want to take the following into account:

  • All private methods and accessors are added before any fields are. This way, field initializers can call private methods without having to worry about ordering. Since field initializers can call public methods which come textually later this way (since they're already installed on the prototype), private methods should also be available at that time.
  • If we take the "instance"/type-checking approach, making the methods immutable makes sense because they are shared between all instances--just like public methods. There's an additional implementation benefit, that it should be easier to inline methods, represent things in memory (in the hidden class, rather than taking a whole word in the instance), etc.

New, more JS-like syntax

The proposed syntax for private fields is absolutely disgusting, to simply put, it is confusing, is without prior art and very much alien to the elegant syntax JS has.

I must ask, why did you decide to use such an ugly syntax? Last I checked private had no use at all.
Combine that with the fact that we already in the process of adapting metaproperties, here is my proposal to you as to what an elegant syntax would look like:

class Counter extends HTMLElement {
  private xValue = 0;

  private get x() {
    return private.xValue; // !
  }
  private set x(value) {
    private.xValue = value; // !
    window.requestAnimationFrame(
      private.render.bind(this) // !
    );
  }

  private clicked() {
    private.x++; // !
  }

  constructor() {
    super();
    this.onclick =
      private.clicked.bind(this); // !
  }

  connectedCallback() {
    private.render(); // !
  }

  private render() {
    this.textContent =
      private.x.toString(); // !
  }
}
window.customElements.define('num-counter', Counter);

Reference:
function.sent meta property
new.target meta property

As much as I despise the idea of private fields, as I do believe this goes very much against the spirit of the open web and my love towards userscripts, at least make this abomination something pleasing to look at.

What about protected methods, better declaration of private fields, private async functions, and destructuring?

How could we allow a private field to be shared only when subclassing but not when it's an instance? Why not protected fields were not proposed? Have you thought if in the future somebody does propose them? (we are running out of characters)

I know it was discussed many times why $ was chosen (I read the FAQs). It's ugly but I get it. However, that doesn't mean there cannot be another way of declaring them. What about this?:

class A {
  static prop0 = 123
  private prop1 = 123
  prop2 = 123 // public property
  method() { // public method
    console.assert(new.target.prop0 === 123) // static
    console.assert(this.#prop1 === 123) // private <- accessing changes
    console.assert(this.prop2 === 123) // public
  }
}
console.assert(A.prop0 === 123) // static (devs know it's not A.static.prop0)

Declaring them that way makes more sense besides it doesn't break the current proposal's idea.
However, if somebody comes up with "this may be confusing..." or similar, devs do not seem confused using static [prop], they know how to access a static var (new.target[propName]). In a similar way, private [prop] won't make it more confusing, devs will know to access with this.#[prop].

An example where it will look better:

class A {
  #$ = jQuery
  #_ = lodash
  method() { this.#$(...) }
}
class A {
  private $ = jQuery // You still have to access with this.#$
  private _ = lodash // You still have to access with this.#_
  method() { this.#$(...) }
}

What about async functions?

class A {
  #async method() {}
  // or
  async #method() {}
}

How will it behave with destructuring. For instance (with no Private fields):

class A {
  prop = 10
  constructor() {
    const { method } = this
    method()
  }
  method() { console.log('Not bound to "this" but works') }
}

With private fields:

class A {
  prop = 10
  constructor() {
    const { #method } = this // or { method } = this# ???
    #method() // ??? or not allowed, throws? (but what)
  }
  #method() { console.log('Not bound to "this" but works') }
}

Are private fields not enumerable?, I mean, is it possible to get all the names of private fields from inside a class? If they are not enumerable, how does it affect applying a function decorator and changing the descriptor to enumerable: true? got it :P

How does it act with Proxy?

class MyProxy extends Proxy {
  constructor() {
    return new Proxy(this, {
      get(target, propName) {
        if (typeof target[propName] === 'function')
          return target[propName]
        console.log(`Accessing [[${propName}]]`)
      }
    })
  }
}
class A extends MyProxy {
  one = 5
  prop = 10
  #prop = 20
  method() {
    this.prop
    this.#prop
    this.#method()
  }
  #method() {
    this.one
  }
}
new A().method()
// outputs: Accessing [[prop]]
// outputs: ??? Accessing [[#prop]]
// outputs: ??? Accessing [[one]] <- as a result of calling this.#method, does the trap still work?

So far I have more questions that actually use cases since I'm not pretty sure how much does it affect everything we know about JS. Now we have block scope vars and function scope vars. Maybe explaining how to categorize private block/function scope vars will clarify better all questions.

Utterly flummoxing development process...

As promised several months ago, I'm still not going to make any more suggestions regarding this bolloxed proposal, but I have questions that need answering.

I just finished reading this thread where yet another good alternative to using # was given and shot down because the TC39 board seems to think that such proposals (and there have been many) make it "highly more likely" that a developer will make a hard to detect error.

Here's my question: since when has that stopped well thought-out and syntacticly appealing features from being added into JS?

There's a long history of features since day 1 that act confusingly and still to this day cause developers problems:

  • this: Potentially different in different scopes of the same object.
    a. function (foo()) vs new function (new foo())
    b. function vs nested function
    c. member function (foo.fn()) vs reference to member function (a = foo.fn; a())
    d. nested function vs nested arrow function (new in ES6)
  • Symbol: Asymmetric primitive constructor. (new in ES5)
    a. All other primitive constructors allow the use of new to create an object version of the primitive
    b. Symbol just throws a TypeError if used with new even though typeof(Symbol) == "function"
  • Proxy: Confusingly weak. (new in ES5)
    a. Compares result of proxy handler operations to the results of internal functions called against the target instead of checking for consistency against related proxy handler operations. This makes the original target a confusing and unnecessary constraint on what the proxy can do with calls against the target.

I could go on, but the point should be clearly visible. The TC39 board is no stranger to creating features that cause hard to detect errors. However, these features were still added without using hard to read, non-ergonomic, non-convention breaking syntax. I've been programming for 35 years. It's only been the past 15 of those that I've seen this bewildering trend of programming languages trying to protect the developer from making small, but admittedly potentially disastrous mistakes.

I get it. However, in the past, those were learning opportunities that turned good programmers in to great ones, programmers that knew how to squeeze every last bit of power out of the platform and language combination they had to work on. Those who didn't have the courage to try new and novel things stuck to using the paradigms and techniques they were already comfortable with using. Was there anything wrong with that? IMO, no!

Let me roll it all up this way: the likelihood that one of the many, many sigil-less alternatives will lead to hard to find consistency errors is far, far less likely than someone forgetting a single # somewhere in a 1000+ line JS module producing some obscure error in someone else's code 3+ nested require/imports later. How can I justify this?

  1. Who hasn't experienced the pain of trying to find where the missing [.,,,;] goes?
  2. Almost every other major C-based language out there has already handled this feature without a sigil. The developers understood the syntax well and has been using them for years.

To assert without evidence (even anecdotal) that developers are highly likely to stumble into such a trap without knowing how to use a debugger and find the issue just feels disingenuous.

Stop this proposal

I think it will impair existing elegance of Javascript so it should be omitted.

How does the private fields interact with the Proxy?

Hi all,

In Java, we can test private methods via Reflection. In PHP, we cannot test private methods. 30 PHP engineers in my company feel like in practice they need to test some private methods.

The usual consequences of a limitation like in PHP are:

  • to end up with less tests or
  • more methods than necessary being exposed.

The infamous "private" __SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED from React was used in Unit Testing as well if I'm not mistaken.

Is there any possibility to avoid this situation in JS? Like being able to access private methods with proxies/any other mean?

Using "_" instead of "#"

Shouldn't a leading underscore be a better option, given that this is a not unusual convention? There are many sites and texts that recommend using a "_" prefix to signify that a variable or attribute is meant to be private, and not be accessed.

Syntax change suggestion

I made the comments below in a different thread, but I think it warrants another look into the syntax all its own.
@littledan I get where you've been trying to go with the class-private names approach, though as someone who has written new languages, still think you've not thought thing through thoroughly (pardon the alliteration). An equals method can be developed even using private fields as described by @zocky while still taking into account most of what you're trying to achieve with private fields, and all without destroying the fact that class is just syntactic sugar. What if this example:

class myClass {
  private foo = "bar";
  doSomethingWithFoo() {
     // foo is accessible here, but not accessible to methods of this or any other object
     // defined outside the class definition 
     doSomething(this[foo]);
  }
  equals(other) {
    let retval = false;
    if (other instance of myClass)
      retval = this[foo] === other[foo];
    return retval;
  }
} 

translated to this:

var myClass = (function defineMyClass() {
  var privateScope = new WeakMap(); //Your private slots go here
  var privateNames = { //Your class-private names
    foo: Symbol() //From your intentions
  }
  with(privateNames) { //Ensures your class private names are available without this
    function _myClass() {
      privateScope.put(this, {
        [foo]: "bar"
      });
    }
    myClass.prototype.doSomethingWithFoo() {
      let pvt = privateScope(this);
      // foo is accessible here, but not accessible to methods of this or any other object
      // defined outside the class definition 
      doSomething(pvt[foo]);
    }
    equals(other) {
      let retval = false;
      if (other instanceof myClass) {
        let pvt = privateScope(this);
        let other_pvt = privateScope(other);
        retval = pvt[foo] === other_pvt[foo];
      }
      return retval;
    }
  }
  return _myClass; 
})();

I've spent much time trying to think of how the sigil (#) approach would work if implemented using current JavaScript. Frankly, it would probably look like the above. What's more, is that unless I missed something critical, what you're trying to do with class-private names can be in a fairly straight-forward manner be little more than a hidden implementation detail while allowing the syntax to remain something more palatable to the existing Javascript developer base.

Private Constructors

It would great if there was also a way to create private constructors in JavaScript. I find myself wishing for this when I create static methods that act as constructors.

class DatabaseClient {

  static async connectedClient() {
    let client = new DatabasClient();
    await client.#connect();
    return client;
  }

  async #connect() {
    // Conect to the database...
  }
}

In this example, I don't want the constructor to be called directly because it doesn't make sense to interact with a DatabaseClient that's not connected to the database. I can't move the connect logic into the constructor because it's asynchronous. It would be great to be able to do something like this instead:

class DatabaseClient {

  static async connectedClient() {
    let client = new DatabasClient();
    await client.#connect();
    return client;
  }

  #constructor() {}

  async #connect() {
    // Conect to the database...
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.