且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

为什么派生类属性值在基类构造函数中看不到?

更新时间:2022-12-30 21:35:40

不是错误



这不是TypeScript,Babel或您的JS运行时间中的错误。



为什么要这样做



您可能拥有的第一个跟进是为什么不这样做正确!?我们来看一下TypeScript的具体情况。实际的答案取决于我们发行的代码的ECMAScript的版本。



Downlevel emit:ES3 / ES5



我们来看看Type3s为ES3或ES5发出的代码。我已经简化了+注释这一点可读性:
  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(){
//运行基类CTOR
_super();

//衍生类属性INITIALIZERS
this.myColor ='red';

//派生类中的代码ctor body
}
return Derived;
}(Base));

基类emit是无争议的 - 字段被初始化,然后运行构造函数。你肯定不会反对 - 在运行构造函数体之前初始化字段将意味着在构造函数之后直到看不到字段值,这不是什么任何人都想要。



派生类是否正确发送?



不,您应该交换订单



许多人会认为派生类的排序应该如下所示:

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

//运行基类CTOR
_super();

由于任何原因,这是超级错误:




  • 它在ES6中没有相应的行为(见下一节)

  • 'red' for myColor 将立即被基类值'blue'覆盖

  • 派生类字段初始化器可能会调用基类方法这取决于基类初始化。



最后一点,考虑这个代码:

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

如果派生类初始化器在基类初始化器之前运行, Derived#something 永远是 undefined ,当显然应该是'ok'



不,你应该使用时间机器



许多其他人会认为一个模糊的应该这样做,以便 Base 知道派生有一个字段初始化器。



您可以编写依赖于知道要运行的整个代码世界的示例解决方案。但是TypeScript / Babel / etc不能保证这个存在。例如, Base 可以在一个单独的文件中,我们看不到它的实现。



Downlevel emit: ES6



如果您还不知道这一点,现在是时候学习:类不是TypeScript功能。它们是ES6的一部分,并已定义语义。但是ES6类不支持字段初始化器,所以它们被转换为ES6兼容的代码。它看起来像这样:

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

而不是

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

我们应该有吗?

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

否,因为它不工作。在派生类中调用 super 之前,请参考这个是非法的。



ES7 +:公共字段



控制JavaScript的TC39委员会正在调查添加字段初始化程序到语言的未来版本。



您可以在GitHub上阅读阅读有关初始化顺序的具体问题



OOP刷新:构造函数的虚拟行为



所有OOP语言都有一般的指导原则,一些是明确的强制执行的,一些隐含的约定:


不要从构造函数调用虚方法>

示例:





在JavaScript中,我们必须扩展这个规则一点


不要从构造函数观察器虚拟行为



类属性初始化为虚拟




解决方案



标准解决方案是将字段初始化转换为构造函数参数:

  class Base {
myColor:string;
构造函数(颜色:string =blue){
this.myColor = color;
console.log(this.myColor);
}
}

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

//按预期打印红色
const x = new Derived();

您还可以使用 init 虽然您需要谨慎 观察虚拟行为在派生的 init 方法中不执行任何操作这需要基类的完全初始化:

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

//按预期打印红色
const x = new Derived();


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?

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 observer virtual behavior from the constructor

and

Class property initialization is 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();