Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Переопределение методов JS: Погружаясь в классы

Устранение дублирования кода — не единственная задача наследования классов. Иногда оно применяется для изменения существующего поведения базового класса.

Тег <select> в браузере представлен классом HTMLSelectElement. У него есть дополнительные методы, которые нужны для работы со списком элементов. Один из таких методов: item(index). С его помощью можно извлекать конкретный вариант из списка.

<select name="variants">
  <option>Opt 1</option>
  <option>Opt 2</option>
  <option>Opt 3</option>
</select>
// Гипотетический код, который возвращает элемент выше в виде объекта класса HTMLSelectElement
const element = document.querySelector('select');

element.item(0); // HTMLOptionElement(textContent="Opt 1")
element.item(1); // HTMLOptionElement(textContent="Opt 2")

Представим себе, что нам нужно часто обращаться к элементам этого списка с конца. Для этого постоянно придется выполнять подобный код:

// Свойство length описывает число option элементов внутри select
element.item(element.length - 1);

В этом коде нет ничего криминального, но можно лучше. Один из возможных вариантов решения этой задачи состоит в том, чтобы расширить поведение метода и научить его работать с отрицательными числами. Возможность обращаться к индексам в обратном порядке — распространенная практика во многих языках.

// Последний элемент
element.item(-1); // HTMLOptionElement(textContent="Opt 3")
// Третий с конца
element.item(-3); // HTMLOptionElement(textContent="Opt 1")

Как это сделать? Наследование дает возможность переопределять методы суперклассов. Посмотрите на пример:

class HTMLCustomSelectElement extends HTMLSelectElement {
  item(possibleIndex) {
    const realIndex = possibleIndex >= 0 ? possibleIndex : this.length + possibleIndex;
    // super указывает на родительский класс
    return super.item(realIndex);
  }
}

Выше создан подкласс HTMLCustomSelectElement, который переопределяет метод item(index). Переопределение означает, что в подклассе создается метод с тем же именем, что и в родительском классе. Наш новый метод выполняет дополнительную работу по вычислению индекса, но ему все еще нужен исходный метод item(index), для выборки нужного элемента. Для этого применяется специальный синтаксис, который указывает явно что нужно взять метод из родительского класса: super.item(realIndex).

Почему понадобился специальный синтаксис? Представьте, что вместо него там был бы такой код:

item(possibleIndex) {
  this.item(possibleIndex);
}

Какой в этом случае метод item() нужно брать — в определении которого мы находимся прямо сейчас или родительский? Наследование так устроено, что всегда выбирается тот метод, который находится ближе в цепочке наследования. Поэтому вызов через this породит рекурсию, но родительский метод никогда не будет вызван.

По этой же причине, снаружи объекта невозможно вызвать методы родительского класса, которые были переопределены в наследниках:

const select = new HTMLCustomSelectElement();

// Этот вызов всегда относится к методу item, переопределенному внутри HTMLCustomSelectElement
// Вызвать item напрямую из HTMLSelectElement невозможно
select.item(3);

Переопределение не ограничивается одним уровнем наследования. Любой переопределенный метод можно снова переопределить в наследниках текущего класса.

Немного по-другому работает вызов конструктора родительского класса. Для этого достаточно использовать super как функцию:

// Родительский класс
class HTMLElement {
  constructor(attributes = {}) {
    this.attributes = attributes;
  }

  // ...
}

// Дочерний класс
class HTMLSelectElement extends HTMLElement {
  constructor(attributes) {
    super(attributes);
    this.tagName = 'SELECT';
  }
}

const selectElement = new HTMLSelectElement({ class: 'form-select' });
console.log(selectElement.attributes); // => { class: 'form-select' }
console.log(selectElement.tagName); // => 'SELECT'

В примере выше super(attributes) вызывает конструктор родительского класса, он принимает attributes. Кроме вызова super() в конструкторе дочернего класса, можно выполнить оставшуюся работу для этого класса. В примере выше присваивается имя тэга tagName. Благодаря этому в конструкторе дочернего класса выполняются оба конструктора: родительского класса и дочернего.

Использование наследников

Создать класс-наследник и начать его использовать — это две большие разницы. В ситуациях, где эти классы создаются вами, все просто — достаточно заменить вызовы старого класса на новый, но если объекты этого класса создаются чужим кодом, то задача усложняется. Для подмены такого класса, от чужого кода требуется поддержка полиморфного поведения.

Например, при работе с элементами HTML, объекты этих классов иногда порождаются самим программистом, а иногда системой. Например:

// Создаем сами
const element1 = new HTMLSelectElement();

// Где-то внутри создается объект HTMLSelectElement
const element2 = document.querySelector('select');

Можно ли подменить класс в примере с querySelector()? Зависит от реализации библиотеки по работе с HTML-элементами. В тех библиотеках, что нам известны, это сделать невозможно. Это значит, что единственный выход использовать собственный класс — это конвертировать вернувшийся объект в объект нужного нам класса. Стоит ли оно того? Почти наверняка, что нет.

const element = document.querySelector('select');
const convertedElement = new MyHTMLSelectElement(element);

Другими словами, наследование для переопределения поведения хоть и кажется логичным шагом, но в действительности имеет серьезные ограничения по использованию.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 21 ноября
профессия
от 39 525 ₸ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 21 ноября
профессия
от 25 000 ₸ в месяц
Разработка бэкенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 21 ноября

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»