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

时间:2017-04-24 19:08:31

标签: javascript class typescript ecmascript-6

我写了一些代码:

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

我希望我的派生类字段初始值设定项在基类构造函数之前运行。 相反,派生类在基类构造函数运行之前不会更改myColor属性,因此我在构造函数中观察到错误的值。

这是一个错误吗?怎么了?为什么会这样?我该怎么做呢?

2 个答案:

答案 0 :(得分:28)

不是错误

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

为什么必须这样

您可能拥有的第一个后续行动是"为什么不正确这样做!?!?"。我们来看看TypeScript发射的具体情况。实际答案取决于我们为哪些版本的ECMAScript发布类代码。

向下发射:ES3 / ES5

让我们检查一下TypeScript为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() {
        // 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));

基类的发射是无可争议的 - 字段被初始化,然后构造函数体运行。你当然不会想要相反 - 在运行构造函数体之前初始化字段意味着你无法在之后构造函数之前看到字段值,这不是任何人想要的。

派生类是否正确发出?

不,你应该交换订单

许多人会争辩派生类的发射应该如下所示:

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

    // RUN THE BASE CLASS CTOR
    _super();

由于各种原因,这是超级错误:

  • 在ES6中没有相应的行为(参见下一节)
  • 'red'的值myColor将立即被基类值覆盖'蓝色'
  • 派生类字段初始值设定项可能会调用依赖于基类初始化的基类方法。

关于最后一点,请考虑以下代码:

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

如果派生类初始值设定项在基类初始值设定项之前运行,Derived#something始终为undefined,则显然应为'ok'

不,你应该使用时间机器

许多其他人会争辩说,应该做一个模糊的其他,以便Base知道Derived有一个字段初始值设定项。

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

向下发射:ES6

如果您还不知道这一点,那么现在是时候学习:类不是TypeScript功能。他们是ES6的一部分,并定义了语义。但ES6类不支持字段初始化程序,因此它们转换为与ES6兼容的代码。它看起来像这样:

class Base {
    constructor() {
        // Default value
        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);

不,因为它不起作用。在派生类中调用this之前引用super是非法的。它根本无法以这种方式工作。

ES7 +:Public Fields

控制JavaScript的TC39委员会正在研究将字段初始化程序添加到该语言的未来版本中。

您可以read about it on GitHubread the specific issue about initialization order

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

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

  

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

示例:

在JavaScript中,我们必须稍微扩展此规则

  

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

  

类属性初始化计为虚拟

解决方案

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

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

您也可以使用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";
    }
}

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

答案 1 :(得分:-2)

我会恭敬地说,这实际上是一个错误

通过执行意外的操作,这是不受欢迎的行为,破坏了常见的类扩展用例。这是可以支持您的用例的初始化顺序,我认为这是更好的选择:

Base property initializers
Derived property initializers
Base constructor
Derived constructor

问题/解决方案

-类型脚本编译器当前在构造函数中发出属性初始化

这里的解决方案是将属性初始化与构造函数的调用分开。尽管C#会在之后派生的属性中设置基本属性,但这还是违反直觉的。这可以通过发出辅助类来实现,以便派生类可以按任意顺序初始化基类。

class _Base {
    ctor() {
        console.log('base ctor color: ', this.myColor);
    }

    initProps() {
        this.myColor = 'blue';
    }
}
class _Derived extends _Base {
    constructor() {
        super();
    }

    ctor() {
        super.ctor();
        console.log('derived ctor color: ', this.myColor);
    }

    initProps() {
        super.initProps();
        this.myColor = 'red';
    }
}

class Base {
    constructor() {
        const _class = new _Base();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}
class Derived {
    constructor() {
        const _class = new _Derived();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}

// Prints:
// "base ctor color: red"
// "derived ctor color: red"
const d = new Derived();

-基本构造函数不会因为我们使用派生类属性而中断吗?

任何在基本构造函数中发生中断的逻辑都可以移至在派生类中将被覆盖的方法。由于派生方法是在调用基本构造函数之前初始化的,因此可以正常工作。示例:

class Base {
    protected numThings = 5;

    constructor() {
        console.log('math result: ', this.doMath())
    }

    protected doMath() {
        return 10/this.numThings;
    }
}

class Derived extends Base {
    // Overrides. Would cause divide by 0 in base if we weren't overriding doMath
    protected numThings = 0;

    protected doMath() {
        return 100 + this.numThings;
    }
}

// Should print "math result: 100"
const x = new Derived();