Home > Net >  Javascript class composition : access attribute of secondary class in attribute of main class
Javascript class composition : access attribute of secondary class in attribute of main class

Time:02-02

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);

  • Related