tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

A summary of feedback regarding the # sigil prefix #100

Closed glen-84 closed 6 years ago

glen-84 commented 6 years ago
Issue/comment 👍 👎 🎉 ❤️
Stop this proposal 196 31 20 45
Why not use the "private" keyword, like Java or C#? 132 7 12 5
Why not use the "private" keyword, like Java or C#? 💬 144 0 16 9
Why not use the "private" keyword, like Java or C#? 💬 51 0 0 0
Private Properties Syntax 33 4 0 0
Yet another approach to a more JS-like syntax 32 0 0 41
New, more JS-like syntax 32 4 0 0
Please do not use "#" 32 0 1 1
Proposal: keyword to replace `#` sigil 18 0 0 1
Why not use private keyword instead of #? 16 0 0 1
Explore a more JS-like syntax 7 0 0 0
The '#' looks awful, please change it 9 4 0 0
can we just use private keyword? 2 0 0 0
Do we still have a chance to stop private fields (with #)? 3 3 1 1
[Private] yet another alternative to `#`-based syntax 3 0 0 0
and yet another suggestion for "private but not closure" 0 0 0 0
Why don't use `private` instead of `#`? 0 3 0 0
Why "private" and "protected" future reserved keywords are not used? 0 0 0 0

Other feedback:

The TypeScript team are not fond of the syntax either.

My suggestion to rename the proposal: https://github.com/tc39/proposal-private-fields/issues/72 (the idea is mainly about making it a lower-level feature, and making room for a cleaner syntax at a later date)

@littledan @bakkot @ljharb – I apologize in advance, as I know that you must be thinking "Oh g*d, not again!", but I just wanted to put this all in one place, and allow the community to share their opinion in the form of reactions. It would be great if you could keep this open for a while (30 days?), or perhaps even indefinitely to prevent other users from submitting even more issues on the same topic.

To the non-committee members reading this: These proposals are now at stage 3, so it's unlikely that a syntax change will occur, but please feel free to up-vote this issue if you feel that the sigil (#) prefix is not a good fit for the language, or down-vote the issue if you believe that it's perfectly acceptable.

bakkot commented 6 years ago

Um. Thanks for the effort, but... I don't really get what you intend this issue to accomplish. We've seen this feedback already.

glen-84 commented 6 years ago

I don't really get what you intend this issue to accomplish

  1. Emphasize and centralize the large amount of negative feedback towards the sigil prefix
  2. Prevent users from submitting the same topic over and over again

We've seen this feedback already

Repeatedly. And how many unique threads does the commitee need to see before they consider it a real issue, as that clearly hasn't happened yet.

bakkot commented 6 years ago

Prevent users from submitting the same topic over and over again

I was kinda hoping the FAQ would accomplish that, but no such luck so far. Still, might be worth a shot.

And how many unique threads does the commitee need to see before they consider it a real issue

We do and always have considered it a real issue. But as always it's one to be balanced against other issues, which we've done, and we concluded that this was the right path. There's no number of threads which will cause the constraints to change.

stevenvachon commented 6 years ago

The day that ECMAScript/JavaScript died.

ljharb commented 6 years ago

@glen-84 That isn't unique threads nor unique counts; how many of those 99 or 111 etc are the same people? If you actually uniquify it to "how many unique accounts have indicated dislike of #", the numbers will be less impressive.

Either way, "popularity" is not nearly as relevant as the issues listed in the FAQ. There is no tenable syntax alternative, and no sigil that's any "better" in terms of the complaints you're describing.

thysultan commented 6 years ago

This documents a few others not listed here.

@ljharb There are tenable alternatives and community backlash is not the only motivating factor as there are concrete issues with the dot#sigil syntax that have mostly been ignored – #28 and #56.

ljharb commented 6 years ago

@thysultan I wouldn't say ignored; more, considered and disagreed with.

glen-84 commented 6 years ago

That isn't unique threads nor unique counts; how many of those 99 or 111 etc are the same people? If you actually uniquify it to "how many unique accounts have indicated dislike of #", the numbers will be less impressive.

I never said that they were unique counts. Even if the numbers aren't very high, the relative numbers are significant IMO.

Either way, "popularity" is not nearly as relevant as the issues listed in the FAQ. There is no tenable syntax alternative, and no sigil that's any "better" in terms of the complaints you're describing.

There is no tenable syntax alternative currently. That may change in 5-10 years. All I'm suggesting is to make room for a cleaner syntax in the future, by making this a lower-level (read: even uglier) feature and letting users transpile to that from better-looking code (code that actually looks like JavaScript, and not ASCII art).

tasogare3710 commented 6 years ago

@stevenvachon

                   __________\
                  /          \\\
                 /    REST    \\\
                /      IN      \\\
               /     PEACE      \\\
              /                  \\\
              |       es         |\
              |                  |\
              |   killed by a    |\
              |  private-fields  |\
              |       2018       |\
             *|     *  *  *      | *\
     ________)/\\\\_//(\\/(/\\)/\\//\\/|_)_______
tasogare3710 commented 6 years ago

People are not merely dislike sigil, they point out a many real issues. someone does not understand it well. However, this proposal is nothing that can be done as it has reached stage 3.

Everything happens for a reason.

bakkot commented 6 years ago

There is no tenable syntax alternative currently. That may change in 5-10 years.

There are fairly strong reasons to believe that will not be the case. Few of the considerations in the FAQ are likely to change.

littledan commented 6 years ago

Even before this compilation, it was clear to me that discomfort with # is widespread, and this discomfort has been visible to the committee for a couple years. However, I agree with @bakkot that it's unlikely that we will find a better syntax in the future.

Whenever I have discussed this issue with someone in person, explaining the argument in the FAQ, they have ultimately been accepting of the # syntax. It seems like many of the people who agree with the motivation for adding private fields and methods are accepting of the ergonomic cost of this syntax. It also seems like most of the concern comes from it not being exactly this.x--I don't have the feeling that any other syntax will be as popular.

glen-84 commented 6 years ago

There are fairly strong reasons to believe that will not be the case. Few of the considerations in the FAQ are likely to change.

That's unfortunate. Long live TypeScript (and WebAssembly + C#?).

ghost commented 6 years ago

It is pretty clear that the JS community looks at this proposal with disdain, so I must ask: who is this proposal for? The community clearly dislikes the syntax. Enterprise? TypeScript team clearly dislikes the syntax and they are really big in the enterprise.

Why do you even allow the community to contact the team, when you just disrespectfully dismiss everything, even valid points.

If you don't care about the community's voice a single bit then just straight up state the facts instead of sugar coating things and dancing around the issue. There are a ton of more appealing suggestions regarding the syntax and instead of talking about it with the community you just shut down the discussions.

This is ridiculous.

jeffmo commented 6 years ago

@ayylmar: I’m not participating in the committee much anymore, but having worked on public fields im still following all of the fields discussions.

I think there might be some context that you may have missed. I think it’s clear that several people in the community do dislike the syntax, but it’s not clear what a viable alternative is. If you put yourself in the position of those championing private fields they’re basically left with “keep it and annoy some people with syntax, or toss it altogether and annoy some people with a lack of private fields”.

It’s certainly unfortunate that many people dislike the syntax, but without a viable alternative it doesn’t seem like we’re left with anything but those 2 choices. Alternatives that many have proposed in the past tend not to make it very far down the path of exploration before an objectively blocking concern arises with them.

I get it though, on its surface this seems like a simple solution: Just fix the syntax. It also kinda sucks that there’s been so many issues about this that it’s a little tricky to find the exploration of all of the alternatives that ultimately just couldn’t work.

doug-numetric commented 6 years ago

I'm late to the discussion, but why can't we do something like this?

class Point {
  // instance private, just an intuitive closure
  let instance;
  constructor(_x, _y) {
    // class private, a private version of this to assign you class private stuff to
    private this = { x: _x, y: _y };
    instance = uuid();
  }

  equals(p) {
    { x: my_x, y: my_y } = private this;
    // this works only if p is an instance of Point
    { x: p_x, y: p_y } = private p;
    return p_x === x && p_y === y;
  }
}

javascript is inherently prototypal, not classical. The classical stuff should be 2nd class, not the other way around. Besides, this looks clean and intuitive.

ljharb commented 6 years ago

@doug-numetric private isn't "classical", it's bringing closure-based privacy to instances. For your example - destructuring syntax allows computed property names, including symbols - and private this and private p would have to not be objects you can pass around or reflect over, but still looks like objects because you're destructuring out of them. That's a lot of complexity to add to the grammar, and more importantly, to the user's mental model.

doug-numetric commented 6 years ago

@ljharb Thanks for the response. I see the need for private this to not be an object. I also see the concern with this.#foo !== this['#foo'] which I think is unintuitive. I for one would opt for verbosity over using the # sigil in any case. With not adding complexity to the mental model as a goal, why pretend to add class privates to the this object when it's not really there. Javascript has private reserved already. If it's not used for this feature, when? Other than verbosity, what's wrong with:

class Point {
  // class private
  private z = 0;

  // instance private, just an intuitive closure
  let instance;
  constructor(_x, _y, _formatter) {
    // class private
    private this x = _x;
    private this y = _y;
    private this formatter = _formatter;
    instance = uuid();
  }

  equals(p) {
    // this works only if p is an instance of Point
    return private p x === private this x && private p y === private this y;
  }
  print() {
    return (private this formatter)(private this x, private this y, private this z);
  }
}

const p1 = new Point(2, 4, (x, y, z) => `<${x}, ${y}, ${z}>`);
p1.print(); // <2, 4, 0>

const p2 = new Point(2, 4, function(x, y) { this.x = 0; return `<${x}, ${y}>; });
// I suspect this should throw a TypeError "Cannot set property 'x' of undefined"
p2.print();

The destructuing shorthand is just really handy sugar, which I like as this can get verbose.

bakkot commented 6 years ago

Other than verbosity, what's wrong with

See the FAQ.

// instance private, just an intuitive closure

Separately, no, that's not. In JavaScript as it stands, a given function doesn't see different values for a closed-over variable depending on how the function is invoked. (Keep in mind that class methods are shared; there's not a separate copy created for each instance.) As such the only consistent semantics for this syntax would be for it to have the same value for all instances of the class, just as if the declaration had occurred outside the class. Which I think is not what you want.

jonmersan commented 6 years ago

I've finally been motivated to sign up to github. I've been reading issues and such around several TC39 proposals because I'm in the process of helping my company figure out how we want to approach es going forward (a former employee enabled stage 0 plugins in Babel and no real discussion of risks ever happened).

See the FAQ.

Even with that, I disagree with the claim that # is significantly better than declaring a private field as private and then accessing it like any other field (just as in Java and other languages). From what I understand, it looks like you are avoiding implementation of privates using WeakMaps? Used properly, I have yet to see a case that breaks. As for the example in the FAQ, it has a flaw that's easily corrected:

let Person = (function(){
  const privates = new WeakMap;
  let ids = 0;

  function getPrivate(obj, name) {
    if(privates.has(obj)) {
      return privates.get(obj)[name];
    }

    //fall back to the public value, if it has one
    return obj[name];
  }

  return class Person {
    constructor(name, makeGreeting) {
      this.name = name;
      privates.set(this, {id: ids++, makeGreeting});
    }
    equals(otherPerson) {
      return privates.get(this).id === privates.get(otherPerson).id;
    }
    greet(otherPerson) {
      return getPrivate(this, 'makeGreeting')(otherPerson.name);
    }
  }
})();
let alice = new Person('Alice', name => `Hello, ${name}!`);
let bob = new Person('Bob', name => `Hi, ${name}.`);
alice.equals(bob); // false
alice.greet(bob); // === 'Hello, Bob!'
let mallory = new Person('Mallory', function(name) {this.id = 0; return `o/ ${name}`;});
mallory.greet(bob); // === 'o/ Bob'
mallory.equals(alice); // false. As expected!

I didn't bother refactoring the entire sample code to use getPrivate vs. directly accessing members of the privates object in every instance, but it's enough to demonstrate that the reasoning in the FAQ isn't entirely convincing. The important part is that you should never leak the references of any objects stored in the privates WeakMap.

Of course, I know you can't do getPrivate(this, 'foo') = 'bar', but you could just make a generic function to handle this case:

fieldAccess(scope, fieldName, setValue) {
  if(privates.has(scope)) {
    if(arguments.length === 3) privates.get(scope)[fieldName] = setValue;
    privates.get(scope)[fieldName];
  } else {
    if(arguments.length === 3) scope[fieldName] = setValue;
    return scope[fieldName];
  }
}

Basically, assignments to a private field go from this.id = 0 to fieldAccess(this, 'id', 0) while all other instances of this.id become fieldAccess(this, 'id'). If you don't want to pay the cost of calling fieldAccess for every field of every object within code declared within the class, you could make the substitution only wherever the member of any object you're accessing matches the names of any private members. For things like this['id'], you could run it through fieldAccess but I think it'd be better to leave it unchanged as it would simply fall back to a public id which would give developers a way to access a public member from within a class declaring a private member by the same name. For for...of (as I've seen it mentioned in some thread or another here) I don't think I've seen much of anyone giving a reason that it should need to iterate over private members.

As far as I can tell, the primary motivating factor behind #uglymess is performance considerations. I think it'd be a small price to pay to bring this in to the language in a way the people will be happy to write code with. After all, if coding was about performance above all else, why did anyone ever adopt C++ or C# over C or even Java to begin with?

Personally, I'd probably look into something like TypeScript before actually using # in any real code to declare/access private fields.

Primarily I ended up here because I had to just now refactor some code involving the use of static and went to look up where that proposal was at only to find it had merged with this. It almost seems like if this proposal goes to stage 4, it's because static carried it there.

ljharb commented 6 years ago

Instances (public and private) have been stage 3 for awhile; statics only reached stage 3 this week.

Separately, typescript doesn’t have truly private fields - nothing ensures their privacy at runtime.

jonmersan commented 6 years ago

From what I've read, it looks like private/public, static, and decorators are kind of merging together due to the fact that decorators apparently need knowledge of private members.

Considering the demand for decorators, I wouldn't be surprised if the motivations for implementations that are spec compliant with private only happen because the contributors really wanted decorators but had to deal with this version of privates because they had to in order to be fully compliant.

I don't know much about typescript, but I wonder if the lack of true privacy has to do with the fact that WeakMap itself is fairly new and not everyone can work only with modern browsers.

pumano commented 6 years ago

I'm really glad to see how @glen-84 grab issues together (I'm author who create one of that).

In my humble opinion, it's developer experience.

Community want to use private. That keyword is reserved. Why not to use it?

Committee should understand, that, you do it in your "own" way. It's truly egoistic way. Think about other people. If many people don't want it, why you not listen?

All enterprise languages like Java, C#, TypeScript, also PHP use private.

It's a big problem to implement proper public / private access to field without using #? Come on!

You are talented engineers! You add many amazing features to JavaScript, and you can do it properly too (under the hood) without losing semantic. Some of people in issues provide possible examples for using private.

fuchsia commented 6 years ago

General feedback:

I gave slots a serious test on Monday (Chrome 67). I found they caught dumb typos and it was great to have a list of (private) fields rather than having to squint at the constructor code; I even warmed to the # — it's a big warning these are different to and and can shadow conventional properties.

But, I've got into the habit of destructing the properties I need at the start of a method:

class {
   pos( dec )
   {
      const { latitude, HA } = this;

      const diff = cos( dec - latitude ),
            sum  =  cos( dec + latitude ),
            cosHA = cos( HA );

      /* etc... */
   }
};

It's ergonomic (one this plus a little boiler plate for every access), visually compact, and shows all the dependencies at a glance.

Unless, I've missed something, slots break that. Doing it manually is cumbersome and uses a heap of screen estate so I was slipping back into this.#x for single accesses. And as a class developed, variables were shifting between slots, properties, arguments and local variables so there was a lot of inserting and deleting of this.#. I am seriously thinking about a single this.#state slot and storing all my variables in it — after all, that's what you'd do with a WeakMap.

So my feedback is that slots have rolled back usability and manageability of the code. But that can be fixed with follow-on proposals: allowing access to slots as just #whatever without the this. prefix might be fine, or maybe it should be possible to destructure slots into local names (const {#latitude} = this; creates a variable called latitude — I can rename conflicts) .

Other than that, I found them far less horrible than they looked, and have been itching to use them in production code.

bakkot commented 6 years ago

@fuchsia, thanks for the feedback. I think we likely will want to allow writing const { a: b, #y: z } = this; to mean const b = this.a; const z = this.#y;, at the very least - would that help? (Other things, including shorthand access, are on the table; this just seems the most likely, and unlikely to get in the way of other potential followups.)

@pumano, please see the FAQ. It turns out that, yes, this is the best solution for JavaScript, in the opinion of the committee. Our constraints are unusual, especially the lack of a type system. The other solutions people have suggested have all seemed unacceptably bad in one way or another.

jridgewell commented 6 years ago

The renamed destructure seems like a great follow on. To avoid variable name confusion, it’ll probably be necessary to force the rename { #x: x } until we’re used to the pattern.

rdking commented 6 years ago

@ljharb At any point over the past few years of mulling this proposal over did the TC39 board ever consider that the reason they're receiving such viscerally negative reactions from commenters might be due to the nature of the requirements that went into this proposal? I'm curious as to what would happen if all of the non-logical ("I feel that...") made by the board and contributors were given as little consideration as those made by those of us who have levied equally valid opinions. While it cannot be said that there is anything logically wrong with this proposal, solutions which do less damage to the language's conventions have been offered and dismissed due only to such opinions.

@bakkot I've been trying to figure this one out. class as it exists in ES today is just syntactic sugar. There is literally nothing that it does that cannot be done without the class keyword. Even inheriting from a native type like Array or HTMLElement can be done properly in ES5 with exactly the same results as what you'd achieve from class. However, with private fields, there will be no exactly equivalent means of creating such privacy. The closest you might come would be to implement the private fields using a Proxy object, but even that comes with 2 issues

  1. no equivalent access syntax
  2. no equivalent result object (since the outer object will always be a Proxy)

To both of you: Is having this implementation of private fields really worth promoting class from syntactic sugar status to its own unique entity when other solutions have been offered that do not break this well-known and heavily depended-upon symmetry simply because the opinions of a few members of the TC39 board outweigh the many dissenting opinions and logical issues that have been offered? Consider that even the original author of this proposal once tried to kill it.

bakkot commented 6 years ago

However, with private fields, there will be no exactly equivalent means of creating such privacy.

This is not true. There is a fairly straightforward and precisely equivalent desugaring using WeakMaps, though it requires some non-obvious care to get calls right. See the babel transform for an implementation.

ljharb commented 6 years ago

Class can definitely do things that can’t be done otherwise; extending builtins, in particular.

zenparsing commented 6 years ago

@rdking I hear you 😉

rdking commented 6 years ago

@bakkot WeakMaps are not precisely equivalent to internal slots and the "private names" that underlie this proposal. Where internal slots are directly a part of the metadata of the instance to which they're attached, WeakMaps have to exist inside a closure. While they can both be used to produce the same effect, they are distinctly different with different semantics. And don't forget about the syntax issue. While I can replace a class with a function without breaking any code using that class, to do the same under this proposal would mean adjusting every method of the class that accessed private fields.

There's also an oddity that no one has mentioned yet (for as far as I've read). Consider that I can actually use your proposal to put private values on an unrelated object. Since the private names themselves are not fields, but rather Symbols owned by the class, there's little stopping anyone from creating a method on a class that returns an object on which a value has been assigned to a private field known to the class.

class Test {
   #field
   getWeirdObject() {
      var retval = {};
      retval.#field = "Surprise!";
   }
}

Only methods of Test will ever be able to find that field. This possibility is not excluded by anything in this proposal, and certainly cannot be reproduced by traditional means since trying the same thing with a normal Symbol instance can be discovered with both Object.getOwnPropertySymbols and Object.getOwnPropertyDescriptors.

This proposal breaks symmetry in some very interesting ways.

rdking commented 6 years ago

@ljharb I've got a class library that proves you wrong. If you have the inclination to do so, go look at it [here]. The trick isn't hard. All you really need to do is, while constructing an instance of a class that inherits from a native:

  1. Construct the instance without the native.
  2. Construct the native part.
  3. Replace the "Object.prototype" of the instance with the prototype of the native part.
  4. Set the prototype of the native part to the instance.
  5. Return the native.

Works like a charm.

ljharb commented 6 years ago

@rdking your "getWeirdObject" method absolutely won't work; you can only use the private "#field" field on objects that were created with new Test() (or a subclass).

The link to your lib is missing; I'm happy to take a look at it.

rdking commented 6 years ago

@ljharb Sorry about that. Try this one..

As for getWeirdObject, I did not see anything in the discussion threads, FAQ, or anywhere else that excluded this possibility. Consider that the declaration of a private name in a class doesn't mean that the corresponding field has been created. Also consider that private names can be used to access private fields on non-this instances. When you throw in the fact that JS is a duck-typed, you end up with the ability to apply private fields to objects that were not created with the constructor of the class owning the private name.

How would you go about making that not true? To do so would mean that every class instance would need to have private slots by default, and that extra logic would have to be written to ensure that the private slots of an instance can only be accessed via the private names specified in the corresponding class definition. That's easily doable, but it's also a serious performance hit for writing to private fields.

ljharb commented 6 years ago

@rdking the declaration of a private name in a class means that immediately after super() (or immediately, in a base class), the field is created on the instance - and attempting to access or set a private name on something that lacks the field will brand check, and throw. This is a core criteria for this proposal.

I tried your lib in node 0.12 - an ES5 engine - and your package has no "main", so a require failed, and require('java-class/Class') failed with a syntax error, since ES5 doesn't support computed property keys.

bakkot commented 6 years ago

@rdking

While they can both be used to produce the same effect, they are distinctly different with different semantics.

I'm not sure what "produce the same effect [...] different semantics" means, but they do have the same observable semantics. To give an example:

class Example extends Sup {
  #x = Math.random();
  m() {
    return this.#x;
  }
}

is equivalent to

let Example = (function(){
  const x = new WeakMap;
  return class Example extends Sup {
    constructor(...args) {
      super(...args);
      x.set(this, Math.random());
    }
    m() {
      if (!xMap.has(this)) {
        throw new TypeError;
      }
      return xMap.get(this);
    }
  };
})();

By the way, I suggest reading the spec for it, if you haven't. (Or reading the babel transform.) The above does not exercise all the subtleties, but they are covered in the spec.

While I can replace a class with a function without breaking any code using that class, to do the same under this proposal would mean adjusting every method of the class that accessed private fields.

You have to change code in the class, but not outside of it. That seems unsurprising; replacing a class with a function will also require changing the code in the class.

This possibility is not excluded by anything in this proposal

Yes, it is. However, it is possible to add private fields to arbitrary non-primitive objects via different, more obscure means. I'm not sure why that's a problem, though; you can already associate data with arbitrary non-primitive objects using a WeakMap.

fuchsia commented 6 years ago

@bakkot Yeah, destructuring private slots would be great! I'd prefer not to have to explicitly name them (i.e. const {#latitude:latitude}=this), as @jridgewell suggests, as it would get very tiresome for half a dozen reasonably named variables. But baby steps.

zenparsing commented 6 years ago

Notice how parallel lexical namespace solutions tend to creep further into the language. Here are some of the "solution opportunities" (i.e. problems) we've encountered so far:

rdking commented 6 years ago

@ljharb Ouch... Nice catch. I forgot to switch back to old node when testing that.... Back to testing that again.... In either case, here's some comparatively simple (working!) code for how to do it:

function isNativeFunction(obj) {
   return ((obj instanceof Function) &&
           /^function\s+\w+\(\)\s+{\s+\[native\s+code\]\s+}$/.test(obj.toString()));
}

function extend(dest) {
   var sources = Array.prototype.slice.call(arguments, 1);

   for (var i=0; i<sources.length; ++i) {
      var src = sources[i];
      while (src && (src !== Object.prototype)) { 
         var keys = Object.keys(src);
         for (var j=0; j<keys.length; j++) {
            var key = keys[j];
            var desc = Object.getOwnPropertyDescriptor(src, key);
            if (desc && desc.value && (typeof(desc.value) == "function")) {
               Object.defineProperty(dest, key, desc);
            }
         }
         src = Object.getPrototypeOf(src);
      }
   }

   return dest;
}

function makeSuper(parentType) {
   var retval = isNativeFunction(parentType) ? 
      function superN() {} :
      function superO() {
         var args = Array.prototype.slice.call(arguments, 0);
         return parentType.apply(this, args);
      };

   Object.setPrototypeOf(retval, extend({}, parentType.prototype, Function.prototype));
   return retval;
}

function inherit(childType, parentType) {
   Object.setPrototypeOf(childType.prototype, Object.create(parentType.prototype));
   childType.prototype.constructor = childType;
   var retval = function() {
      var args = Array.prototype.slice.call(arguments, 0);
      var isNative = isNativeFunction(parentType);
      var retval = isNative ? new parentType() : Object.create(childType.prototype);
      retval.super = makeSuper(parentType);
      isNative && Object.setPrototypeOf(retval, childType.prototype);
      retval = childType.apply(retval, args) || retval;

      return retval;
   };
   retval.prototype = childType.prototype;
   return retval;
}

Using it would look something like this (contents copied from Chrome debugger)

function foo() {
    if (this instanceof foo) {
        console.log("Dude! You're getting a foo!");
        this.super();
    }
    else {
        throw new Error("You poor, poor foo!");
    }
}
> undefined

Foo = inherit(foo, Array)
> ƒ () {
      var args = Array.prototype.slice.call(arguments, 0);
      var isNative = isNativeFunction(parentType);
      var retval = isNative ? new parentType() : Object.create(childType.prototype);…

test = new Foo();
> Dude! You`re getting a foo!
v [super: ƒ]super: ƒ ()arguments: nullcaller: nulllength: 0name: "superN"prototype: {constructor: ƒ}__proto__: Object[[FunctionLocation]]: [[Scopes]]: Scopes[2]length: 0__proto__: Arrayconstructor: ƒ foo()__proto__: Array__proto__: Array(0)concat: ƒ concat()constructor: ƒ Array()copyWithin: ƒ copyWithin()entries: ƒ entries()every: ƒ every()fill: ƒ fill()filter: ƒ filter()find: ƒ find()findIndex: ƒ findIndex()forEach: ƒ forEach()includes: ƒ includes()indexOf: ƒ indexOf()join: ƒ join()keys: ƒ keys()lastIndexOf: ƒ lastIndexOf()length: 0map: ƒ map()pop: ƒ pop()push: ƒ push()reduce: ƒ reduce()reduceRight: ƒ reduceRight()reverse: ƒ reverse()shift: ƒ shift()slice: ƒ slice()some: ƒ some()sort: ƒ sort()splice: ƒ splice()toLocaleString: ƒ toLocaleString()toString: ƒ toString()unshift: ƒ unshift()values: ƒ values()Symbol(Symbol.iterator): ƒ values()Symbol(Symbol.unscopables): {copyWithin: true, entries: true, fill: true, find: true, findIndex: true, …}__proto__: Object

test instanceof foo
> true
test instanceof Foo
> true
test instanceof Array
> true
Array.isArray(test)
> true
ljharb commented 6 years ago

@rdking I’m afraid Object.setPrototypeOf and __proto__ are both in ES6, and the latter is nonstandard before that - it is impossible to do that in pure ES5.

rdking commented 6 years ago

@bakkot Thank you for point me to the spec. Sadly even the spec is in agreement with my assessment.

First paragraph of section 4

The Private Name specification type is used to describe a globally unique identifier which represents a private field name. A private name may be installed on any ECMAScript object with the PrivateFieldAdd internal algorithm, and then read or written using PrivateFieldGet and PrivateFieldSet.

Given that the first sentence of paragraph 3 in section 4 states:

All ECMAScript objects have a new additional internal slot, [[PrivateFieldValues]]... there is literally nothing in 4.3 PrivateFieldFind and 4.6 PrivateFieldSet that precludes a private name known to a member function of some class from assigning a private value to an arbitrary object using that private name.

If this isn't intended to be true, then the spec needs to be adjusted... or did I miss the part where this has already been excluded?

rdking commented 6 years ago

@ljharb I'll grant you that, and yet, proto is a de facto standard as it is implemented in nearly every ES5 engine in popular use. So while you can say it can't be done in pure ES5, it can in fact be done in the version of JavaScript based on ES5. From where most developers sit, that's all that counts. If I were a member of the board defining the specification behind one of the current most popular development languages, I would think it remiss to neglect widely adopted de facto standards when considering how the language is being used and how potential future enhancements will affect those usage patterns.

bakkot commented 6 years ago

@rdking:

The syntax of retval.#field = "Surprise!";

The notes you highlight are true as written, but are not the relevant part of the spec.

But, again, it is possible to add private fields to arbitrary objects by more obscure means, if you really insist on doing so; this is just a technical point about your example. And, again, I don't understand what your objection to this is, given that you can use a WeakMap for the same effect.

rdking commented 6 years ago

@bakkot I understand now. I saw my mistake. Now I feel a little less uncomfortable with this proposal. At least with ES6 proxies, equivalent code can be generated that follows everything about this spec save for the .#field part.

Is there any particular reason that the sigil is part of the field name (#field) and not part of the access operator (.#)? That seems to me to be a bit of a contradiction. It seems to me that the engine will have to check the first character of the field name and use that to determine whether or not to use the string as a direct field lookup or a private name lookup, using the latter to look into the [[PrivateFieldValues]] for the final value. Why not do this at the operator level such that if the operator is .# only do the private lookup? There's nothing really wrong with having the sigil be part of the field name. It just seems even more counter-intuitive that the sigil is not an operator.

bakkot commented 6 years ago

Is there any particular reason that the sigil is part of the field name (#field) and not part of the access operator (.#)?

You mean, in the grammar? I don't think it makes much difference. Engines are inevitably going to end up (in fact, already do, in existing implementations) parsing private field access to a different representation than regular property access and subsequently generating different code.

The reason that the declaration and access both have the # is that the symmetry makes it easier to keep things straight.

Conceptually, I think it makes sense to explain and use private fields as if the # were part of the name, though, since many people are already familiar with the existing pattern of foo._bar for internal-by-convention properties.

rdking commented 6 years ago

I get the whole argument about thinking of the sigil(#) as a replacement for the underscore(_) convention many used. However, while it might not make much of a difference to engine developers, it could make a difference for the language itself. If .# was an operator for accessing private members of a class, then the broken symmetry around . and [] could be fixed allowing for the following possibilities, all without breaking this proposal's requirements:

var aSymbol = Symbol();
var bet = "bet";
class Test {
   #field = 3;
   #['alpha' + bet] = 'abcdefg...'
   #[aSymbol] = "It works!"

   test() {
      var alphabet = 'alpha' + bet;
      console.log(`There are ${this.#field} private fields in this class.`);
      console.log(`this.#${alphabet} = ${this.#[alphabet]}`);
      console.log(`this.#[aSymbol] = ${this.#[aSymbol]}`);
   }
}

Used in this way, the entire intent of the proposal is still preserved while still allowing every known form of access to an instance's [[PrivateFieldValues]].

rdking commented 6 years ago

This just occurred to me. You could take it one step further to make it more palatable and easier to learn for everyone. What if # was very literally it's own operator? What if it meant "retrieve the private field values of the object" such that this# is a syntax error by itself, but when used like this, this#.field returns this.[[PrivateFieldValues]].field? All other details would stay the same and the meaning of the sigil would be clear, easy to teach, and wouldn't muddy up developer code with field names that currently can't be field names.

One potential side effect of this is that the notion of "private names" becomes unnecessary. Since in this approach, the sigil would provide direct access (within a class method) to the [[PrivateFieldValues]] of the object to its left, current object access semantics would not need to be modified at all.

Has this idea been suggested already?

shannon commented 6 years ago

@rdking that looks similar to what I suggested in #75. In the "Updated Proposal" section of my description. Be warned, that's a very long thread.

rdking commented 6 years ago

@shannon It's the same kind of idea, except it:

rdking commented 6 years ago

@Ephys I wouldn't mind hearing what you don't like about the idea. Remember that it's not the same as the one @shannon in #75. It's the same kind of idea but the consequences are different.