I've used the same title of this blogpost, if you have 6 minutes (??? da hell Medium!) to read it, but the gist is that while everything defined within a class declaration (developers' eyes and syntax) is already available at instance definition time, class fields end up surprising developers.
The ClassFieldDefinition Record Specification Type doesn't mention when the Record is being considered, and The List and Record Specification Types doesn't mention neither (did I miss it?) when the class fields are attached to the just created instance.
In practice, differently from any other class public, private, static or accessor that are available at instantiation time, class fields are available "who knows when" during the instantiation process.
I feel like an example could speak thousand words here:
class A {
value = 'a';
constructor() {
console.log(this.value);
}
}
class B extends A {
value = 'b';
}
no constructor dance is even needed to realize that new B
will log "a"
instead of the expected value, defined at B
class level, which is "b"
.
Where's the inconsistency?
These are all preserved developers' intent at class definition time:
class A {
static value = 'a';
static getValue() { return this.value }
shenanigan = 'a';
get value() { return 'a' }
getValue() { return this.value }
constructor() {
console.log(
this.constructor.value,
this.constructor.getValue(),
this.shenanigan,
this.value,
this.getValue()
);
}
}
class B extends A {
static value = 'b';
static getValue() { return this.value }
shenanigan = 'b';
get value() { return 'b' }
getValue() { return this.value }
}
new B;
// "b", "b", "a", "b", "b" !
The private field is some kind of fresh new hell, because the following isn't just ambiguous, it throws an error (rightly so, accordingly with the current upside-down logic, in terms of expectations):
class A {
#value = 'a';
get value() { return this.#value };
constructor() {
console.log(this.value);
}
}
class B extends A {
#value = 'b';
get value() { return this.#value };
}
new B;
// Uncaught TypeError: can't access private field or method:
// object is not the right class
excuse me? ... which part of my definition is not in the right instance object I've just created?
answer the accessor tries to reach A#value
instead of B#value
which is defined in B
prototype
The chicken/egg elephant
I understand that if fields are inherited, like anything else, the reason previous examples don't throw an error, rather show what's attached to the class A {}
at its definition time, but at the same time if a field is defined at the sub-class definition, it's not clear to me why that's the only exception to the "inherited from the prototype / class definition time" rule any other stuff defined in the sub class benefits from.
In short, I believe this is a footgun backed in the current specification, and I am raising as much concerns and awareness I can hoping this is not going to land forever as JS feature, because me, among others, already have been bitten by this counter-intuitive exception to the class definition rules, so please tell me there's hope this stuff will go out in a better shape, thank you