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

Rya*_*ugh 19 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();
Run Code Online (Sandbox Code Playgroud)

我期望我的派生类字段初始化程序在基类构造函数之前运行.相反,派生类myColor在基类构造函数运行之前不会更改属性,因此我在构造函数中观察到错误的值.

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

Rya*_*ugh 30

不是一个Bug

首先,这不是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));
Run Code Online (Sandbox Code Playgroud)

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

派生类是否正确发出?

不,你应该交换订单

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

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

    // RUN THE BASE CLASS CTOR
    _super();
Run Code Online (Sandbox Code Playgroud)

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

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

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

class Base {
    thing = 'ok';
    getThing() { return this.thing; }
}
class Derived extends Base {
    something = this.getThing();
}
Run Code Online (Sandbox Code Playgroud)

如果派生类初始值设定项在基类初始值设定项之前运行,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';
    }
}
Run Code Online (Sandbox Code Playgroud)

代替

    super(...arguments);
    this.myColor = 'red';
Run Code Online (Sandbox Code Playgroud)

我们应该这样吗?

    this.myColor = 'red';
    super(...arguments);
Run Code Online (Sandbox Code Playgroud)

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

ES7 +:公共领域

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

您可以在GitHub上阅读它阅读有关初始化顺序的特定问题.

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();
Run Code Online (Sandbox Code Playgroud)

您也可以使用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();
Run Code Online (Sandbox Code Playgroud)

  • 如果“人们非常困惑”,则意味着语言语法非常令人困惑……虽然这在向后兼容 ES6 类方面是有道理的,但从开发人员的角度来看却没有意义。在技​​术上正确和有用是不同的事情。 (4认同)
  • 我会说,这是解释这么简单事情的非常冗长的方式.这只是'超级总是先行'."ES7"一词已经过时,现在是ES.next.考虑到这是无偿的自我回答问题,原始问题中的例子并不十分雄辩.常规问题可能会被低估,因为它无法得到一个建设性的答案,片段缺乏上下文,并且不清楚为什么OP会做他/她所做的事情. (3认同)
  • 我写这篇是因为人们在TypeScript GitHub问题跟踪器https://github.com/Microsoft/TypeScript/issues/1617上无休止地对此感到困惑,并且拒绝接受一个简单的解释(我的"超级先行"评论坐着7"拇指向下"的反应) (2认同)

fro*_*975 6

我会恭敬地辩称这实际上是一个错误

通过执行意想不到的操作,这是破坏常见类扩展用例的不良行为。这是支持您的用例的初始化顺序,我认为更好:

Base property initializers
Derived property initializers
Base constructor
Derived constructor
Run Code Online (Sandbox Code Playgroud)

问题/解决方案

- 打字稿编译器当前在构造函数中发出属性初始化

这里的解决方案是将属性初始化与构造函数的调用分开。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();
Run Code Online (Sandbox Code Playgroud)

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

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

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();
Run Code Online (Sandbox Code Playgroud)