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.
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.
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 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.
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
Total:
When creating an object of an inherited class, the order of what is happening is as follows:
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