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

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

我写了一些代码:

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

我期望派生的 class 字段初始值设定项在基础 class 构造函数之前变为 运行。 相反,派生的 class 不会更改 myColor 属性 直到基 class 构造函数 运行s 之后,所以我观察到构造函数中的错误值.

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

不是错误

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

为什么必须这样

您的第一个跟进可能是 "Why not do this correctly!?!?"。让我们检查一下 TypeScript emit 的具体情况。实际答案取决于我们为哪个版本的 ECMAScript 发出 class 代码。

下层发射: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));

基础 class emit 毫无争议地正确 - 字段被初始化,然后是构造函数主体 运行s。您当然不希望相反 - 在之前运行初始化构造函数主体意味着您无法看到字段值,直到之后构造函数,这不是任何人想要的。

导出的 class 是否正确?

不行,你应该调换顺序

很多人会争辩说派生的 class emit 应该是这样的:

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

    // RUN THE BASE CLASS CTOR
    _super();

出于多种原因,这是非常错误的:

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

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

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

如果派生 class 初始化器 运行 在基础 class 初始化器之前,Derived#something 将始终是 undefined,而显然它应该是 'ok'.

不,你应该使用时光机

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

您可以编写示例解决方案,这些解决方案取决于了解整个代码领域 运行。但是 TypeScript / Babel / etc 不能 gua运行tee 这存在。例如,Base 可以在我们看不到其实现的单独文件中。

下层发出:ES6

如果您还不知道这一点,是时候学习了:classes 不是 TypeScript 的特性。它们是 ES6 的一部分并且定义了语义。但是 ES6 classes 不支持字段初始值设定项,因此它们被 t运行sformed 为 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);

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

ES7+:Public 字段

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

你可以read about it on GitHub or read the specific issue about initialization order.

OOP 复习:来自构造函数的虚拟行为

所有 OOP 语言都有一个通用准则,有些是明确强制执行的,有些是按照约定隐含的:

Do not call virtual methods from the constructor

示例:

  • C#Virtual member call in a constructor
  • C++ Calling virtual functions inside constructors
  • Python Calling member functions from a constructor
  • Java Is it OK to call abstract method from constructor in Java?

在Java脚本中,我们必须稍微扩展一下这个规则

Do not observe virtual behavior from the constructor

Class property initialization counts as virtual

解决方案

标准的解决方案是t运行sform字段初始化为构造函数参数:

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:

的事情
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();

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

通过做一件意想不到的事情,这是破坏常见 class 扩展用例的不良行为。这是支持您的用例的初始化顺序,我认为更好:

Base property initializers
Derived property initializers
Base constructor
Derived constructor

问题/解决方案

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

这里的解决方案是将 属性 初始化与构造函数的调用分开。 C# 会这样做,尽管它在 派生属性之后 初始化基本属性,这也是违反直觉的。这可以通过发出助手 classes 来实现,这样派生的 class 可以以任意顺序初始化基础 class。

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 属性而中断吗?

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

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