When using inheritance with the new class syntax it's often advised to not call any methods from within the constructor to avoid problems caused by acting on an incomplete object (fields not yet defined / initialized), however this extends to more than just methods and in real world applications I find it to severely limit constructors and to some degree contradict their very purpose of setting up the state of the object.
Consider the following (abridged) example:
class InputElement extends HTMLElement {
get required () { return this.hasAttribute('required') }
set required (v) { this.toggleAttribute('required', v); }
constructor (conf = {}) {
super();
this.required = !!conf.required;
}
}
class FancyInputElement extends InputElement {
#update () { ... }
set required (v) {
this.#update();
super.required = v;
}
}
new FancyInputElement(); // error: cannot access private field
I think there's two possible solutions to this, either execute the constructor of InputElement with a partial prototype chain according to it's associated prototype: HTMLElement <- InputElement
rather than HTMLElement <- InputElement <- FancyInputElement
or have all explicitly defined class fields setup before executing constructors which imho would be the preferable solution.
That's not really an issue with constructors and fields. Believe me when I say that there are many, many issues with fields, but this isn't one of them. Constructors are only intended to initialize the class they are a part of. However, with the way you've done this, you actually have the base class depending on a detail of the derived class. You have to be sure in your designing that each class constructor only bothers its own details and those of it's ancestors.
If you really need to be able to bother the details of derived members, you need a means of providing access to those members to the base class. Most languages supporting class use some sort of protected mechanism for that. However, there are TC39 members who would rather "die on a hill" than see such a feature implemented. There is my CFProtected library that can help, but it still relies on you to not try to directly override public members and use them to call private members that have yet to be initialized.
In truth, this is one of the design flaws of private fields, but that road is very much dead where arguing is concerned. They made a lot of mistakes with this, but we're stuck with them now.
The sub class fields are not installed until the super constructor has completed. This is something developers need to be aware of when using inheritance and overloads. It applies to other languages too.
One solution is to track which method were called early before the super constructor has returned, so then the subclasss constructor can do the work when it is ready. The pattern would look a little like:
class FancyInputElement extends InputElement {
static #callUpdate = new WeakSet();
constructor(...args) {
super(...args);
if (FancyInputElement.#callUpdate.has(this)) {
this.#update();
}
}
#update () { ... }
set required (v) {
if (! #update in this) {
FancyInputElement.#callUpdate.add(this);
} else {
this.#update();
}
super.required = v;
}
}
@rdking Sorry but I think you've misunderstood my example code as the base class in it is completely agnostic to its derived classes. The problem at hand is that one can not safely override any code that can eventually be reached from within any constructor in the inheritance chain.
@aclaymore I find the workaround you propose extremely unattractive as it adds a ton of verbose and error prone complexity for something that could "just workβ’", it also alters code execution order: what if #update depends on the super setter not having been executed yet, now we have to handle two different possible underlying states. Furthermore derived classes should be agnostic to the internal workings of their parent class, your code hard-couples the base-class constructor code to the design of the deriving classes, it's also leaking memory but that could be fixed with even more code.
I realize that this is currently "expected" behavior which is why I'm proposing language changes to improve / alter the current behavior. I think from an ergonomic pov having private fields available would be nice (and in-line with public fields) but I can see since class syntax is supposed to promote good practices doing the incremental prototype chain approach I suggested might be the better option.
I'm afraid I didn't misunderstand you at all. Maybe if I restate my point you'll understand. It's not even safe in a language like C++ to call a derived function from a base class before the derived class has been initialized without planning. That's just part and parcel of learning to deal with class hierarchies. Consider that any function on the prototype of an ES class is very similar in behavior to a virtual function member of a C++ class. If you were to set up the same thing in C++, and let the private function of the derived try to do something with an uninitialized variable, I'm sure you can guess how unpredictable the results might be.
If you're thinking something like "but that's different since the private function in C++ will exist already", then just make "update" a private function pointer that is later to be initialized by the constructor. (Note: this actually isn't that unusual a thing to do.) The point is, you'll get a bad result either way. The moral of the story is don't mess around with uninitialized things. The derived class setter should trap any exception caused by calling #update and handle it cleanly, preferably by doing as @aclaymore suggested and deferring the call to #update until the FancyInputElement constructor has run.
Just so you understand, the design flaw isn't in the base class. It's in the derived. Haphazardly overriding functions in any language that allows it can lead to bad results. It's the job of the developer to know how to get around those issues in the language.
Unfortunately the two proposed solutions have their own downsides.
Installing private fields early
while this could stop the exception, it would also hide an error of using a field before it is initialized
class B extends A {
#f = true;
methodCalledBySuperConstructor() {
this.#f; // undefined if installed early before the super constructor
}
}
The fields would also need to be installed twice if the super constructor overrides the return value:
class A { constructor() { return new DifferentA(); } }
class B {
#f;
constructor() {
// if #f was installed on the instance being constructed now, before super
super();
// #f wouldn't be installed on 'this' now, because A swapped to a different instance
}
}
Adding the prototype incrementally. The two issues this causes are: not backwards compatible, and performance implications:
There can be existing code that successfully overrides a methods used by the super constructor today, and this would break if the prototype was applied incrementally
Performance wise, most JS engines de-opt their internal representation of objects if their prototype is changed
The good news is if someone did want this incremental prototype behavior then that can be implemented in user-land already and opted-into:
// Re-usable utility:
function incrementalProto(klass) {
return class _ extends klass {
constructor(...args) {
// Construct the super class directly, using its own prototype;
const _this = new klass(...args);
// Now set the prototype
Object.setPrototypeOf(_this, new.target.prototype);
return _this;
}
};
}
I don't find C++ to be a good analogue to explain javascripts behavior considering it's a compiled language with (from my experience) vast differences in how compilers handle various cases.
The following is C#:
using System;
class Vehicle { // base class (parent)
public virtual void honk() {
Console.WriteLine("Tuut, tuut!");
}
public Vehicle () { // constructor calling method
this.honk();
}
}
class Car : Vehicle { // derived class (child)
private string modelName = "Mustang";
public override void honk() {
Console.WriteLine("Honk, honk! Says the " + this.modelName);
}
}
public class Program {
public static void Main() {
Car myCar = new Car(); // outputs "Honk, honk! Says the Mustang"
}
}
All fields are initialized before any user code (including constructors) is being executed, imho that's also what the syntax suggest (in both languages) as otherwise the values would be assigned in the constructor rather than the class definition. This is already true for public fields in JS, only private fields behave differently (js code again):
class InputElement extends HTMLElement {
get required () { return this.hasAttribute('required') }
set required (v) { this.toggleAttribute('required', v); }
constructor (conf = {}) {
super();
this.required = !!conf.required;
}
}
class FancyInputElement extends InputElement {
update () { console.log('updatin') } // update is now public
set required (v) {
this.update();
super.required = v;
}
}
customElements.define('x-test', FancyInputElement);
new FancyInputElement();
This works as expected: update being called during super constructor, you can run it in devtools.
If the c# example is updated to have a field that requires initialization then the code also breaks, in that the field exists but is not properly initialized. One benefit of JS private fields throwing an error is that it stops these types of things from being a silent error, which is likely a mistake.
using System;
class Vehicle { // base class (parent)
public virtual void honk() {
Console.WriteLine("Tuut, tuut!");
}
public Vehicle () { // constructor calling method
this.honk();
}
}
class Car : Vehicle { // derived class (child)
static private int count = 0;
private string modelName;
public Car (string modelName = "") { // constructor calling method
this.modelName = modelName;
}
public override void honk() {
Console.WriteLine("Honk, honk! Says the " + this.modelName);
}
}
public class Program {
public static void Main() {
Car myCar = new Car("mustang"); // outputs "Honk, honk! Says the"
}
}
I could have been clearer, but the point I was trying to make is backing up the notion that calling virtual methods in a constructor has issues in other languages, including c#.
Field initializers in JS can reference 'this', it seems less complex overall to not special case the ones that refer to simpler (constant) values as being initialized at a different time.
class B extends A {
#f = 100; // if this gets installed and initialized early
}
// code then updated to:
class B extends A {
#x = 10;
#f = this.#x * 10; // this small change would impact the order the fields are installed and/or initialized
}
If it's only private methods you need, and not fields you can use a static private method.
class A extends Base {
static #update() { ... }
set prop(v) {
A.#update.call(this);
super.prop = v;
}
}
I will admit its not beautiful, but it makes the semantics explicit. Static values are initialized with the class declaration and the code is looking up the private function on the class, and not relying on this having completed subclass initialization.
Perhaps it's possible to relax private methods to be allowed to be called without being installed. I wasn't involved with the class fields proposal so I'm less sure on why the restriction applies to private methods. Private fields erroring on early access makes more sense as it is likely that early access will only return undefined and not the value that the subclass actually wants to assign. It also wouldn't have the base class overriding the instance value (this).
I didn't use C++ to explain javascript's behavior, but rather to show that even in other languages, what you were trying to accomplish is a no-go. I used C++ simply because all of how ES works can be mapped directly into C++ equivalents. This shouldn't be surprising since V8 is a C++ implementation of an ES engine.
What you're arguing about here is something I argued about with various TC39 members to no avail. In any language design, there are tradeoffs to be made. Unfortunately for us, they decided to pursue their own personal agendas instead of what most developers would expect from such features when implementing fields. All I can say is "they had their reasons".
In either case, your statement still isn't accurate. Public fields aren't initialized until the constructor is run, just like private fields. That's one of the many problems with fields, and it leads to other issues. For instance:
class A {
data = 55;
}
class B extends A {
#data = 42;
get data() { return this.#data; }
}
console.log((new B).data); //prints 55
Why does this not work properly? It's because the accessor is on the prototype while the field is directly applied to the instance (something TC39 insisted on to avoid a well known, commonly handled footgun, only to create a more surprising one). Despite years of arguing, they didn't see this issue as significant enough warrant not going after their personal goals. So we're stuck with it.
All griping aside, what you're either missing, or maybe directly arguing about, is that unlike functions in a class definition which are placed on the prototype, all fields are defined onto the instance. Other than the overlooked technical issues with doing this, it forces the requirement that this exists before applying the fields. As a result, no fields can be initialized before super completes.
So in your example, it's not just that there's a difference between public and private, but also differences in placement and placement timing. Your public update function isn't a field anymore. If you make it a field, you'll get the problem back.
class FancyInputElement extends InputElement {
update = function update () { console.log('updatin') } // update is now public
set required (v) {
this.update();
super.required = v;
}
}
To put it simply, private functions are little more than read only private fields with functions assigned to them, just like a public function is just a public prototype property with a function assigned to it.