Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
180 views
in Technique[技术] by (71.8m points)

javascript - Why are derived class property values not seen in the base class constructor?

I wrote some code:

class Base {
    // Default value
    myColor = 'blue';

    constructor() {
        console.log(this.myColor);
    }
}

class Derived extends Base {
     myColor = 'red'; 
}

// Prints "blue", expected "red"
const x = new Derived();

I was expecting my derived class field initializer to run before the base class constructor. Instead, the derived class doesn't change the myColor property until after the base class constructor runs, so I observe the wrong values in the constructor.

Is this a bug? What's wrong? Why does this happen? What should I do instead?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Not a Bug

First up, this is not a bug in TypeScript, Babel, or your JS runtime.

Why It Has To Be This Way

The first follow-up you might have is "Why not do this correctly!?!?". Let's examine the specific case of TypeScript emit. The actual answer depends on what version of ECMAScript we're emitting class code for.

Downlevel emit: ES3/ES5

Let's examine the code emitted by TypeScript for ES3 or ES5. I've simplified + annotated this a bit for readability:

var Base = (function () {
    function Base() {
        // BASE CLASS PROPERTY INITIALIZERS
        this.myColor = 'blue';
        console.log(this.myColor);
    }
    return Base;
}());

var Derived = (function (_super) {
    __extends(Derived, _super);
    function Derived() {
        // RUN THE BASE CLASS CTOR
        _super();

        // DERIVED CLASS PROPERTY INITIALIZERS
        this.myColor = 'red';

        // Code in the derived class ctor body would appear here
    }
    return Derived;
}(Base));

The base class emit is uncontroversially correct - the fields are initialized, then the constructor body runs. You certainly wouldn't want the opposite - initializing the fields before running the constructor body would mean you couldn't see the field values until after the constructor, which is not what anyone wants.

Is the derived class emit correct?

No, you should swap the order

Many people would argue that the derived class emit should look like this:

    // DERIVED CLASS PROPERTY INITIALIZERS
    this.myColor = 'red';

    // RUN THE BASE CLASS CTOR
    _super();

This is super wrong for any number of reasons:

  • It has no corresponding behavior in ES6 (see next section)
  • The value 'red' for myColor will be immediately overwritten by the base class value 'blue'
  • The derived class field initializer might invoke base class methods which depend on base class initializations.

On that last point, consider this code:

class Base {
    thing = 'ok';
    getThing() { return this.thing; }
}
class Derived extends Base {
    something = this.getThing();
}

If the derived class initializers ran before the base class initializers, Derived#something would always be undefined, when clearly it should be 'ok'.

No, you should use a time machine

Many other people would argue that a nebulous something else should be done so that Base knows that Derived has a field initializer.

You can write example solutions that depend on knowing the entire universe of code to be run. But TypeScript / Babel / etc cannot guarantee that this exists. For example, Base can be in a separate file where we can't see its implementation.

Downlevel emit: ES6

If you didn't already know this, it's time to learn: classes are not a TypeScript feature. They're part of ES6 and have defined semantics. But ES6 classes don't support field initializers, so they get transformed to ES6-compatible code. It looks like this:

class Base {
    constructor() {
        // Default value
        this.myColor = 'blue';
        console.log(this.myColor);
    }
}
class Derived extends Base {
    constructor() {
        super(...arguments);
        this.myColor = 'red';
    }
}

Instead of

    super(...arguments);
    this.myColor = 'red';

Should we have this?

    this.myColor = 'red';
    super(...arguments);

No, because it doesn't work. It's illegal to refer to this before invoking super in a derived class. It simply cannot work this way.

ES7+: Public Fields

The TC39 committee that controls JavaScript is investigating adding field initializers to a future version of the language.

You can read about it on GitHub or read the specific issue about initialization order.

OOP refresher: Virtual Behavior from Constructors

All OOP languages have a general guideline, some enforced explicitly, some implicitly by convention:

Do not call virtual methods from the constructor

Examples:

In JavaScript, we have to expand this rule a little

Do not observe virtual behavior from the constructor

and

Class property initialization counts as virtual

Solutions

The standard solution is to transform the field initialization to a constructor parameter:

class Base {
    myColor: string;
    constructor(color: string = "blue") {
        this.myColor = color;
        console.log(this.myColor);
    }
}

class Derived extends Base {
    constructor() {
        super("red");
     }
}

// Prints "red" as expected
const x = new Derived();

You can also use an init pattern, though you need to be cautious to not observe virtual behavior from it and to not do things in the derived init method that require a complete initialization of the base class:

class Base {
    myColor: string;
    constructor() {
        this.init();
        console.log(this.myColor);
    }
    init() {
        this.myColor = "blue";
    }
}

class Derived extends Base {
    init() {
        super.init();
        this.myColor = "red";
    }
}

// Prints "red" as expected
const x = new Derived();

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...