private vs static private

I would tend to argue that a static initialization block is in fact indistinguishable from a static constructor. In every language where I encounter them, a static constructor is a block of code that is run to initialize any data on the object being constructed, after which it is no longer accessible. I would expect the same thing here.

At the same time, I also wonder what the danger is of having a class be aware of when it is being extended? That would be a useful feature as it would directly allow for the development of final. A class could reasonably refuse to allow itself to be extended.

The behavior of static initialization block in JavaScript is to not run for derived classes, so that makes them observably different from hypothetical static constructors. Similarly, static fields initialization, private or not, does not run for derived classes.

Whether we'd want to introduce static constructors is another question. However it'd be a breaking change to make private static fields initializers follow of a static construction pattern. Whether that breaking change is acceptable is also a question. I for one would be worried about making private and public static fields initialization work differently (and you definitely can't change public static fields semantics at this point)

There is a way to do it without causing a breaking change, but it would require new notation. I can see that as a point of contention. There would be the static initializer block:

class Ex1 { 
   static { /*your initializer code here */ }
}

and there would also be a static constructor function:

class Ex1 { 
   static constructor(/* No Args Allowed*/) {
      /*your initializer code here */
   }
}

In the absense of either, the static initializer approach would be used to keep consistency with existing code. However, whereever a static constructor is specified, the corresponding class has opted into sharing its initialization with descendants. It would be an error to specify both a static initializer and a static constructor.

Somehow, I can't see something like this passing, but this would be how I would choose to do it.

Sure static constructors are not a problem to add. As I said it's another problem, albeit related.

The problem is about finding a behavior that makes sense for private static fields initialization. I haven't seen a solution proposed to make them compatible with derived classes, and I can't really figure one out.

1 Like

That's just it. If a static constructor is used, static fields get initialized as they would if this were initialization of an instance. In all other cases, the initialization continues as it currently would. What are the problems with this kind of behavior?

The second and third points seem like non-problems, but the first seems like a dealbreaker.

A static constructor was explicitly rejected during discussion of the static block proposal. It must never be possible imo for a class to automatically react to the act of something extending it.

What's with that restriction? The concepts of both "abstract" and "final" are both exactly that, reactions to attempts to extend a class. So why shouldn't such meta-programming be allowable?

Yes, I definitely disagree with that assessment. While there can (and should) be accurate and logical argumentation about the design of features, and different mental models may lead to different conclusions about inconsistencies, I do not see them affecting usability of the features. Don't try to be logical only and disregard ignorance as "political" - show concrete, well-engineered real-world code that is broken or requires unusable syntax to work!

Please read carefully. I didn't write that it can't be passed, I wrote that it shouldn't. A program that does this is usually not well-typed. In what concrete use case do you have a property/field on both the constructor and its instances that serves the same purpose?

You say that while you wish to have members with the same name, you may want to use them differently, for different purposes. I say that this is bad software engineering: you should use different names then! I even welcome the compiler error about the naming conflict, forcing me to use appropriate names.

The fundamental design of all the builtin classes disagrees. Instances of classes that have internal state/data or are exotic objects should not be prototypically inherited from, the methods will not work on the derived objects. (Most can't even be proxied). You are supposed to subclass the class itself, and construct an entirely new instance that will have its own internal state/data. Or instead implement a wrapper object with the same public interface as the wrapped object (i.e. use the decorator pattern) - inheritance is just the wrong tool here.
Private fields work in exactly the same way.

That's spooky action at a distance.

You can't have static initializers behave differently based on the presence of an additional static constructor.

As I said the problems are related, but one doesn't solve the other. The only way I can see this work is through explicit support by the base class, with the static field forgoing an initializer, and the base class explicitly adding a class constructor to initialize the fields.

I agree I'd find it surprising for this behavior to be implicit. Maybe this would require some kind of explicit opt-in by the derived class, but short of additional syntax for class declaration, I don't see what would be sufficient.

All things considered, it seems like changing static initialization is not really possible. Maybe an alternative is my option 2, copying the static private fields initial value, and some kind of decorator to help initialize at first access. Again this would require explicit support by the base class.

As I said before, you're free to disagree, but at this point neither your opinion or mine on what got us to where we are is relevant. Besides, calling the issues that led to this state of affairs "political" actually came from discussions with various members of TC39.

Again, your opinion and you're free to express it. As a software engineer/architect who's been doing this for over 30 years, I'm usually pretty confident about my opinion, but not to the point that a good logical argument can't get me to change my position.

A concrete example of where your opinion is invalid is in the cfproteced library I mentioned above. This library provides a means to share the private members of a class with a class that extends it. That includes both instance and static data. So yes, a well-typed piece of code can provide the same functionality to both a class and its instances, and expect to find members of the same name on both.

I'm curious though. Do you find it equally as wrong when it's a public member with the same name on both the constructor and the instance? If so, then how do you deal with this perfectly well-typed example?

class Building {
   name;
   floors;
   floorRooms = [];
}

Not very detailed but an instance of this class will have a name property. Likewise, the class also has a name property. They're not even going to be used for the same purpose, and yet....

Yes, but the design of all built in classes are by definition an exception. They are all exotic, and required to be so. Each one of them has at least 1 function that requires access to something that is not a property of the object (internal slots usually). My statement was with respect to non-exotic ES objects. After all, you said it yourself:

I would even disagree with the first part of this statement because, if the designer is cautious and aware that developers may want to extend the class, it is easily possible to design a class with external state that doesn't exhibit issues when inherited. I'm working on a UI library of such classes even now.

This is where we disagree. Both composition and inheritance have their use cases. I would argue that those who blindly favor one tool over another don't fully understand either. But again, that's just my opinion.

The concept of static private is borrowed from compiled languages where in a call to an inherited public static function that accesses private data of the base class does not fail. That portion of the concept is critical to making the notion of static methods usable. Sacrificing it as has been done here is therefore "surprising".

It's not that the initializers behave differently. They would behave precisely the same as always, installing their data on the current context object. The difference is that the context object, in this case, would not be the constructor of the class that defined them, but instead the constructor of a derived class. This is no more "spoky action at a distance" than having a class definition define properties on an object that is not a product of the definition (instance fields).

No, currently static initializers run only once in the context of the class that defined them. Changing that behavior is a breaking change.

It is only a breaking change if there is no "opt-in" for the alternative. What is being suggested here is exactly that. The new behavior only takes place if the developer declares a static constructor() function.

Which is semantically different and visually separate from the static initializers, ergo the action at a distance. Adding something to the body of the class should not change the behavior of other parts of the body.

You say that, and yet fields currently do it. Be they public or private, there can be an effect on the behavior of other parts of the class body just because they are present. This is one of the footguns I and others brought up many times, only to have the issue summarily dismissed as being of low importance.

My argument here is that from a certain (still reasonable) perspective, this suggestion does not cause such a change in the behavior of other parts of the code. In fact, I just thought of an opt-in approach that doesn't even require new syntax.

class Ex2 extends Ex {
   static {
      super();
   }
}

It's a clear opt-in and makes it visually obvious that you're wanting to also initialize your new class with the static initializer block of the base class. No need for static constructor(). No "spooky action at a distance". Everything is up front and explicit. Does this work?

Can you clarify with an example. I'm not following.

There are 2 problems: the base class opting into having static init code run for every derived class instead of once, and the derived class opting into revealing itself to the base class.

The second one is fixable with static constructors, or whatever equivalent mechanism. I have yet to see a way to solve the former problem that doesn't change semantics in some surprising way.

Private fields interfere with the class body behavior in the presence of Proxy. Public fields interfere with the class body behavior in the presence of accessors. Unlike the initialization behavior you're worried about, these behavioral differences are continuous for the lifetime of the condition. Far more severe, and yet treated as though they are of little to no importance.

The second "problem" isn't even a problem as even with static initializers as they are now, the derived class can opt into revealing itself to the base class.

As for the first "problem", again I do not see how this is a problem. In fact, to be clear, you have designated this as a problem without even once describing the issue it can present. That makes it somewhat difficult to give you a satisfactory response. On top of this, you seem to have forgotten that I have altered my suggested syntax, thus removing the base class from being in control of whether or not its static initializer is run for a derived class.

Here's a reminder of the new suggestion:

class A {
   static #spvt = 'HHGTTG';
   static run() {
      console.log(`this.#spvt = ${this.#spvt}`);
   }
   #pvt = 42;
   run() {
      console.log(`this.#pvt = ${this.#pvt}`);
   }
}

class B extends A { 
   static {
      super();  //Causes static initializers of A to run against B
   }
}

Can you clarify? Are you talking about public fields being [[Define]] behavior?

They're just different. Both are external to the class body being defined. In the private case it's a proxy trying to interfere with an instance, in the public case (assuming define semantics) it's a derived class behavior. Neither is relevant to the problem at hand.

I suppose writing from my phone, I wasn't the most expressive.

Here is what I mean:

class A {
  static #foo = (console.log('init #foo'), 'foo');
}

The expectation of the A author is that the field init is only ever performed once ("init foo" printed once). Extending the class should not be able to change that. Adding something to the A body should not change that, unless it is semantically related (e.g. a decorator, or some kind of syntax on that specific private field declaration). I have not seen a reasonable suggestion on how to satisfy this constraint (besides moving the inline init statement into a new "static constructor" block):

class A {
  static #foo = 'default foo';

  static get foo() { return this.#foo; }

  static constructor() {
    if (this !== C) {
      console.log('init #foo')
      this.#foo = `A's foo`;
    }
  }
}
// Prints "init #foo"

class B extends A {}
// Prints "init #foo"
B.foo; // "A's foo"

class C extends A {
  static get foo() { return 'overridden foo'; }
  static constructor() {
    super(); // Does not print anything
    console.log(`super foo: ${super.foo}`);
  }
}
// Prints "super foo: default foo"
C.foo; // "overridden foo"

That's the only way I can imagine reasonably working with the constraints I stated. That is basically option 2 (copy initial value, preventing its collection), in which case I think it might be possible to emulate the static constructor logic into an accessor decorator that performs init on first access.

And that is exactly the problem, the base class should be in control of when its static initializers run, as the current class static fields semantics guarantee they are only ever run once.

I think I'm beginning to understand your perspective here, and yes, I was referring to the [[Define]] behavior. I'll repeat an argument that I posted regarding this choice. When creating a lexically defined object:

let x = {
   ...
};

it makes sense that the engine uses [[Define]] semantics to add properties on the new object since the object itself is a direct production of that syntax. Likewise with a class definition, it makes sense that the declared static members use [[Define]] semantics on the constructor, and the declared non-static functions and accessors use [[Define]] semantics on the prototype, as the constructor and attached prototype are direct productions of the class syntax.

Class instances are not direct productions of the class syntax. As such, class instances should not be subject to [[Define]] semantics imposed by the class keyword. Until this choice was made, it was always the case that only direct productions were created using define semantic. Violating this has created a situation where expectations of standard inheritance rules can be violated.

What makes this even worse is that the creation of field initializers completely side-stepped the need to avoid the prototype for the sake of avoiding the "object on prototype" footgun. As such, [[Define]] semantics could have been used on the prototype, and set semantics on the instance, thus preserving all expectations regarding inheritance... at least in the public case. Something similar could have also been done for the private case as well.

TL;DR: Since the class body of a class containing non-static public fields is not completely formed until all define statements generated by the class syntax have been completed, it cannot be said that these elements:

As for the private field case, I would agree that this is external to defining the class body. I was not limiting my arguments to that scope. I will do so for future arguments. I hope this clears up my understanding of this for you.

That's not necessarily a valid assumption. Consider the case of class A being defined in a function that is called multiple times with different parameters. This is not uncommon. Consider the case of class A being defined by an eval statement or Function() call. These might also print the log multiple times depending on how they're triggered. Claiming that there is a strict expectation of single execution is figuratively handcuffing developers, and limiting them to use cases you expect. I have personally written code using both of those alternative scenarios.

However, for the sake of argument, let's stick to the most common scenarios, where your description makes the most sense. I still can't say that I agree with your assertion. From my perspective, the expectation is that an initializer is used once to initialize its target field. This is subtly different from your assertion in that it accounts for the initialization of non-static fields as well. Those are most certainly expected to be repeated on each instance.

I think the problem arises because of the difference between static and dynamic languages. In a static language supporting class, it could be guaranteed that all classes are compiled at the same time and their corresponding structures set before the code has a chance to mutate anything. We're not so lucky. What makes this worse is that each function in the prototype chain of a given class is it's own top-level object, and subject to be used independently of the functions that extend it.

This is why my thought is to treat each derived class as if it were it's own island, and the definition of the base class were simply a mix-in, applying all fields and properties of the base to the derived and initializing accordingly. I would also think that it is ok for the decision of whether to use this behavior to be left to the derived as this would be more consistent with the behavior of static languages supporting this feature. After all, any surprises that occur would be the responsibility of the derived class developer, not the source class developer, much as is currently the case for non-static inheritance.

That being said, I'm not against the static constructor() approach, though I can't say I like the idea of a class preventing me from properly extending it by some means other than a final implementation.