Home > Software engineering >  Why declare a same property both inside and outside of constructor?
Why declare a same property both inside and outside of constructor?

Time:06-25

From Classes on mdn web docs:

Public field declarations

class Rectangle {
  height = 0;
  width;
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

Aren't the height and width outside of the constructor meaningless since they're always overwritten in the constructor?

What's the significance of the height and width declaration/initialization outside of constructor in this case?
Are they used/reachable by any means?

CodePudding user response:

The properties are only declared once (outside the constructor); if you didn't have those declarations, there'd be no declarations for those properties (they'd be created by assignment instead). Public property declarations (aka "public class fields") are optional, but can help avoid shape changes in objects (which improves efficiency) and/or reduce the analysis JavaScript engines need to do on the object being created to avoid shape changes. Basically, they provide a declarative (rather than imperative [step-by-step]) means of saying (in this case) that Rectangle objects always have a height and width property. While the JavaScript engine can figure that out using analysis of the constructor code, the declarations mean it doesn't have to worry about that (for those properties).

Aren't the height and width outside of the constructor meaningless since they're always overwritten in the constructor?

The initializer (= 0) on the height property is pointless, yes, since it gets unambiguously overwritten by the code in the constructor and the assignment doing so is the first statement in the constructor. If it weren't the first statement, then code in the constructor would be able to observe the 0 in height prior to the assignment.

Are [the height and width declaration/initialization outside of constructor] used/reachable by any means?

They could be used by subsequent class field initializers. For instance, this is valid:

class Example {
    a = 2;
    b = this.a * 3;
}
console.log(new Example().b); // 6

It's probably also worth pointing out that there's a difference if the class is a subclass: When you use the declaration syntax, the property is created via "define" semantics (as though you used the Object.defineProperty function), whereas if you just assign to it you're using assignment semantics. That matters if the superclass also defined the property:

class Super {
    #example = 42;

    get example() {
        console.log("Getting example");
        return this.#example;
    }
    set example(value) {
        console.log("Setting example");
        this.#example = value;
    }
}

class Sub extends Super {
    example = 67;
}

const super1 = new Super();
console.log(`What kind of property is super1.example?`);
console.log(propertyKind(super1, "example"));

const sub1 = new Sub();
console.log(`What kind of property is sub1.example?`);
console.log(propertyKind(sub1, "example"));

function propertyKind(obj, name) {
    let descr;
    do {
        descr = Object.getOwnPropertyDescriptor(obj, name);
        if (descr) {
            break;
        }
        obj = Object.getPrototypeOf(obj);
        if (!obj) {
            break;
        }
    } while (!descr);

    return descr
        ? "value" in descr
            ? "data"
            : "accessor" // presumably
        : "none";
}

Notice that example is an accessor property on super1 (via inheritance from its prototype), but it's a data property on sub1 because it was redeclared by Sub. If Sub had just assigned to it, it would have just used the property Super created:

class Super {
    #example = 42;

    get example() {
        console.log("Getting example");
        return this.#example;
    }
    set example(value) {
        console.log("Setting example");
        this.#example = value;
    }
}

class Sub extends Super {
    constructor() {
        super();
        this.example = 67;
    }
}

const super1 = new Super();
console.log(`What kind of property is super1.example?`);
console.log(propertyKind(super1, "example"));

const sub1 = new Sub();
console.log(`What kind of property is sub1.example?`);
console.log(propertyKind(sub1, "example"));

function propertyKind(obj, name) {
    let descr;
    do {
        descr = Object.getOwnPropertyDescriptor(obj, name);
        if (descr) {
            break;
        }
        obj = Object.getPrototypeOf(obj);
        if (!obj) {
            break;
        }
    } while (!descr);

    return descr
        ? "value" in descr
            ? "data"
            : "accessor" // presumably
        : "none";
}

Notice that now, it's an accessor property on both sub1 and super1 (and that when Sub did its assignment, the setter code in Super.example ran).


TL;DR - I would keep the declarations for clarity and potential efficiency, but remove the = 0 initializer on height, since that value is clearly never meant to be used.

  • Related