private vs static private

I often fail at providing the real-world use case I am after ... 'cause it takes time ... so right now I am implementing the DOM, you read that correctly ... thousand nodes with potentially zero attributes is what I am after, for both memory constraints and logic sake, so that their element.attributes NamedNodeMap thing doesn't need to exist for everything that doesn't need to access, or set, those attributes.

My example can then be re-written as such:

class Element {
  static #attributes(element) {
    return element.#attributes || (element.#attributes = new Map);
  }
  #attributes = null;
  setAttribute(name, value) {
    // note: the attribute is simplified for convenience in this thread
    Element.#attributes(this).set(name, { name, value });
  }
  getAttribute(name) {
    return this.#attributes?.get(name)?.value;
  }
}

And so on ... so yeah, thousands easy new instances that won't need at all the following, as the following will create X thousands Map objects for no reason whatsoever:

class Element {
  #attributes = new Map; // <- there!
}

Accordingly, am I still right that using a class private field, that "obviously" (in your opinions) shouldn't be named after the instance private field itself, is faster than attaching N private fields to each instance?

I always strive for best performance (with least RAM needed) and every of my libraries do too ... so that having a clear statement here from TC39 would help me shaping this new wonder too, thank you.

edit to be honest, the current state not aligned with globals (not privates) stinks for both engines and DX, but I am sure it's been discussed plenty already.

I'd love to provide more context to that "rant" ...

  • 20 years of easy disambiguation between prototype and Function fields worked
  • everything works as expected with the _ prefix workaround, as that's still public and not really private, yet that's what everyone has used until private landed in ECMAScript
  • long lasting patterns that use without issues fields VS static fields erased by some internal shenanigan around private meaning for classes
  • somebody mentioned "in Java is the same" because the meme has to live happily ever after that "JavaScript is not Java" but that's how we move forward? By checking what Java does?
  • there is no logical explanation for the current behavior but many tried very hard to explain why is that, and every "why" is an implementation detail, instead of a DX focused answer ... the DX is suboptimal here and from DX perspective nobody cares if private is a single or a double WeakMap (or whatever engines do), one per class static fields, one per instances fields
  • one of the biggest and most relevant meme about programming since about ever is that naming is hard ... and here we have an implementation detail related inability to name an own, VS a static class field, to do whatever best and semantic name was chosen by any developer?

If there's one XMas wish I have is that this discussion get closed with a link that points at specs updates so that a class can have any private field for any instance among any private class field the class need, without an impossible (to argue in favor) limitation about name clashing that from a user, developer, JS learner, makes literally zero sense.

Thanks for reading and hearing, in case you did.

I'd probably use a differently-named private field to hold a null object rather than a Map. You could certainly benchmark whether a private field is faster than a closed-over WeakMap, though.

you answered nothing relevant to this topic though ... so I am no interested in discussing what private name I've used and I have RAM traces already my solution works RAM wise.

Did you add anything relevant to this topic that I've missed thought? This is about this simple thing any eyes can disambiguate, but JS can't:

class Why {
  static #isThat = 'class reasons';
  #isThat = 'instance reasons';
}

Any other discussion around examples fail shortly to explain why that class can't exist as it is and I am not interested in those conversations neither (the one where my made-up examples are not ideal 'cause they are examples, in particular).

I did; you were asking about performant ways to solve your problem, and I provided that - an object should be faster than a Map.

The reason it was banned is because fields can be on any object, and it would be very confusing and ambiguous when you typed obj.#isThat whether obj was an instance, or a constructor, so this way people are forced to use a name only to mean one thing - which is better code anyways, you shouldn't want to overload a name like that imo.

not necessarily for has, get or delete operations, but that was completely out of context and scope?

no? it has been working forever with Class._thing() VS instanceOfClass._thing ???

I want? one of my point is that naming is th hardest thing in programming (whichis true) and you are telling me I should use different names when none of this is needed with public fields, the most common use case?

Once again, I am not interested in opinions here, I am interested in DX and so should TC39.

The DX here is error prone (literally thrown on execution) and surprise prone and it has no reason to explain that static fields are meant as clashing with instance fields as that's never been the case for 20+ years.

Please update specs as there's no reason to defend current state if not by saying "we didn't think about it" ... it's extremely hard to explain yet another inconsistency in the language with private fields, there is no argument that bakes the current state in an explainable way, since everything works with public fields without any issue whatsoever, imho.

Sadly, in their zeal to address concerns that at best were only tangential to the main goal of providing support for object private data in classes, they ended up convoluting instance object scope and constructor function scope into some new half-baked monstrosity called class scope. That class scope is the same for all instances of the class as well as the class constructor itself. That's where the private names for the private fields "live".

Believe me when I say that the ship has sailed. I tried my best to present them with logical arguments as to why the approach they were taking was bad, as did several others. All we can do now is accept that this is what happens when politics plays a heavy role in development. No point in arguing about it. You can only either figure out a good way to use it, or abandon it until circumstances force you to do otherwise.

Good luck.

more than a person said if it throws now, it doesn't have to throw forever, so this is what I am after (without any great hope, as I know this space well enough).

Or you can perpetuate the inconsistencies meme about TC39 but I am sure every time that happen some member fights back heavily to defend the decision or the status quo ... I believe here nobody can defend thae fact the inconsistency is obvious, DX hostile, and hard to explain to anyone new learner, from a foreign PL too, without feeling embarassed ... it's really obvious there was a mistake in the current resolution, it's hard to justify the way it is considering the history of static fields, from functions to class, that never clashed with instance fields to date.

DX is subjective and is thus almost entirely opinion. And yes, my opinion is that it would be horrifically bad DX to permit the semantics of an in-scope name to be overloaded like you're asking for. We thought about it, and made this choice intentionally, and this choice made the DX superior.

You don't have to agree, but that's exactly why it's subjective.

I'm curious as to why that would be "horrifically bad DX" when that's exactly what we've always had. Try thinking about this from the developer perspective.

foo.bar; //"bar" is a public property of object "foo"
foo.#bar; //"bar" is a private property of object "foo"

This isn't the reality, but this is the common way to think about it. So given:

class X {
   static a = 21;
   constructor() {
      this.a = 42;
   }
}

... it is not unusual in any way to have both X.a and (new X()).a, both with different values. So if it's valid code for public, why would it be so bad to have the same thing for private? What makes it horrifically bad? X and new X() are 2 completely different objects in the same way that a & b in let a = {}, b = {}; are 2 completely different objects. As such, it is acceptable to have a property with the same name on both of them in both cases. So I don't understand what argument might justify this being a bad DX with private fields when the only difference from the developer perspective is a #.

Because what we’ve always had had bad DX. This is better.

I hope that’s not the common way to think about it, that’s what the syntax is supposed to help prevent - that’s the wrong mental model.

To those of us who do not believe that this feature of the language was bad DX, the path chosen makes little to no sense. Even taking into account your belief that the existing pattern was bad, introducing inconsistencies in functionally similar syntax is an even worse DX, despite how good the individual case for the new functionality might seem to some.

Even if it was bad DX, it was well understood bad DX. Now you've got a mixture of well understood bad DX and poorly understood "good?" DX. Nevermind that the new "good?" DX introduces new footguns. The mixure makes the overall DX even worse than what it already was. Reasonable levels of consistency in intentionally similar features is required to have a good DX.

Try thinking about it this way: if private had been implemented in such a way that it resembled public properties precisely, save for the necessary difference of not being publicly accessible, then the overall DX at least wouldn't have degraded, and there would be no new footguns.

Thank you for explaining your point. I get your reasoning, but I understand it to be too narrow and short sighted. Aren`t' you glad you're not required to agree?

1 Like

Like I said above, you'd have to benchmark to know the answer to this question. I'm afraid it is not possible to give a clear, unambiguous answer to almost any instance of "is this faster than that".

Personally I think having obj.#x mean either the static field or the instance field depending on what obj is would in fact be worse DX than what we have.

I'm not saying anything about implementations here, though that's relevant as well. I'm saying it would be worse as a reader of code for there to be this ambiguity that you're asking for. It is in fact better for these names to be unambiguous. We could change it, but I think that would be worse.

You can disagree, of course. But you'd have to convince people that your preferred semantics are better. I am not convinced. Just saying "it makes zero sense" is not convincing, since I think the current state makes much more sense than allowing names to be ambiguous.

I don't find anything horrific with the following pattern (made up for this reply purpose):

Class.hasOwn(instance, key);
instance.hasOwn(key);

What I find horrific is the will to keep breaking or ignoring legacy and consistency that worked for dozen years in the name of "we know better now". In the book this is simply yet another inconsistency to be aware of that brings nothing better to any user, it's just hostile and inconsistent.

I also start believing that I can move forward avoiding completely private fields and that's sad, but at least I can write consistently anything I want to write, without asking permission to anyone:

const pvt = Symbol();

class MyWay {
  static [pvt] = 1;
  constructor() {
    this[pvt] = 2;
  }
}

I don't know about perf VS just a prefixed _ for the name but at least symbols have slightly better guards than public fields (both JSON and postMessage safe out of the box).

Of course I could also use a different name for the static field but this discussion suggests me there's really too much magic behind the private fields and it's magic I don't really like ... which is very unfortunate.

Consistency with 'string' based fields could lead to an incorrect mental model of how they work. Each private field declaration creates a new unique name, almost like a special unique symbol.

Even without private static fields this can be seen in a few ways:

function createClass() {
  return class {
    #f = 1;

    compare(v) {
       return this.#f === v.#f;
    }
  }
}

const C1 = createClass();
const C2 = createClass();
const c1 = new C1();
const c2 = new C2();
c1.compare(c2); // Error 'c2' does not have the same private field as 'c1'

another way to see this:

class Outer {
    #f = 1;

    static Inner = class Inner {
       #f = 2;

       compare(v) {
           return this.#f === v.#f;
       }
    };
}

const o = new Outer();
const i = new Inner();
i.compare(o); // Error 'o' does not have the same private field as 'i'

If static and instance fields could share the same name this would break this model of "each declaration creates a unique name".

An idea at the time for a follow on proposal to address use cases where the same name wanted to be shared was something like this (exact syntax I can't remember):

private #foo;

class C {
   static [#foo] = 1;
   [#foo] = 2;
}

class Friend {
   [#foo] = 3;
}

Which is similar to the shared symbol approach, but with the added privacy guarantees that come with private fields.

2 Likes

all your examples would also fail (not throwing though) with Symbol used privately or created within new scopes ... right?

function createClass() {
  const f = Symbol();
  return class {
    [f] = 1;

    compare(v) {
       return this[f] === v[f];
    }
  }
}

const C1 = createClass();
const C2 = createClass();
const c1 = new C1();
const c2 = new C2();
c1.compare(c2); // false

We don't have a way to brand-check private fields as in Reflect.hasPrivate(instance, #field) so while I am OK in your example throwing instead of just returning false I don't understand what is so difficult to name uniquely static fields VS instance fields ...

class Why {
  static #name = 1;
  #name = 2;
}

why is so difficult, if a private scope map or whatever is used behind the scene, to disambiguate between names there? it's statically easy to understand one is a :name and one is a .name, or one is a c-name and another one is a i-name ... or ... I mean, we all know how AST works and I am sure I could use Babel to transform private fields into something compatible with static fields or instances one.

So, where is the impossibility or the impossible to solve thing there? Why can't static fields get a differently unique private name out of the box?

There is:

#field in instance; // returns true/false

Because the access site wouldn't know which one is being looked up:

class C {
  static #f = 1;
  #f = 2;

  static {
     function f(v) {
         return v.#f; // is this looking up `class:#f` or `instance:#f`
     }
  }
}

1 Like

After a super quick lunch I might have an understanding of what's going on and why it's difficult ...

class Why {
  static #name = 1;
  static name(ref) {
    // this is static, it's not clear if `ref` would be the `Why` class itself or an instance
    // hence it's not possible to disambiguate at the syntax level ... is this the actual issue?
    return ref.#name;
  }
  // instances private
  #name = 2;
  // this is an instance accessor ... `#name` there gotta be the second one
  get name() {
    return this.#name;
  }
}

If my comment in the static case is why actually it's difficult to solve this problem I still think there's an easy way to transform that code:

  • instance methods don't need any disambiguation or guards as even binding the class would throw there as non-existent #name (as it's the instance field one, not the class one)
  • only static methods defined within the class, as any other lazy attempt would file at implementing new private fields or accessing these, require some overload or logic in place to exclude the ref is not Why

As methods could also use ref.#name without knowing what is ref, this is how I can imagine a Babel transformer for that code:

const privates = {
  static: {
    name: Symbol()
  },
  field: {
    name: Symbol()
  }
};

const guard = (ref, name, ...rest) => {
  const symbol = (
    ref === Why ?
      privates.static :
      privates.field
  )[name];
  if (!Reflect.has(ref, symbol))
    throw new Error(`Unable to access #${name}`);
  return rest.length ? (ref[symbol] = rest[0]) : ref[symbol];
};

class Why {
  static [privates.static.name] = 'static';
  static name(ref) {
    // return ref.#name;
    return guard(ref, 'name');
  }
  [privates.field.name];
  constructor(name = 'field') {
    // this.#name = name;
    // constructor can be direct
    this[privates.field.name] = name;
  }
  // instances methods and fields don't need guards
  // when `this` is explicit but can work with guards
  // too if there is a reference to disambiguate
  get name() {
    // return this.#name;
    return this[privates.field.name];
  }
  set name(value) {
    // this.#name = value;
    // disambiguation example
    guard(this, 'name', value);
  }
}

console.log(Why.name(Why));
console.log(Why.name(new Why));
console.log((new Why).name);

Would anything even remotely similar to this solve the current issue? Is this a performance hazard that makes privates too slow to work with?

An even simplified approach ... requires more work at class definition but once it's done ... it's done:

const statics = {
  name: Symbol()
};

const fields = {
  name: Symbol()
};

const symbol = (ref, name) => (ref === Why ? statics : fields)[name];

class Why {
  // define statics
  static name(ref) {
    return ref[symbol(ref, 'name')];
  }
  static [statics.name] = 'static';
  static get [fields.name]() {
    throw new Error('wrong reference');
  }
  static set [fields.name](_) {
    throw new Error('wrong reference');
  }

  // define fields
  [fields.name];
  get [statics.name]() {
    throw new Error('wrong reference');
  }
  set [statics.name](_) {
    throw new Error('wrong reference');
  }

  // the class
  constructor(name = 'field') {
    this[fields.name] = name;
  }
  get name() {
    return this[fields.name];
  }
  set name(value) {
    // example: not needed with `this`
    this[symbol(this, 'name')] = value;
  }
}

console.log(Why.name(Why));
console.log(Why.name(new Why));
console.log((new Why).name);

with @decorators the dance would be just:

@throwWithFields(statics, fields)
class Why {
  static [statics.name] = 'static';
  static name(ref) {
    return ref[symbol(ref, 'name')];
  }
  [fields.name];
  constructor(name = 'field') {
    this[fields.name] = name;
  }
  name() {
    return this[fields.name];
  }
}

There are two separate unique private identifiers that can throw out of the box as non configurable accessors so that any attempt to access private fields within non static methods will throw if the the reference is the Why class itself and only public static methods require a guard to disambiguate, even if I believe that's also a not really needed use case, although I understand the correctness around it.

Putting it all together

const statics = { name: Symbol() };
const fields = { name: Symbol() };

const guardPrivate = (ref, name, symbol) => {
  if (!Object.hasOwn(ref, symbol))
    throw new Error(`private #${name} not reachable`);
  return symbol;
};

const guardField = (ref, name) => guardPrivate(
  ref, name, fields[name]
);

const guardStatic = (ref, name) => guardPrivate(
  ref, name, (ref === Why ? statics : fields)[name]
);

class Why {
  static [statics.name] = 'static';
  static name(ref) {
    return ref[guardStatic(ref, 'name')];
  };

  [fields.name];
  constructor(name = 'field') {
    this[guardField(this, 'name')] = name;
  }
  name() {
    return this[guardField(this, 'name')];
  }
}

console.log(Why.name(Why));
console.log(Why.name(new Why));
console.log((new Why).name());
console.log((new Why).name.call(Why));
// last one throws rightly so

I don't think done internally this latest case is super expensive or particularly convoluted but I don't know internals around private fields. I believe there's gonna be a guard somewhere so this won't add much overhead (in theory) it will just slow a micro-second bit only static fields private accessors (I think) ... but that's OK as it won't be noticeable, imho.