Let's take this exemple :
class A {
attrA = 3;
}
class B {
constructor() {
this.a = new A;
}
attrB = this.a.attrA;
methB() { console.log(this.a.attrA);}
}
const test = new B;
console.log(test.attrB);
test.methB();
I can access the class A attribute through method of class B, but I can't use it with attribute of class B, I have an "undefined" error.
The only way to do this is :
class A {
attrA = 3;
}
class B {
constructor() {
this.a = new A;
this.z = this.a.attrA
}
methB() { console.log(this.a.attrA);}
}
const test = new B;
console.log(test.z);
test.methB();
Why I need to put the attribute in the constructor and not the method?
Thanks!
CodePudding user response:
You can think of class fields as being hoisted, meaning this:
class Example {
constructor() {/*...*/}
property = "value";
}
Is "actually" this:
class Example {
property = "value";
constructor() {/*...*/}
}
Or (similar to) this:
class Example {
constructor() {
this.property = "value";
/*...*/
}
}
Further, identifiers are resolved upon access. So by the time you execute test.methB()
, test.a
has been initialized, allowing the method to correctly resolve this.a.attrA
. It works the same as this code:
let variable = null;
const logVariable = () => console.log(variable); // You may think this logs null, but ...
variable = "value";
logVariable(); // ... it actually logs `variable`'s value at the time of calling.
As you have observed, mixing property initializations from within the constructor and using field initializers may be confusing. Therefore, a more intuitive way to write your code would be:
class A {
attrA = 3;
}
class B {
a = new A;
attrB = this.a.attrA;
constructor() {}
methB() {
console.log(this.a.attrA);
}
}
const test = new B;
console.log(test.attrB); // Works now!
test.methB();
Personal recommendations:
- Declare all instance and class fields.
- Prefer field initializers.
- Only reassign/initialize fields in the constructor for non-default values.
You may want to read on for more details for a better technical understanding.
Class syntax
Classes are just uncallable constructor functions:
class Example {}
console.log(typeof Example); // "function"
Example(); // Error: cannot be invoked without `new`
This is a quirk of the ES6 class syntax. Technically, class constructors are still "callable", but only internally (which happens when using new
). Otherwise constructors would serve no purpose.
Another aspect of the class syntax is the ability to call super()
in the constructor. This only comes into play when a class inherits, but that comes with yet its own quirks:
You cannot use this
before calling super
:
class A {};
class B extends A {
constructor() {
this; // Error: must call super before accessing this
super();
}
}
new B();
Reason being, before calling super
no object has been created, despite having used new
and being in the constructor.
The actual object creation happens at the base class, which is in the most nested call to super
. Only after calling super
has an object been created, allowing the use of this
in the respective constructor.
Class fields
With the addition of instance fields in ES13, constructors became even more complicated: Initialization of those fields happens immediately after the object creation or the call to super
, meaning before the statements that follow.
class A /*no heritage*/ {
property = "a";
constructor() {
// Initializing instance fields
// ... (your code)
}
}
class B extends A {
property = "b";
constructor() {
super();
// Initializing instance fields
// ... (your code)
}
}
Further, those property "assignments" are actually no assignments but definitions:
class Setter {
set property(value) {
console.log("Value:", value);
}
}
class A extends Setter {
property = "from A";
}
class B extends Setter {
constructor() {
super();
// Works effectively like class A:
Object.defineProperty(this, "property", { value: "from B" });
}
}
class C extends Setter {
constructor() {
super();
this.property = "from C";
}
}
new A(); // No output, expected: "Value: from A"
new B(); // No output, expected: "Value: from B"
new C(); // Output: "Value: from C"
Variables
Identifiers are only resolved upon access, allowing this unintuitive code:
const logNumber = () => console.log(number); // References `number` before its declaration, but ...
const number = 0;
logNumber(); // ... it is resolved here, making it valid.
Also, identifiers are looked up from the nearest to the farthest lexical environment. This allows variable shadowing:
const variable = "initial-value";
{
const variable = "other-value"; // Does NOT reassign, but *shadows* outer `variable`.
console.log(variable);
}
console.log(variable);