📜 ⬆️ ⬇️

An unexpected order to initialize inherited classes in JavaScript

Today I had a small task of refactoring JS code, and I came across an unexpected feature of the language, about which over 7 years of my programming experience in this hated by many language did not think and did not come across.


In addition, I could not find anything in either the Russian or English Internet, and therefore decided to publish this not very long, not the most interesting, but useful note.


In order not to use the traditional and meaningless foo/bar constants, I will show directly on the example that we had in the project, but still without a bunch of internal logic and with fake values. Remember that all the same, the examples turned out to be quite synthetic.

We step on a rake


So we have a class:


 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 

Everything is logical


And then we needed to create another type of tooltip in which the template field changes


 class SpecialTooltip extends BaseTooltip { template = 'otherTemplate' } 

And here a surprise awaited me, because when creating an object of a new type, the following happens


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

The render method was BaseTooltip.prototype.template with the value BaseTooltip.prototype.template , not SpecialTooltip.prototype.template , as I expected.


We step on the rake carefully, shooting video


Since chrome DevTools do not know how to assign class fields, you have to resort to tricks to understand what is happening. Using a small helper, we log the moment of assignment to a variable


 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 

And when we apply this approach to the inherited class, we get the following strange:


 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 

I was sure that at first the fields of the object are initialized, and then the rest of the constructor is called. It turns out that everything is trickier.


We step on the rake, painting the stalk


We complicate the situation by adding another parameter to the constructor that we assign to our object


 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 

And only this way of debugging (well, not with alerts) made me a little clearer.


Where did this problem come from:

Previously, this code was written on the Marionette framework and looked (conditionally) like this


 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' }) 

When using Marionette, everything worked as I expected, that is, the render method was called with the template value specified in the class, but when copying the module logic to ES6, the problem described in the article got out


Count the bumps


Total:


When creating an object of an inherited class, the order of what is happening is as follows:



We return the rake to the barn


Specifically, in my situation, the problem can be solved either through mixins or passing the template to the constructor, but when the application logic requires overriding a large number of fields, this becomes a rather dirty way.


It would be nice to read your suggestions in the comments on how to elegantly solve the problem.


')

Source: https://habr.com/ru/post/461399/


All Articles