在JavaScript中初始化继承的类的意外顺序

今天,我有一个重构JS代码的小任务,并且遇到了该语言的意外功能,在此方面我有超过7年的编程经验 被许多人讨厌 语言没有思考,也没有遇到。


此外,我在俄语或英语互联网上都找不到任何内容,因此决定发布此书的时间不长,不是最有趣但有用的注释。


为了不使用传统的,毫无意义的foo/bar常量,我将直接在项目中使用的示例中显示,但仍然没有一堆内部逻辑并且带有假值。 请记住,所有这些示例都是完全综合的。

我们踩耙


所以我们有一个类:


 class BaseTooltip { template = 'baseTemplate' constructor(content) { this.render(content) } render(content) { console.log('render:', content, this.template) } } const tooltip = new BaseTooltip('content') // render: content baseTemplate 

一切都是合乎逻辑的


然后,我们需要创建另一种类型的工具提示,其中的template字段会发生变化


 class SpecialTooltip extends BaseTooltip { template = 'otherTemplate' } 

这让我感到惊讶,因为在创建新类型的对象时,会发生以下情况


 const specialTooltip = new SpecialTooltip('otherContent') // render: otherContent baseTemplate // ^  

正如我所期望的BaseTooltip.prototype.template ,使用值BaseTooltip.prototype.template而不是SpecialTooltip.prototype.template BaseTooltip.prototype.template了render方法。


我们小心地踩着耙子, 拍摄影片


由于chrome DevTools不知道如何分配类字段,因此您必须借助技巧来了解正在发生的事情。 使用一个小助手,我们记录分配给变量的时刻


 function logAndReturn(value) { console.log(`set property=${value}`) return value } class BaseTooltip { template = logAndReturn('baseTemplate') constructor(content) { console.log(`call constructor with property=${this.template}`) this.render(content) } render(content) { console.log(content, this.template) } } const tooltip = new BaseTooltip('content') // set property=baseTemplate // called constructor BaseTooltip with property=baseTemplate // render: content baseTemplate 

当我们将这种方法应用于继承的类时,会遇到以下奇怪的情况:


 class SpecialTooltip extends BaseTooltip { template = logAndReturn('otherTemplate') } const tooltip = new SpecialTooltip('content') // set property=baseTemplate // called constructor SpecialTooltip with property=baseTemplate // render: content baseTemplate // set property=otherTemplate 

我确定对象的字段首先被初始化,然后构造函数的其余部分被调用。 事实证明,一切都比较棘手。


我们踩着耙子,给茎上油漆


我们通过向构造函数添加另一个参数使情况复杂化,该参数分配给我们的对象


 class BaseTooltip { template = logAndReturn('baseTemplate') constructor(content, options) { this.options = logAndReturn(options) // <---   console.log(`called constructor ${this.constructor.name} with property=${this.template}`) this.render(content) } render(content) { console.log(content, this.template, this.options) // <---   } } class SpecialTooltip extends BaseTooltip { template = logAndReturn('otherTemplate') } const tooltip = new SpecialTooltip('content', 'someOptions') //    : // set property=baseTemplate // set property=someOptions // called constructor SpecialTooltip with property=baseTemplate // render: content baseTemplate someOptions // set property=otherTemplate 

而且只有这种调试方式(嗯,不是警报)可以澄清一点


这个问题是从哪里来的:

以前,此代码是在Marionette框架上编写的,并且(有条件地)看起来像这样


 const BaseTooltip = Marionette.Object.extend({ template: 'baseTemplate', initialize(content) { this.render(content) }, render(content) { console.log(content, this.template) }, }) const SpecialTooltip = BaseTooltip.extend({ template: 'otherTemplate' }) 

使用Marionette时,一切工作都与我预期的一样,即使用类中指定的template值调用render方法,但是将模块逻辑复制到ES6时,本文中描述的问题就解决了


计算颠簸


结果:


创建继承类的对象时,发生的顺序如下:


  • 从继承的类声明中初始化对象字段
  • 继承类的构造函数的执行(包括构造函数内部字段的初始化)
  • 仅在初始化当前类中的对象的字段之后
  • 执行当前类的构造函数

我们把耙子还给谷仓


具体来说,在我的情况下, 可以通过mixins或将模板传递给构造函数来解决问题,但是当应用程序逻辑需要覆盖大量字段时,这将成为一种相当肮脏的方式。


最好在评论中阅读您对如何优雅地解决问题的建议。

Source: https://habr.com/ru/post/zh-CN461399/


All Articles