proposal: Object Members

# Ranando King (25 days ago)

I've written up a new draft proposal based on my own work with ES5 & ES6 compatible classes with fields. That can be found here. I'm already aware of the class-members proposal, but I think it breaks far to many things and doesn't do anything to maintain the notion that "class is just syntactic sugar".

This proposal is specifically based on the code here. I've also got a repl.it that shows the same code running.

The idea behind the proposal is that instead of injecting a lot of new logic into how class works, let's allow class to remain syntactic sugar, and put that extra ability into object declarations instead. Then simply allow class to do the same with it's own prototypes.

# Jordan Harband (25 days ago)

class is already not just syntactic sugar, so that notion isn't correct, and shouldn't be maintained.

# Ranando King (25 days ago)

---------- Forwarded message --------- From: Ranando King <kingmph at gmail.com>

Date: Mon, Jul 23, 2018 at 4:04 PM Subject: Re: proposal: Object Members To: <ljharb at gmail.com>

You've made that argument before. Exactly what is it in ES6 that you can do with class that you cannot do without class? I'd like some clarification on this.

# Jordan Harband (25 days ago)

Extend builtins, in particular - ie, super() allows your subclass to obtain internal slots it can't otherwise get.

Even if class were just sugar, I don't think I see the argument that that's a good thing to preserve.

# Ranando King (25 days ago)

Granted about super(). That's the one thing I can't easily reproduce. However, barring those internal slots, I can reproduce the functionality of super and the checks performed as a result of the internal slots, all in ES6. As for built-ins, I can easily and properly extend builtins without class since ES6 officially has Object.setPrototypeOf(). If you don't think it's possible, you should take a close look at what I'm doing in the repl.it link from my first post.

As for whether or not the sugary nature of class is a good thing, it really is a matter of opinion. I just happen to be of the persuasion that since there's literally no construct that class can produce that I cannot reproduce by other means, then that means the class keyword (even in light of super) is little more than syntactic sugar. As such, we shouldn't be so hasty to turn an Object Oriented Prototype Based language into an Object Oriented Class Based language. The only way to do that reasonably is to ensure that whatever you can construct with class can always be equivalently constructed without it.

Here's a more logical argument instead. Even if there are subtle differences between class constructors and object factory functions, providing an isolated path specific to class is likely to lead to situations very similar to what happens when an open source package gets forked. Eventually, the difference between the two paths may become so great that one is eventually abandoned (by developers) in favor of the other. This is only a valid argument because the power of ES is in it's simplicity. It's like building a house with wood, nails, sheetrock, etc... (JS) vs. building a house with pre-fabricated parts (class-based languages).

Don't get me wrong. The class keyword is a great thing. It simplifies the production of creating object factories with prototypes. As I understand it, that was the purpose. Let's not make the mistake of allowing something to be done with class that cannot be reasonably reproduced without it. The moment we do that, we're diverging from the intended purpose of class.

# Ben Wiley (25 days ago)

What exactly can be accomplished with super that can't be accomplished otherwise? I know the transpiled code is very verbose and unintuitive to read if you avoid explicitly naming the base class, but I wasn't aware of new capabilities that were previously impossible.

Ben

Le lun. 23 juill. 2018 18 h 06, Ranando King <kingmph at gmail.com> a écrit :

# Jordan Harband (25 days ago)

When extending builtins, super() is the only way you can get the appropriate internal slots applied to the instance. (Private fields work the same way by providing a matching guarantee - that the only way someone can subclass you successfully is using class extends and super)

# Ben Wiley (25 days ago)

I see, so it's not that you can't do things without class as much as you can impose limitations by using class. Thanks for clarifying

Le lun. 23 juill. 2018 18 h 49, Jordan Harband <ljharb at gmail.com> a écrit :

# Jordan Harband (25 days ago)

That, and that the existing builtins already impose those limitations - and only class allows you to do those things, with them.

# Darien Valentine (25 days ago)

That, and that the existing builtins already impose those limitations - and only class allows you to do those things, with them.

I’m surprised by that statement — it appeared to me that it currently remains possible to create classes, including classes that extend built-ins, without class syntax.

const test = value => {
  if (typeof value !== 'string') throw new TypeError('nope');
  return value;
};

function StringSet(init) {
  if (new.target === undefined)
    throw new TypeError('StringSet is not a constructor');
  if (init !== undefined)
    init = Array.from(init, test);

  return Reflect.construct(Set, [ init ], new.target);
}

Reflect.defineProperty(StringSet.prototype, 'add', {
  configurable: true,
  value: Object.setPrototypeOf({
    add(value) {
      return super.add(test(value));
    }
  }, Set.prototype).add
});

Reflect.defineProperty(StringSet.prototype, Symbol.toStringTag, {
  configurable: true,
  value: 'StringSet'
});

Reflect.setPrototypeOf(StringSet, Set);
Reflect.setPrototypeOf(StringSet.prototype, Set.prototype);

new StringSet('abc');

Not that this is something one is apt to want to do normally, though it being possible does remain useful sometimes for meta/compositiony stuff. It cannot be achieved in an <= ES5 environment, but it seems all the reflection tools needed are present in environments that actually have class syntax — even, despite the awkwardness necessitated by HomeObject stuff, what is needed to employ super in methods.

I’m curious if, aside from the possibility of implementation-specific things like type error messages being different, there there is anything about the above class which ends up observably different from the ES-side from one created with class syntax instead?


Edit: I can think of one. Because of how the home object’s prototype has been set for add here, reassigning the prototype of StringSet.prototype will not affect the super reference resolution as it would in a class-syntax class. This can be very awkwardly addressed too, though:


Reflect.defineProperty(StringSet.prototype, 'add', {
  configurable: true,
  value: {
    add(value) {
      return Reflect.getPrototypeOf(Reflect.getPrototypeOf(this)).add.call(this, test(value));
    }
  }.add
});

(To clarify I’m not obv not suggesting this is better than class StringSet extends Set { ... }, just want to understand what can’t presently be done other ways.)

# kai zhu (25 days ago)

Extend builtins, in particular - ie, super() allows your subclass to obtain internal slots it can't otherwise get.

does extending builtins in the javascript-language even make sense, given the dominant industry application of it in a web-context? as i've said before, the primary industry-painpoints with javascript are integration-related, namely serializing/reconstructing JSON data between client <-> server. whatever low-level benefit you gain from extending builtins, typically is not worth the extra high-level integration-cost of serializing/reconstructing these custom data-structures to/from JSON.

p.s. - as an aside, new primitives like BigInt should have focused more on ease-of-use with JSON-serialization. BigInt’s primary use-case in industry as i see it, is as a mechanism for JSON-serializing 64-bit integers between client <-> server <-> database.

kai zhu kaizhu256 at gmail.com

# Michael Theriot (25 days ago)

Extend builtins, in particular - ie, super() allows your subclass to obtain internal slots it can't otherwise get.

Even if class were just sugar, I don't think I see the argument that that's a good thing to preserve.

Reflect.construct allows subclasses to obtain internal slots without super() / class syntax.

const SubDate = function (...args) {
  const instance = Reflect.construct(Date, args, SubDate);
  return instance;
};

Object.setPrototypeOf(SubDate.prototype, Date.prototype);

const sub = new SubDate();
sub.getDate(); // has internal slots, does not throw
sub instanceof SubDate; // true
sub instanceof Date; // true

This is the first I have heard class is anything but sugar.

# T.J. Crowder (25 days ago)

On Tue, Jul 24, 2018 at 8:00 AM, Michael Theriot <michael.lee.theriot at gmail.com> wrote:

Reflect.construct allows subclasses to obtain internal slots without super() / class syntax.

Indeed, Darien pointed that out as well (and if you two hadn't, I would have. :-)

This is the first I have heard class is anything but sugar.

The accurate statement would be that class lets you do things you couldn't do in ES5. But so does Reflect.construct. I believe it was important to the "no new" crowd that a non-class mechanism existed for creating objects using Error and Array as prototypes.

-- T.J. Crowder

# Jordan Harband (25 days ago)

As you've all pointed out, it's not "just sugar" in the sense that you couldn't do it in ES5; it's more that parallel syntax and API were created for the new functionality in ES6. Thanks for providing clear code examples of how one might extend builtins without class.

@kai: yes, extending builtins makes sense, in that it's an important part of ES6. Invoking "the web" doesn't negate any of the features of the language, new or old. Separately, not every web use involves any JSON serialization in either direction.

# Ranando King (24 days ago)

@ljharb: It seems you now understand what I was trying to say. Sadly, I'm not always the most eloquent.

As you've all pointed out, it's not "just sugar" in the sense that you

couldn't do it in ES5; it's more that parallel syntax and API were created for the new functionality in ES6.

The intent of my proposal is to provide both member fields and privilege levels to the class keyword, and the equivalent for object literals in a way that meets with both an intuitive declaration style, and a reasonable access notation that breaks as little as few as possible of the developers expectations of what can and can't be done.

# Andrea Giammarchi (24 days ago)

Private fields also won't work as expected and the mandatory super call in constructor is also different from ES5. Let's add species and special class related Symbol so that it makes no sense to define classes "just sugar" + there's no point in avoiding classes at all costs when any of these features is needed.

P.S. Babel mistakenly sold classes as "just sugar" and never worked properly with Custom Elements and builtins extend until version 7 which is still not perfect but at least it doesn't throw errors for no reason

# Ranando King (24 days ago)

Private fields also won't work as expected...

Can you clarify what you're referring to? I've created a library that essentially implements the functional parts of what I intend with this proposal. Of course the syntax isn't the same, and Proxy was used to create a membrane between the public and private storage, and I can't prevent that proxy from being passed to external functions, but those are ES-specific implementation details and not how it would be implemented in the engine.

... the mandatory super call in constructor is also different from ES5.

You shouldn't really try to compare ES5 and ES6. My statement that "class is syntactic sugar" refers to the fact that anything you can do with class in ES6 can also be done without class in ES6.

P.S. Babel mistakenly sold classes as "just sugar" and never worked

properly with Custom Elements and builtins extend until version 7 which is still not perfect but at least it doesn't throw errors for no reason.

Just because class is essentially syntactic sugar doesn't mean that the desugaring is backwards compatible with older versions of the language. I do not wish to imply that. Nor do I see the need to make such a statement true. Such an attempt to enforce backwards compatibility to that degree would prove excessively burdensome on the process of improving and adding features to the language.

# Andrea Giammarchi (24 days ago)

Proxy was used to create a membrane between the public and private

storage, and I can't prevent that proxy from being passed to external functions

this already underlines what I mean: classes are not just syntactic sugar because you cannot replicate what they do, not even using ES6.

having privates / proxies maybe exposed is not how I'd personally code.

# Michael Theriot (24 days ago)

What can classes do that ES6 can't?

# Ranando King (24 days ago)

this already underlines what I mean: classes are not just syntactic sugar

because you cannot replicate what they do, not even using ES6.

You're misunderstanding a few things.

  1. Any ES6 class I create using the class keyword, I can recreate without the class keyword. Period. There's no exception to this.
  2. What I was describing to you was the work I put in to create a class factory function that allows developers to easily create class instances with data fields of varying privilege. This work is the conceptual basis of my proposal. I never claimed even once that my example code was somehow a flawless implementation of my proposal. That would be expressly impossible. If it weren't, then I wouldn't be asking for a new feature. The fact that in ES6 I cannot prevent a developer from passing the proxied context object to an external method or assign it to an external variable is not an issue that would be suffered by an in engine implementation of my proposal. However, that would be a different version of ES, not ES6.
# Ranando King (24 days ago)

Since it seems to be such an inflammatory statement, I have removed most of the comments that infer the sugar-like nature of class from my proposal. It counterproductive to get stuck on whether or not class is syntactic sugar when the point is supposed to be about how to extend the language features around the class keyword.

# T.J. Crowder (24 days ago)

On Mon, Jul 23, 2018 at 8:38 PM, Ranando King <kingmph at gmail.com> wrote:

I've written up a new draft proposal based on my own work with ES5 & ES6 compatible classes with fields. That can be found here. I'm already

aware

of the class-members proposal, but I think it breaks far to many things...

That's quite vague.

Am I misunderstanding your intent here? It seems like you're proposing dropping the existing Stage 3 proposals (1, 2) in favor of this new proposal. Those proposals are the result of years of work and collaboration. It seems like any issues you have with them would be better addressed by raising issues on those proposals and engaging with the people working on them, rather than suggesting just throwing them out entirely and replacing them with something new and different. But perhaps I'm misunderstanding your intent?

-- T.J. Crowder

# Ranando King (23 days ago)

Let me see if I can clarify things a bit:

You're not misunderstanding. I've spent a fair amount of time in discussion on github in those very same issues. For various reasons, probably mostly due to the time investment the proponents of those proposals have given, it seems to be the prevailing view that the proposal as it stands, not withstanding the many glaring issues its detractors keep raising, is the best possible proposal that can be given. What I'm doing with my proposal is raising a formal counter-argument. I'm not simply developing a new proposal adhoc. Instead, I'm taking advantage of the many points and issues raised about the existing proposals and integrating the best possible solutions I and others can come up with that haven't been shot down for any logical or rational reason into a single new, self-coherent, non-binding, and mostly "means what you'd expect at first glance" proposal.

My goals are straight forward. I'll list them:

  1. Introduce the concept of non-public privilege levels (private & protected)
  2. Introduce a self-consistent means of accessing non-public members (operator #)
  3. Introduce a means of declaring non-public members with and without the class keyword
  4. Prevent the introduction of special case characters in an [[IdentifierName]]
  5. Prevent the breaking of the obj.field === obj['field'] usage pattern
  6. Prevent the unnecessary isolation of functionality into the class keyword
  7. Prevent the unnecessary limiting of future language expansion due to excessively limiting new feature.

Truth be told, the proposal I'm offering can be thought of as a heavy handed revision of the two proposals above. I am presently adding to my proposal a description of the implementation details required to make it all work. I hope that at the very least, from this you and others will be able to see that there is indeed a less limiting possibility than what has been proposed by proposal-class-fields. Even though I am submitting my own proposal, I will still continue to add my observations to the existing proposals.

# T.J. Crowder (23 days ago)

On Thu, Jul 26, 2018 at 7:28 AM, Ranando King <kingmph at gmail.com> wrote:

Let me see if I can clarify things a bit:

Thanks! That does indeed clear things up a lot.

I'd find it helpful (just speaking for myself here) if the proposal opened with a series of specific differences with the two existing proposals and statements of why your way is better. (If it refers to existing issues raised on their repos, links are great with a summary of where you think the issue stands.) You've made a few statements along those lines in that list, but if you could flesh it out in the proposal, that would be really helpful.

-- T.J. Crowder

# Ranando King (22 days ago)

Thank you for the advice. Those will be the next updates I make to the proposal.

# Ranando King (22 days ago)

I've just finished updating my proposal with an Existing proposals section that lists the major differences.

# Waldemar Horwat (21 days ago)

On 07/26/2018 01:55 PM, Ranando King wrote:

I've just finished updating my proposal with an Existing proposals section that lists the major differences.

Reading the proposal, I'm not yet sure what it's supposed to do. Some things I've noticed:

  • The proposal commingles static and instance private properties. In the first example, you could read or write this#.field2, and it would work. How would you encode the generic expression a#.[b]?

  • Worse, the proposal commingles all private properties across all classes. There's nothing in the proposed code stopping you from reading and writing private properties defined by class A on instances of an unrelated class B.

    Waldemar

# Ranando King (21 days ago)

The proposal commingles static and instance private properties. In the

first example, you could read or write this#.field2, and it would work. How would you encode the generic expression a#.[b]?

That's not quite right. I added some details about how all this works a few hours after you wrote this message. Here's the gist: The static keyword would cause field2 to be placed in the [[PrivateValues]] record of the function. The protected keyword would cause a key/value pair resembling "field2": Symbol("field2") to be placed in the [[DeclarationInfo]] record of the function. Since you used this, I will assume it is an instance of ExampleFn. The expression this#.field2 within ExampleFn would translate into

`this.[[PrivateValues]][this.[[DeclarationInfo]].field2] ->

this.[[PrivateValues]][undefined] -> TypeError`

assuming that the prototype of ExampleFn contained no definition for field2. Since the static field belongs to the function it is declared in, it can only be accessed via the function object itself, i.e. ExampleFn#.field2 or this.constructor#.field2 or the array-notation equivalents. I can't say that I'm understanding what you're looking for with a#.[b] That looks like a syntax error to me. If you're trying to understand how a#[b] works, it would be exactly the same. The best way to understand how it works is to look at this equation for the # operator.

<lParam>#<rParam> === <lParam>.[[PrivateValues]][<lParam>.[[DeclarationInfo]]<rParam>]

In the case of a#[b], the result would be:

a.[[PrivateValues]][a.[[DeclarationInfo]] [b] ]

I get that all the square braces can make that hard to read, so here's the step by step:

  1. Throw a type error if a is not an object
  2. Let D be the [[DeclarationInfo]] record of object.
  3. if D is not in the lexical scope chain of the current function, throw a TypeError
  4. Let N be the value for the key matching b in D, or undefined if no such key exists.
  5. Let P be the [[PrivateValues]] record of object a
  6. If N is not a key of P, Throw a TypeError
  7. return P[N]

Worse, the proposal commingles all private properties across all

classes. There's nothing in the proposed code stopping you from reading and writing private properties defined by class A on instances of an unrelated class B.

That's not right either. The example desugaring does indeed throw all maps into the same WeakMap. I'll work on that to make a better example. However, to get a better understanding of how I see the implementation, you should read the implementation details section. There's a much better description there. Besides, the 7 step description I just wrote for you should help you realize that:

  • If the function you're in doesn't know the names of the private fields of your object, you get a TypeError.
  • If the object you're accessing doesn't recognize the name of the field you're trying to access, you get a TypeError.

So there really is no way to apply the private field names of class A onto an instance of class B.

I'll spend some time tonight re-tooling my example desugaring and a few other details I thought of while writing this in the proposal. Hopefully, that'll prevent more confusion.

# Jordan Harband (21 days ago)

If the "name" is just a string, though, then it's just another global namespace. I could access a private field named "id" on every object that had one, in a try/catch, and i'd be able to not just observe if an object has an "id" private field, but access or alter its contents.

# Ranando King (20 days ago)

You're almost right about that. The guardian logic that ensures this cannot happen is that the [[DeclarationInfo]] of the instance whose private fields are being accessed must either be or be a prototype of the [[DeclarationInfo]] of the current function. This was part of the update I made to the proposal last night after replying to Waldemar. The only way for this to be true is that the current function would have been declared within the same declaration scope as the function used to create the instance. When this match doesn't occur, use of the # operator causes a TypeError.

# Darien Valentine (20 days ago)

Ranando, I share your reservations about private fields being bound too tightly to class syntax. In my case it isn’t because I don’t want to use classes, but rather because in the last few years, using the WeakMap solution, a good number of times I’ve needed to do things which the private field proposal either doesn’t permit or doesn’t account for:

  • Adding the same “slot” to multiple classes which don’t inherit from each other
  • Selectively sharing access to private state through functions declared outside the class body
  • Adding slots dynamically, e.g. when adding mix-in methods that may initialize a new slot if necessary when called, since subclassing is not always appropriate

With the WeakMap solution, the privacy mechanism is one that already exists: a scope. This makes it very flexible (it handles the above three cases fine), but it has a key limitation in terms of achieving privacy, which is that global.WeakMap and WeakMap.prototype may be compromised. Given this limitation — plus the amount of boilerplate WeakMap privacy can entail — I am very happy to see private instance state being addressed syntactically. However because the model chosen for “scope of privacy” is “class declaration body” — not previously something that provided a closure/scope at all? — instead of just using existing scopes, I have found them impractical to use in some cases.

If I’m understanding your alternative proposal, Ranando, I don’t think it addresses these issues either, not in the way I’m looking for anyway — I’m wishing for a syntactic solution for true private slots on objects, but where said slots are associated with a scope (almost always a module scope) rather than a class declaration. In particular, I’m not convinced that the concept of “protected” makes sense within the JS models of objects and dispatch.

I’m gonna get more detailed about what I see as inadequacies in the current proposal. These are subjective, but not hypothetical: I’ve been doing WeakMap-based privacy for a few years now and I’ve tried converting existing code to use private fields since Chrome shipped it behind a flag. I found that, unfortunately, it did not meet my needs.


Regarding exposing functions that operate on private state but which do not live on the constructor or prototype — there is a way to achieve this in the proposed spec. It’s awkward, but it is technically possible:

class Foo {
  #bar = 1;

  getBarOfFoo(foo) {
    return this.#bar;
  }

  // [[ ... other methods that may manipulate but do not expose #bar here ... ]]
}

const { getBarOfFoo } = Foo.prototype;
delete Foo.prototype.getBarOfFoo;

It gets more awkward in the “multiple classes with the same semantic slot” case, since one will have to wrap each attempted access in a try-catch, as there is no other way to be certain whether the target has the slot. With WeakMap, in contrast, one will just get undefined — and one may use the same WeakMap to manage the same slot across multiple classes that are declared in the same scope as the WeakMap.

Assume we have two classes with a private bar “slot” which is meant to be semantically equivalent. It holds an integer. We want to create a function that adds together two bar values from any classes that implement this slot. If an argument has no bar slot, bar defaults to zero. With WeakMaps, such a function might look like this:

function addBars(a, b) {
  return (Object(wm.get(a)).bar || 0) + (Object(wm.get(b)).bar || 0);
}

Realizing the same logic with classes that use private field syntax is still possible (using the aforementioned “pop off a method” pattern), but now it looks like this:

function addBars(a, b) {
  let aBar, bBar;

  try {
    aBar = getBarOfFoo.call(a);
  } catch {
    try {
      aBar = getBarOfBaz.call(a);
    } catch {
      aBar = 0;
    }
  }

  try {
    bBar = getBarOfFoo.call(b);
  } catch {
    try {
      bBar = getBarOfBaz.call(b);
    } catch {
      bBar = 0;
    }
  }

  return aBar + bBar;
}

¯\(ツ)


This is a more minor issue, but assuming we can’t have dynamic slots, I would like to take advantage of the fact that whether-a-function-may-access-a-slot is statically knowable by having immediate brand checking occur in all methods that may access private state. This is actually the main source of boilerplate in the WeakMap solution for me, but admittedly I’m probably in a tiny minority here:

set foo(value) {
  if (!wm.has(this)) throw new TypeError(`Illegal invocation`);

  const str = String(value);

  if (VALID_FOO_VALUES.has(str)) {
    wm.get(this).foo = str;
  } else {
    throw new Error(`Invalid value for foo`);
  }
}

The difference between the above function with and without the guard concerns guarantees about behavior. The String(value) call actually might throw, but it ought to be predictable that a method which requires a branded receiver always throws the same error when called on anything unbranded — even if (especially if!) private state access occurs in the method only conditionally, since without the guard, throwing/not-throwing/what-gets-thrown makes an implementation detail observable. The above example is minimal, but there could be more involved state manipulation or observable effects that occur prior to the first private access, possibly leading to being left in an invalid state.

Note that all host and intrinsic functions that may access slots perform these checks. It is the existing pattern in the language for this, and with a syntactic solution, it could be enforced automatically. Right now, with the existing proposal, the boilerplate still exists:

set foo(value) {
  try {
    this.#foo;
  } catch {
    throw new TypeError(`Illegal invocation`);
  }

  const str = String(value);

  if (VALID_FOO_VALUES.has(str)) {
    this.#foo = str;
  } else {
    throw new Error(`Invalid value for foo`);
  }
}

(You could drop the try-catch if you don’t care whether the error thrown reveals implementation details, but if, like me, you are aiming for behavior matching host APIs, the boilerplate actually increases.)

I can understand if these early checks are deemed undesirable, because they are strictly less flexible than the current proposed behavior, and they would also be incompatible with any solution that allows slots to be added dynamically (unlike the current proposal). However between this and the inability to manage privacy by scope instead of by class declaration body, I will probably find myself sticking with WeakMaps in general (in library code, anyway) because my attempted conversions have often increased rather than reduced complexity and verbosity.


Sorry this is a long post. It’s hard to talk about this subject without getting pretty wordy, but hopefully this is useful feedback about what at least one dev is looking for with private slots. It seems, admittedly, that those of us who need private slots to remain "reflectable" are in the minority.

FWIW I actually love # syntax though :)

# Darien Valentine (20 days ago)

To put this another, much briefer way, here’s a hypothetical, minimalist model for associating private state with objects that would cover me. Privacy would be provided...

  1. in the form of symbolic keys whose presence cannot be observed (i.e., they would not be exposed by getOwnPropertySymbols) without access to the symbol itself
  2. and which have a syntactic declaration so that one can be sure they are really getting private keys (i.e., an api like Symbol.private() wouldn’t work)
const bar = private();

// alternatively: const #bar; could be anything so long as it’s syntactic

class Foo {
  constructor() {
    this[bar] = 1;
  }
}

// etc

The keys would be typeof 'symbol'; the only difference being that they are symbols which are flagged as private when created. They would be permitted only in syntactic property assignments and accesses. Existing reflection utilities would disallow the use or appearance of such symbols both to ensure privacy and to maintain the invariant that they are always simple data properties:

Reflect.defineProperty({}, bar, { ... }); // throws type error
Object.getOwnPropertyDescriptors(someObjWithAPrivateSlot); // does not include it
foo[bar] = 2; // fine

This is significantly simpler than what’s in flight both in terms of syntax and mechanics, which makes me suspicious that I’m probably ignoring things that other people find important. However it would bring parity to ES objects wrt being able to implement genuinely private slots in userland with the same flexibility as what is done internally.

In total, this entails a new primary expression, a boolean flag associated with symbol values, and an extra step added to several algorithms associated with Object and Reflect.

# Ranando King (20 days ago)

In a word... wow. You've got me thinking hard here. Those are some peculiar use cases, and they do a great job of highlighting why someone might forego using class. One thing both proposal-class-fields and proposal-object-members have in common is that the focus is on producing instance-private fields. All 3 of the scenarios you presented lay outside of that focus for one reason or another.

Adding the same “slot” to multiple classes which don’t inherit from each

other

I'm a little confused by this one. Are you saying you want multiple non-hierarchally related classes to have an instance private field with shared name, such that the same private field name refers to a distinct and separate field on each instance of every such class, but where any such instance can have that field referenced by that shared name from any member function of the corresponding classes? (Wow that was wordy to write out...) If this is what you meant, you're describing friend classes. The top-down processing nature of ES makes this a difficult thing to create a clean syntax for without risking leaking the private state or fundamentally altering how ES is processed. Mutual friendship is even harder.

... and yet I just thought of a way to do it. By telling you this I'm leaving myself to consider writing a proposal containing 2 new keywords: befriend and friendly. I don't know if this can be done with the existing proposal being what it is. However, with my proposal, there's a chance. The friendly keyword would declare that an object is prepared to share select information with any object that befriends it. The befriend keyword would allow an object to request friendship with an existing friendly object. I'm not sure this is a good idea, though. This means that any object declared 'friendly' is automatically insecure as all it takes to gain access to the selected members of its private space would be to 'befriend' it.

Selectively sharing access to private state through functions declared

outside the class body

The example you gave above still declares the functions in question inside the class body, so that's not really a solution. If the example you gave actually solves your use case, then what you're asking for here isn't even needed. If, however, that was a bad example, then it sounds like you're looking for friend functions. See the previous section.

Adding slots dynamically, e.g. when adding mix-in methods that may

initialize a new slot if necessary when called, since subclassing is not always appropriate

Sounds to me like you'd love for class syntax to look like this:

class [<identifierName1>] [extends <identifierName2>] [mixes
<identifierName3>[, <identifierName3>[, ...]]] { ... }

so that the private fields of the objects in the mixes list are added to the set of private fields provided by the class definition directly. That would also require another proposal, but I think that can be done regardless of which instance-private fields proposal gets accepted.

# Darien Valentine (20 days ago)

Are you saying you want multiple non-hierarchally related classes to have an instance private field with shared name [...]

Yeah. This is a hard problem to solve when trying to integrate private fields with class syntax, but it’s not a problem at all when privacy is a more generic tool based on scope. This also isn’t a foreign concept in ES: consider this intrinsic method:

tc39.github.io/ecma262/#sec-arraybuffer.isview

This method returns true if the argument has the [[ViewedArrayBuffer]] slot. This slot exists on genuine instances of both %TypedArray% and %DataView%, but they do not receive these slots by way of inheritance from a common constructor. There are similar cases in HTML host APIs.

The befriend keyword would allow an object to request friendship with an existing friendly object. I'm not sure this is a good idea, though.

I don’t think it is either, no. It’s too much complexity for too little gain. But again, this is achievable “for free” just by divorcing “private object state” from class declarations (or object literals). I would ask: what problem is solved by making this a feature of the declarations themselves? Does it merit the complexity and the hoop jumping needed to handle edge cases?*

* One person’s edge case; another’s everyday concern haha.

The example you gave above still declares the functions in question inside the class body, so that's not really a solution.

If you’re referring to the first example, that is a demonstration of what is possible using the existing stage 3 class fields proposal as implemented in Chrome. It isn’t what I want; it’s what’s necessary to achieve this with the current stage 3 proposed model.

Sounds to me like you'd love for class syntax to look like this [[example with mixin syntax in declaration]]

Perhaps — it’s interesting for sure! But the pattern that already works, mixin(Cstr), is not presently a source of problems for me. Private object state in particular is only made complex by associating it with declarations instead of scopes that happen to contain declarations (or into which constructors are passed, etc). The complexity is artificial — not a good sign imo.

One thing both proposal-class-fields and proposal-object-members have in common is that the focus is on producing instance-private fields. All 3 of the scenarios you presented lay outside of that focus for one reason or another.

Both the WeakMap solution and the stub concept I provided after are more generic than privacy in either of those proposals. When I say "object private state," it’s true that the object in question could be any object. But in practice, any realization of the feature would pertain chiefly to class instances, and the examples I gave, though contrived, do concern class instances. The reason private object state is chiefly an issue of class instances stems directly from the nature of prototype methods and accessors, so if you are not making use of prototypes, you could instead have used a closure+factory directly.


In a nutshell, my issue with existing proposals could probably be summarized as a concern that they are neither as generic nor as simple as native slots. To be clear, proper “slots” are an internal concept, only observable indirectly — but they are the special sauce underlying a number of behaviors which are presently awkward to achieve in ES code itself, and they are a nice simple model of private object state which is tantalizingly close to, but not exactly the same as in two critical ways, symbol keyed properties. That said, “real” slots would continue to have an advantage with regard to cross-realm stuff even if private symbol keys existed.

That such a model is radically simpler — minmax and all that — feels very important to me, but I dunno. I’m not holding my breath for big changes here. The current stage 3 proposal seems to be unstoppable; much smarter / more important people than me have already tried and failed. :)

# Ranando King (20 days ago)

I've almost given up on making any significant headway in either adjusting or flat-out correcting the flaws in that proposal, but I don't intend to stop trying until either we get stuck with that proposal, or they understand and accept what I'm telling them, or logically prove that my concerns are either irrational or inconsequential.

Private object state in particular is only made complex by associating

it with declarations instead of scopes that happen to contain declarations (or into which constructors are passed, etc). The complexity is artificial — not a good sign imo.

That's not quite right. What you're essentially asking for is a violatable private field, or as has been described by others, a "soft private". Since we agree that the "friendly" & "befriend" pair is a somewhat (if not completely) bad idea, I'm going to take 1 more pass at your 3 requests with a different angle.

Adding the same “slot” to multiple classes which don’t inherit from each

other

Selectively sharing access to private state through functions declared

outside the class body

//Using my proposal
var {A, B, C} = (() => {
  const common = Symbol("common");

  class A {
    private [common] = 1;
    add(...args) {
      var retval = this#[common];
      for (let obj of args) {
        retval += obj#[common];
      }
      return retval;
    }
  }
  class B {
    private [common] = 2;
    optional() {
      console.log(`common member = ${this#[common]}`);
    }
  }
  var C = {
    private [common]: 3,
    required() {
      console.log(`common member = ${this#[common]}`);
    }
  }

  return { A, B, C };
})();

//So you want the following statement to not throw a TypeError and return 6
(new A()).add(new B(), C);

I'm not sure I can make this work in my proposal, and I'm absolutely sure you'd be flatly refused by the other proposal. If a Symbol is provided as the [[IdentifierName]] of a private or protected field, then I can let that Symbol be both the key and value that are added to the [[DeclarationInfo]] and [[InheritanceInfo]] records. That way there will be a common private field name usable by all 3 objects. However, the guardian logic tries to verify that the function trying to access the private fields of an instance is a member of the same or descending prototype that was used to create that instance. If I removed that requirement, it would work. However, there'd be no way to keep the private data from being leaked. Sadly, it's all or nothing with this approach. Hard private or soft private, those are the only choices. The TC39 board has already decided that what they want new syntax for is hard private.

Adding slots dynamically, e.g. when adding mix-in methods that may

initialize a new slot if necessary when called, since subclassing is not always appropriate

Because the TC39 board has set their sights on hard private, this will require new syntax like what I suggested earlier Adding private members dynamically would also pose a leak risk if it could be done after the prototype has been fully constructed. The main reason the privacy is set on a declaration level is because scope-level inheritance isn't very good for class-oriented inheritance. The class keyword was provided to simplify the vertical inheritance model, along with some API to enable inheritance from native objects even without using class. The syntax changes for simplifying private field declaration are just an extension of that. Even though it's not unusual for some developers to spend a lot of time working with fringe use-cases, syntax changes are almost always going to be made for the most common use cases first. Maybe after the private fields problem has been resolved, someone will figure out a better way to handle your use cases.

# Darien Valentine (20 days ago)

What you're essentially asking for is a violatable private field, or as has been described by others, a "soft private".

We might have different definitions here, but I would describe what I’m talking about as hard private. Soft private, at least as it appears to have been defined in prior discussions, described an avenue where symbol keyed properties were given a new syntactic form — but they were still just regular symbol keys, and therefore could be introspected by outside agents who had not been given express privilege to do so:

[...] the core would be that "private state" is simply (public) symbol-named properties, with syntactic sugar for those symbols, and possibly some kind of introspection over them [...]

The thread goes on to contrast the soft model with an earlier version of the private fields proposal seen today. The hard private example uses the class declaration as a pseudo-scope, but contrasting these two options as if they are binary is not accurate: hard private through module/function/block scope already exists, it is just difficult to work with in the context of shared prototypes — one must either use WeakMaps, technically giving up hardness because of the forgeability of global.WeakMap / WeakMap.prototype / WeakMap.prototype.get|has|set, or be willing to either not worry about garbage collection or implement it manually. This could be solved for with a few rather undramatic changes, though.

Notably, the first post there lists the following as a disadvantage of the soft model it describes:

Platform objects, both within ECMAScript and in embedding environments, contain hard private state. If a library wants to be high-fidelity and just like a platform object, soft-private state does not provide this (@domenic)

...but neither model there quite covers that use case. Platform objects can see each other’s private state (cf the isView example earlier, or scan the DOM API specs / Chrome source a bit to find numerous examples). It’s only the ES layer interacting with their interfaces that cannot.

Such things can be achieved with ordinary scope, which is why the WeakMap pattern has worked in practice in my experience to date, while class-declaration-scoped privacy has not. It isn’t uncommon for a library’s exposed interface to be composed of an object graph, where privacy is a concern at this public interface level, but library internal state may be interconnected in unexposed ways under the hood. The most familiar example of this is a DOM node tree. As an experiment, perhaps try to implement the relationships between HTMLFormElement, HTMLFormControlsCollection and the various form control elements using either the main private fields proposal or your alternative proposal and see what happens.

However, the guardian logic tries to verify that the function trying to access the private fields of an instance is a member of the same or descending prototype that was used to create that instance.

Because I’m looking at this in terms of slots, I’d first point out that prototypes don’t determine slottedness, the execution of some specific constructor does. It’s during this process that slots are associated with the newly minted object by its identity. But even the current private fields proposal tracks this behavior closely, and I’m not sure how else it could work. The [[Prototype]] slot of an object is typically mutable (R|O.setPrototypeOf, __proto__) and forgeable (Proxy’s getPrototypeOf trap). Why/how would its value matter when it comes to accessing private state?

const pattern = /foo/;
Reflect.setPrototypeOf(pattern, Date.prototype);
pattern instanceof Date; // true
pattern instanceof RegExp; // false
pattern.getMinutes(); // throws TypeError because [[DateValue]] slot is missing
RegExp.prototype.exec.call(pattern, 'foo'); // works; object has RegExp private slots

If I removed that requirement, it would work. However, there'd be no way to keep the private data from being leaked. Sadly, it's all or nothing with this approach. Hard private or soft private, those are the only choices.

In the context of what you’ve described here this may be true, but no such limitation presently exists. We can already do all this — hard, leak-free privacy, brandedness, “friends” etc — with scopes and WeakMaps, but for the fact that the WeakMap intrinsics may be forged. So what’s baffled me is this: why are all the proposals exploring this space not addressing that relatively simple existing problem, and instead starting off from a place of significant new complexity? You said “maybe after the private fields problem has been resolved, someone will figure out a better way to handle your use cases,” but I’d have hoped for the opposite — I want the primitive building blocks which things like class field syntax could be built over, if it is found that they are still necessary once the root issue is solved for.

The main reason the privacy is set on a declaration level is because scope-level inheritance isn't very good for class-oriented inheritance.

Can you explain this more? I’m not sure what’s meant by “scope-level inheritance” here.

I don't intend to stop [...]

I very much admire your dedication! I’m also digging the discussion. I think we may be representing viewpoints at opposite extremes here, so it’s an interesting contrast, but it also probably means we may be lacking some context for understanding one another’s angles. I’d be curious to hear more about what you see as the problems with the current fields proposal + how your members proposal would solve them; the repo readme didn’t seem to include a rationale section.

# Isiah Meadows (19 days ago)

BTW, I came up with an alternate proposal for privacy altogether: tc39/proposal-class-fields#115

TL;DR: private symbols that proxies can't see and that can't be enumerated.

Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (19 days ago)

Let me see if I can roll this up: you're looking for syntax support for something akin to Java and C#'s "internal" privilege level, and a means to declare a "common private name" so that something like this will work:

var {A, B, C} = (() => {
  static common = Symbol("common");

  class A {
    internal [common] = 1;
    sum(...arg) {
      var retval = this#[common];
      for (let obj of arg) {
        retval += obj#[common];
      }
      return retval;
    }
  }

  class B {
    internal [common] = 2;
    sum(...arg) {
      var retval = this#[common];
      for (let obj of arg) {
        retval += obj#[common];
      }
      return retval;
    }
  }

  class C {
    internal [common] = 3;
    sum(...arg) {
      var retval = this#[common];
      for (let obj of arg) {
        retval += obj#[common];
      }
      return retval;
    }
  }

  return { A, B, C };
})();

(new A()).sum(new B(), new C()); //If it works, returns 6

It's already the case that A, B, & C will have to include the [[DeclarationInfo]] of the arrow function in each of their respective methods' __proto__ chains. That and the Symbol for direct use as a private name, solves the problem, and avoids the original issue without much of a change. The "internal" keyword would be trickier to implement as it would require that the corresponding declarations be added to the [[DeclarationInfo]] of the containing scope while the storage exists in the [[PrivateValues]] of the owning objects. It's not impossible, possibly not even difficult. I'll have to give it some thought.

# Darien Valentine (19 days ago)

Isaiah, that’s pretty similar to what I was talking about earlier — and I think it’s awesome that multiple people have arrived there, since it seems like soft confirmation of the idea that private symbols likely represent the most minimal possible “surgery” to achieve the functionality.

There are some differences; I saw Symbol.private as an obvious API too, but specifically mentioned it to point out that it’s not viable if we consider one of the goals here to be the elimination of the “tamperability-hole” that exists for any non-syntactic solution (unless global.Symbol is redefined as non-configurable, non-writable, etc, but this is likely not an option).

The proxy forwarding is very interesting, but the prototype lookup doesn’t make sense to me. Slots/fields are associated with an object, not subject to the vagaries of that object’s present prototype chain. I don’t think changing that relationship is a good idea.

# Isiah Meadows (19 days ago)
  1. I don't believe tampering is a major issue, especially considering most tampering problems occur after the type is instantiated. If it is, the follow-on proposal of added syntax would likely avoid it.
  2. The prototype part is just for consistency and code reuse. And it's not about the object it's associated, but about the key lookup itself.

Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com

# Michael Theriot (19 days ago)

Private symbols sounds like an easy win. They would be painfully simple, real properties, not just variables with property imitation syntax that undoubtedly confuses people. With the added benefit that children can truly override the base class, freedom to define private members shared across otherwise unrelated objects, and even injection. My only concern is that it could cross into WeakMap use cases.

# Isiah Meadows (19 days ago)

It will, but weak maps will still remain useful for cases when you're semantically dealing with a key/value map. In theory, you could implement a weak map on top of this 1, but in practice, it doesn't always make sense to do it. A good example of this is if you are "tagging" an object with data. If this data isn't really part of the object itself, you shouldn't be using a private symbol for it. Another good example is if you're doing simple caching and you need to clear the weak map by replacing it. Using private symbols for this doesn't really fit with the domain here, so you're more likely just to confuse future readers (including yourself) if you do this.


Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com

# Michael Theriot (19 days ago)

Right, I wouldn't, but I'm concerned others would misuse it. I don't think it's a blocker though, and actually frees weakmaps from trying to fill this role.

# Isiah Meadows (19 days ago)

I'm aware it's possible to misuse, but if concerns of misuse were a serious issue, we wouldn't have iterators, for example 1, 2. But IMHO freeing weak maps from a role they weren't designed for substantially outweighs the risks of abusing them further (and the abuses are incredibly frequent).


Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com

# Michael Theriot (19 days ago)

Also throwing this out there, symbols would now carry additional information: private or normal. Would it be better to configure this on objects instead?

E.g. Object.setPropertySymbolVisibility(object, symbol, true / false)

(and then ideally sugar for this)

That way a symbol's visibility on an object is information held on the object rather than the primitive. A little more work involved, but lines up with Object.defineProperty and symbols remain purely unique identifiers.

# Isiah Meadows (19 days ago)

Um, no. The use case is extremely limited, and that ruins a few optimizations you could otherwise make with private symbols (like caching proxy forwarding without having to bail out).

Besides, whether a symbol is private requires exactly one bit to store, so there's no real overhead with storing it on the object. Heck, if you want to optimize it better, you might choose to store that same bit on both the symbol and the object descriptor itself, and I'd expect engines to do just that - it saves a pointer dereference.

Isiah Meadows me at isiahmeadows.com, www.isiahmeadows.com

# Michael Theriot (18 days ago)

I'll just say it feels inconsistent with how every other property is configured. That the key itself holds magic behavior-changing information. It's not a use case or overhead concern.

# Ranando King (18 days ago)

Isn't this just a different version of the private names idea that Kevin & Daniel were pushing before settling into "proposal-class-fields"? If not, then what's the difference?

# Waldemar Horwat (18 days ago)

On 07/29/2018 04:37 PM, Isiah Meadows wrote:

BTW, I came up with an alternate proposal for privacy altogether: tc39/proposal-class-fields#115

TL;DR: private symbols that proxies can't see and that can't be enumerated.

Aside from syntax, the main semantic difference I see between this alternative and the main one is that this alternative defines private fields as expandos, creating opportunities for mischief by attaching them to unexpected objects. Aside from privacy, one of the things the private fields proposal gives you is consistency among multiple private fields on the same object. In the rare cases where you don't want that, you could use weak maps.

 Waldemar
# Isiah Meadows (18 days ago)

First, my private symbols are properly private. The only "unexpected" thing that could happen is making an object larger memory-wise, which engines already have to be equipped to handle now (libraries aren't always well-behaved, and like to occasionally add expando properties to builtins and DOM elements). About the only thing most people would care about is in the debugger.

Second, I had things like this in mind with supporting expando properties: nodejs/node/blob/ae4fde8bc883686def5badfb324236320669e8f4/lib/internal/linkedlist.js

In that case, the Node.js people made it a pseudo-mixin rather than an actual type for performance reasons - there's fewer object allocations and they needed that.

So I've considered the expando problem, and I disagree about it being a problem at all.


Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (18 days ago)

So you're wanting the ability for a 3rd-party function to be able to store data private to that library on an object it didn't create, and that only that library can access?

# Isiah Meadows (18 days ago)

That is one supported use case, yes. But that isn't the only use case this supports. It can still extend to traditional private class data, too.


Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (18 days ago)

Just that use case alone is problematic. If the 3rd party function is not extensible, then the new private data should not be allowed. If the library cannot function without storing that data, then the function will have no choice but to fall back to WeakMaps which don't care if the key is not extensible. So why not just stick with WeakMaps for that case? And if that's the case, then there would be little need for so open a means of defining private field names. The proposal I'm offering offers the room to extend it in the future to support everything else you might look for from your private symbols idea.... unless you think I missed something.

# Ranando King (18 days ago)

I meant to say if the object passed to the 3rd party function.....

# Isiah Meadows (18 days ago)

The reason private symbols are appropriate for Node's use case is because it's conceptually a mixin, not a simple key/value map with various utility functions (and weak map lookup is slower than property access). JSDOM uses a similar utility 1 as a sort of mixin.

Keep in mind, I'm specifically against the abuse of weak maps for private state that's conceptually (in an abstract sense, not runtime) part of an object. Weak maps make sense when the weak map is the dictionary conceptually (think: caching). But if conceptually, the object is the dictionary, putting it in a weak map is giving the engine the wrong info - properties have inline caches and heavy optimization, but you can't do the same for weak maps in the other direction without literally implementing them as properties. (I would love to be proven wrong here, BTW.)

Let me draw a quick comparison: When do you use a map/set with string keys, and when do you use an object instead?

  • Both are functionally equivalent, but engines use very different algorithms for each one.
  • I can almost guarantee you don't use maps when object properties work.

One last thing: how would you hope to deal with module-internal data stored on arbitrary objects, using any means other than private symbols or something similar? To clarify, I'm talking of opaque object structs 2, not simply classes. (BTW, that one is easier to manage as a struct rather than a class, because of how many "methods" there are operating on the state.)


Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (18 days ago)

One last thing: how would you hope to deal with module-internal data stored

on arbitrary objects, using any means other than private symbols or something similar?

Isn't this precisely what WeakMaps are for? If the data is "module-internal", then the module needs to be the owner of the data store. If the data is about "arbitrary objects" (object from outside the module?) then those objects are the keys to the data store. If any object is thrown away, the associated data is no longer needed. If this doesn't fit the functionality of a WeakMap, I don't know what will.

Weak maps make sense when the weak map is the dictionary conceptually (think: caching).

Isn't that precisely what your question calls for? You're caching module-internal data about external objects.

Keep in mind, I'm specifically against the abuse of weak maps for private

state that's conceptually (in an abstract sense, not runtime) part of an object.

Likewise, I'm specifically against the abuse of objects to store state unrelated to the factory that created it. To me, that's as if I came to visit you and somehow you managed to hide some valuable info in my wallet without me noticing, and even if I completely dismantle my wallet, I wouldn't be able to find it. But somehow you can easily retrieve it the next time I come around. That's just conceptually weird.

# Isiah Meadows (18 days ago)

Isn't this precisely what WeakMaps are for? If the data is "module-internal", then the module needs to be the owner of the data store. If the data is about "arbitrary objects" (object from outside the module?) then those objects are the keys to the data store. If any object is thrown away, the associated data is no longer needed. If this doesn't fit the functionality of a WeakMap, I don't know what will.

Consider what people often use public symbols for now. For example, consider this library 1. In this case, they use a public symbol for their stuff in this file 2.

But here's the thing: that doesn't really need discoverable, and is a pure implementation detail. Wouldn't it make more sense for them to just use a private symbol instead? Because here, it's not a cache, but it's literally extra associated data in the object. And also, in that case, you want the engine to see it as a property, since it can employ relevant IC caching for it.

Isn't that precisely what your question calls for? You're caching module-internal data about external objects.

No, I'm not. I'm drawing a distinction between a pure many-to-one association (weak maps) and a "has a" relationship (private symbol properties). You could implement one in terms of the other, but these two types of relationships are completely different at a conceptual level and how you model them.

For js-symbol-tree, it's not simply associating a node to a value, but setting up the object so it has the data required for a doubly linked list tree node. Because this symbol is repeatedly accessed, it's not caching so much as it's adding data the object needs for it to do what it needs to do.

Another scenario is for JSDOM's Window implementation, where they have a few underscore-private variables like this 3. That particular variable is used in several disparate parts throughout the code base 4, but is still conceptually a property. This is a case where a private symbol property is appropriate.

Conversely in this JSDOM file 5, it's just associating data with an arbitrary object it happens to have, and so using the weak map makes perfect sense.

Likewise, I'm specifically against the abuse of objects to store state unrelated to the factory that created it. To me, that's as if I came to visit you and somehow you managed to hide some valuable info in my wallet without me noticing, and even if I completely dismantle my wallet, I wouldn't be able to find it. But somehow you can easily retrieve it the next time I come around. That's just conceptually weird.

All of the examples here I've presented are for scenarios where the state is related to the factory that created the objects. It's not directly related (and thus encapsulation is warranted), but it's still related, enough so that you usually see the state initialized within the creator's constructor call. It's about as related as the superclass is to a subclass of it.

BTW, you could make a similar argument against superclass private fields - it's like hiding valuable info in your wallet before you receive it for the first time, but even after dismantling it, you can't find any evidence of that valuable info.


Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (17 days ago)

Consider what people often use public symbols for now.

I know that I use them as fixed-value unique keys (module globals) for properties on objects that I export and don't want others to be aware of or able to touch.

For example, consider this library [1]. In this case, they use a public

symbol for their stuff in this file [2].

And as I said before, if someone passes a non-extensible object to this library, it breaks. Since any library can request any object be sealed or frozen, the implementation of this library is too fragile.

Because here, it's not a cache, but it's literally extra associated data

in the object. And also, in that case, you want the engine to see it as a property, since it can employ relevant IC caching for it.

Here's a parallel for you. Right now, Google has a database with information about me on it. I don't have access to that information (module privacy) and that information isn't stored anywhere on me or my devices (module locality). This is a proper conceptual model. The information Google is keeping about me is information they generated. Why should I have to pay to store their information? Thankfully I don't. However, this is precisely what you're claiming to be a good use case. You want module privacy without module locality. If I were to play at a Kevin Gibbons-like response, I'd say you've identified the common pattern, but that pattern itself is a bad use case, and the use of private symbols as you have defined them doesn't do anything to correct the technical issue. Since you cannot stick new properties onto a non-extensible object, even private symbols won't solve the problem with your use case.

No, I'm not. I'm drawing a distinction between a pure many-to-one association (weak maps) and a "has a" relationship (private symbol properties).

First, for any given property bag, the keys will need to be unique, but that doesn't force uniqueness onto the values. As such, even properties on an object provided by your "private Symbol" would still be many-1. When a 3rd party library wants to keep information associated with an arbitrary object, there are only 3 choices:

  • try to store that information on the object
    • this is what you're advocating, but it's not a good pattern. It's too fragile, being subject to break if the incoming object is not extensible.
  • store the information as being associated to the object (WeakMap)
    • this is the pattern that works in all cases (but the syntax is cumbersome and the implementation somewhat slow)
  • return a wrapper containing the original object and the new information (Proxy or custom wrapper)
    • this is another possibility, but requires that any users accept and use the new Proxy or wrapper object in lieu of the original.

Another scenario is for JSDOM's Window implementation, where they have

a few underscore-private variables like this [3]. That particular variable is used in several disparate parts throughout the code base [4], but is still conceptually a property. This is a case where a private symbol property is appropriate.

It's not just "conceptually" a property. It's logically a property. Why? Because all the objects that it exists on were constructed somewhere within the JSDOM library. That's me putting my keys in my pocket. There's absolutely nothing wrong with that.

Conversely in this JSDOM file [5], it's just associating data with an arbitrary

object it happens to have, and so using the weak map makes perfect sense.

Conceptually speaking, this is the same scenario as SymbolTree. In both cases, the library is generating information associated with an object it doesn't own and didn't create.

BTW, you could make a similar argument against superclass private fields

  • it's like hiding valuable info in your wallet before you receive it for the first time, but even after dismantling it, you can't find any evidence of that valuable info.

That dog don't hunt. The difference here is that in your use cases, library A is "sneakily" storing information on object B. In the case of superclass private fields, subclass B has "volunteered" to take on the information and functionality of class A. You've essentially compared apples and asteroids just because they both begin with "a".

# Michael Theriot (17 days ago)

Should a sealed/frozen object be privately extensible? I don't actually know, but interesting point.

# Darien Valentine (17 days ago)

I'd say you've identified the common pattern, but that pattern itself is a bad use case, and the use of private symbols as you have defined them doesn't do anything to correct the technical issue.

I think there’s been a misunderstanding. Everybody agrees that that’s a bad pattern. It’s not what the point of private symbols would be. It’s not a target use case.

Since you cannot stick new properties onto a non-extensible object, even private symbols won't solve the problem with your use case.

That appending private symbols to external objects which are frozen wouldn’t work doesn’t matter precisely because it’s not a target use case. That it doesn’t work reliably might even be considered a positive, since it discourages something we all seem to agree is not good practice.

It’s also not related to private symbols; this is already how properties work, regardless of what kind of key they have.

The difference here is that in your use cases, library A is "sneakily" storing information on object B.

What use case are you referring to here? I can’t find any example in the previous posts that matches these descriptions. As Isiah said, “all of the examples here I've presented are for scenarios where the state is related to the factory that created the objects.” The same is true of my examples. Everybody’s on the same page regarding not wanting to add properties to objects their own libraries do not create.

# Ranando King (17 days ago)

What use case are you referring to here?

In the case of SymbolTree, the objects in use are external.

I think there’s been a misunderstanding. Everybody agrees that that’s a

bad pattern. It’s not what the point of private symbols would be. It’s not a target use case.

That certainly puts my mind at ease.

As Isiah said, “all of the examples here I've presented are for scenarios

where the state is related to the factory that created the objects.”

If the factory that creates the objects is the also the only thing trying to store private information on those objects, then I understand you're only looking for per-instance module-private data, possibly with the ability to use common private names. If that's the case, then it really is just 2 simple extensions of my proposal:

  • allow a Symbol when used as a private or protected property name to persist as the private Symbol name for the private instance field on each object for which it is used.
  • create an additional privilege level (internal) that places the new field's name in the [[DeclarationInfo]] of the function containing the declaration.

The effect of using these 2 features together is that anything within the same function as the declared Symbol will gain access to the internal field of all objects using that Symbol as a field name.

# Darien Valentine (17 days ago)

You’re right, sorry — the SymbolTree example does operate on objects not created in the module itself, so my statement wasn’t accurate.

More carefully I ought to have said that the use cases concern object creation. Decorators and mixin functionality can fall in this bucket, where the object is likely not literally “birthed” by the library that is doing the decorating, yet the functionality is intended to be attached during that process by a consumer of the library.

In my own experience to date, all cases where I have run into the class-declaration scope limitation did concern locally created objects (class instances specifically), so yes, the adjustments you are talking about wrt the object members proposal probably would be able to cover them, though I would still tend to favor a more generic and simple solution.

# Isiah Meadows (17 days ago)

If you look at my proposal, it would be a "no" for the same reasons you can't add normal properties to them.

Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (17 days ago)

I get where you're coming from. The main reasons I've written my proposal this way are:

  • Prior art:
    • Many ES developers come from other, class-based, object-oriented languages where keywords are the primary way of controlling data accessibility. This means using the well known keywords will lower the learning curve and increase adoption.
  • Future expansion:
    • Careful and narrow definition of the keywords and their corresponding actions allows undesirable patterns to be avoided while leaving room for future extensibility.

The argument I raised with Isiah was just 1 example of a bad pattern that being too generic can open up. These are also part of the reasons why I am against proposal-class-fields. It seems so simple on the surface, but effectively makes an already limited keyword even more limited than is should be. Plus it adds difficulty to creating new features around those concepts in a way that will be easily understood by developers migrating from other languages.

# Isiah Meadows (17 days ago)

If you go back a few months, what you're proposing is very similar, at least functionally, to my previous iteration of my proposal: isiahmeadows/private-symbol-proposal/blob/c5c9781d9e76123c92d8fbc83681fdd3a9b0b319/README.md

My main problem was that trying to limit private properties to objects created within a scope got complicated in a hurry once you considered all the small details, and it just didn't seem simple anymore. It only got more complicated when you started getting into the logistics of integrating with modules.

So I've considered the issue and explored it pretty thoroughly - I really don't want private data to be limited to classes (which I dislike), but I did also previously have the concern of trying to limit who could define properties where.

I will point out that you can prevent arbitrary private extension by simply doing Object.preventExtensions(object). Because properties defined using private symbols are otherwise just normal properties, they still have to go through the same access checks normal properties have to, like [[IsExtensible]]. The only other concrete difference is that proxy hooks don't fire when you do things with private symbols.


Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Jordan Harband (17 days ago)

Note that builtins with internal slots, like Map, Set, and Promise, are still mutable after being frozen - so if one is trying to model internal slots with some kind of property stored on the object, then freezing must have no effect on the ability to alter their contents.

# Isiah Meadows (17 days ago)

The argument I raised with Isiah was just 1 example of a bad pattern that being too generic can open up.

BTW, the risk for bad patterns doesn't necessarily justify exclusion of a feature. As I've brought up here before, iterators can be seriously abused similarly 1, thanks to the availability of throw

  • return, and you can always make a deferred out of a promise by simply pulling the resolve/reject out of the promise callback's scope. As long as it looks strange and weird enough and we have more semantically appropriate alternatives, I don't think we have an issue with potential abuse, and I feel private symbols as I have them meet this threshold.

Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Isiah Meadows (17 days ago)

Yeah, I left it without saying that you could just model them as having their state as a single private symbol field with all the relevant data for it. I assumed it would be obvious enough for those who really pay attention to the spec, so I just left it implied.

Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (17 days ago)

Thanks for that information. I wasn't yet sure how to handle it. A parallel question is this: Is there any particular reason that the private container itself shouldn't be mutable? Or more directly, is there a good reason for private fields to only be create-able at declaration time? So far, all the logic I've created hinges on the reference to [[DeclarationInfo]] (which keeps all the known private names). Since that container is created at declaration time, it's not unfeasible for private properties to be appended after the declaration. I'm not particularly fond of the idea, but I'm also trying not to let my own biases commit me to a decision.

# Ranando King (17 days ago)

If you go back a few months, what you're proposing is very similar, at

least functionally, to my previous iteration of my proposal:

That functional similarity is intentional. After pouring over years worth of posts, I figured out what the vast majority of the proposal-class-fields detractors actually wanted: an elegant, easily recognized syntax for adding private members to objects.

My main problem was that trying to limit private properties to objects created

within a scope got complicated in a hurry once you considered all the small details, and it just didn't seem simple anymore.

I noticed that about your proposal too. I'm also pretty sure that Daniel E. and Kevin G. ran into the same issues back during the proposal-private-names days which is why the private names concept is just an implementation detail in their current proposal. My proposal is made less complicated by breaking the problem down into the 3 pieces required to make it all work:

  1. a record to store private data
  2. an array to hold references to the schema records of accessible private data
  3. a schema record for the sharable data.

In this way private = encapsulated on a non-function, protected = private + shared, and static = encapsulated on a function. It should be easy to sort out how the data would be stored given such simple definitions. These simple definitions also mean that encapsulation is naturally confined to definitions. Attempts to alter that state lead to strange logical contradictions and potential leaks of encapsulated data. I have thought of the possibility that private data could be added after definition, but every attempt I make to consider such a thing has so far led to a risk of leaking.

I've been working on some code that can serve as a proof-of-concept in ES6. It will implement all of my proposal that can reasonably be implemented in ES6 using Proxy. It's already in the proposal repository under the POC branch, but it's still a WIP. For now, it already supports inheriting from native objects. I'm working on subclassing right now. By the time I get done (likely this coming Monday), it should support every feature in my proposal. I'm basically using it as a means to check the viability of my proposal.

# Isiah Meadows (17 days ago)

Do you have a link to this proposal so I can take a look at it? It'd be much easier to critique it if I could see the proposal text.

Isiah Meadows contact at isiahmeadows.com, www.isiahmeadows.com

# Ranando King (16 days ago)