Сегодня у меня была небольшая задачка на рефакторинг 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 // ^ СТРАННО
Метод render вызвался со значением BaseTooltip.prototype.template, а не с SpecialTooltip.prototype.template, как я ожидал.
Наступаем на грабли внимательнее, снимая на видео
Поскольку 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 всё работало так, как я ожидал, то есть метод render вызывался с указанным в классе значением template, но при переписывании логики модуля на ES6 в лоб и вылезла описанная в статье проблема
Считаем шишки
Итог:
При создании объекта наследованного класса порядок происходящего следующий:
- Инициализация полей объекта из объявления наследуемого класса
- Выполнение конструктора наследуемого класса (в том числе инициализация полей внутри конструктора)
- Только после этого инициализация полей объекта из текущего класса
- Выполнение конструктора текущего класса
Возвращаем грабли в сарай
Конкретно в моей ситуации проблему можно решить или через миксины (https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Classes#Mix-ins)
или передавая template в конструктор, но когда логика приложения требует переопределять большое количество полей, это становится довольно грязным способом.
Было бы классно прочитать в комментариях ваши предложения о том, как элегантно решить возникшую проблему
ссылка на оригинал статьи https://habr.com/ru/post/461399/
Добавить комментарий