Класс в JavaScript: базовый синтаксис и примеры. Часть вторая

Класс в JavaScript: базовый синтаксис и примеры. Часть вторая

В этой статье вы узнаете что такое mixin, как использовать статические и приватные поля, когда нужно использовать Species, а также как обращаться к родительскому классу с помощью super.

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

* Если вы еще не сталкивались с классами, то советуем вам сначала прочитать эту статью.

Что такое миксины

Миксин (или примесь от англ. mixin)  — это способ комбинировать функциональность разных классов или объектов без использования наследования. То есть это функция, которая принимает в качестве аргумента класс или объект и возвращает новый класс или объект с расширенной функциональностью. Миксин может копировать свойства и методы из других классов или объектов или добавлять новые, а также позволяет создавать гибкие и модульные решения, избегая проблем множественного наследования, например:

// Определяем mixin для добавления метода fly
function flyMixin(target) {
  // Копируем метод fly в целевой класс или объект
  target.prototype.fly = function () {
    console.log(this.name + " летит");
  };
  // Возвращаем целевой класс или объект
  return target;
}

// Определяем mixin для добавления метода swim
function swimMixin(target) {
  // Копируем метод swim в целевой класс или объект
  target.prototype.swim = function () {
    console.log(this.name + " плывет");
  };
  // Возвращаем целевой класс или объект
  return target;
}

// Определяем базовый класс Bird
class Bird {
  constructor(name) {
    this.name = name;
  }
  sing() {
    console.log(this.name + " поет");
  }
}

// Определяем производный класс Duck с помощью mixin
class Duck extends swimMixin(flyMixin(Bird)) {
  constructor(name) {
    super(name);
  }
  quack() {
    console.log(this.name + " крякает");
  }
}

let duck1 = new Duck("Donald");
duck1.sing(); // Donald поет
duck1.quack(); // Donald крякает
duck1.fly(); // Donald летит
duck1.swim(); // Donald плывет

В этом примере мы определили два mixin: FlyMixin и SwimMixin, которые добавляют методы fly (летать) и swim (плавать) соответственно. Мы также определили базовый класс Bird, который имеет одно свойство name (имя) и один метод sing (петь). Мы также определили производный класс Duck, который наследует класс Bird и добавляет свой метод quack (крякать). Мы использовали mixin для расширения класса Duck методами fly и swim. Мы создали объект duck1 на основе класса Duck и вызвали его методы.

Где вам понадобятся mixins

  • Повторное использование кода: это достигается путем выноса общих функций и свойств в отдельный миксин, который можно будет переиспользовать в разных классах.
  • Расширение функциональности: миксины позволяют добавлять новые методы и свойства в классы, не изменяя их основной структуры. Это может быть полезно, когда нужно внести небольшие изменения в поведение класса без необходимости создавать новый класс с нуля.
  • Сочетание различных модулей: примеси позволяют комбинировать несколько различных модулей или функциональностей в одном классе. Это называется множественным наследованием через миксины и позволяет создавать классы с множеством разных возможностей.
  • Создание плагинов или расширений: миксины часто используются для создания плагинов или расширений, которые могут быть подключены к основному приложению или фреймворку для добавления новых функций или поведения.

Поля класса

Определение полей — способ определения свойств класса без использования конструктора или ключевого слова this. Поля класса определяются с помощью знака равенства после имени поля. Поля класса доступны для объекта или класса через точку. Поля класса могут быть статическими, публичными или приватными:

  • Публичные поля — поля, которые принадлежат конкретному объекту, созданному на основе класса. Они могут быть определены в конструкторе класса или вне его с помощью ключевого слова this. Поля экземпляра доступны для объекта через точку.
class User {
  name; // Публичное поле экземпляра
  
  constructor(name) {
    this.name = name; // Присваиваем значение публичному полю
  }
}
  • Приватные поля — поля, которые доступны только внутри класса. Приватные поля определяются с помощью символа # перед именем поля. Приватные поля могут быть нестатическими или статическими.
class User {
  #age; // Приватное поле экземпляра

  constructor(age) {
    this.#age = age; // Присваиваем значение приватному полю
  }
}
  • Статические поля — поля, которые принадлежат самому классу, а не его объектам. Статические поля определяются с помощью ключевого слова static перед именем поля. Статические поля вызываются на имени класса через точку. Статические поля не могут обращаться к свойствам объекта через ключевое слово this.
class User {
  name;
  #age;
  static count = 0; // Статическое публичное поле

  constructor(name, age) {
    this.name = name;
    this.#age = age;
    User.count++; // Увеличиваем значение статического поля
  }
}

Привязка контекста this в прототипных и статических методах

Привязка this в прототипных и статических методах означает, что ключевое слово this внутри методов класса ссылается на текущий объект, на котором вызван метод. Но, если метод передается как аргумент другой функции или сохраняется в переменную, то значение this может потеряться или измениться. Для решения этой проблемы можно использовать один из следующих способов:

  • Использовать стрелочную функцию вместо обычной функции для определения метода. Стрелочная функция не имеет собственного this и берет его из внешнего контекста.
  • Использовать метод bind для привязки значения this к методу. Метод bind возвращает новую функцию, которая всегда будет вызываться с заданным значением this.
  • Использовать метод call или apply для вызова метода с заданным значением this. Методы call и apply позволяют вызвать функцию с указанным контекстом и аргументами.

Все три способа помогают контролировать значение this в методах класса и предотвращать его потерю или нежелательное изменение.

Species

Symbol.species — это специальное свойство класса, которое определяет конструктор для создания производных объектов. Species используется, когда объект класса передается в некоторые встроенные методы, такие как map, filter и slice, которые возвращают новый объект того же типа. Также оно позволяет указать, какой конструктор использовать для создания нового объекта, например:

class MyArray extends Array {
  // Переопределяем species для возврата родительского конструктора
  static get [Symbol.species]() {
    return Array;
  }
}

let myArray1 = new MyArray(1, 2, 3);
let myArray2 = myArray1.map(x => x * 2);

console.log(myArray1 instanceof MyArray); // true
console.log(myArray2 instanceof MyArray); // false
console.log(myArray2 instanceof Array); // true

В этом примере мы определили класс MyArray, который наследует класс Array. Потом переопределили свойство species для возврата родительского конструктора Array. Далее создали объект myArray1 на основе класса MyArray и применили к нему метод map, который возвращает новый массив с удвоенными значениями. Отметьте — новый массив myArray2 не является экземпляром класса MyArray, а является экземпляром класса Array. Это произошло потому, что мы указали species как Array.

В каких случаях свойство Symbol.species будет полезно

  • При поддержке наследования: если вы создаете пользовательский класс, производный от встроенного класса, такого как Array, вы можете использовать Symbol.species, чтобы указать, что при создании новых экземпляров должен использоваться ваш собственный класс. Это позволяет легко создавать подклассы с тем же поведением, что и родительский класс, но с добавленной функциональностью или измененным поведением.
  • Если вы используете методы массива map(), filter() или slice(): Symbol.species позволяет указать, какой конструктор должен использоваться для создания нового массива. Например, если вы имеете подкласс массива с дополнительными методами, вы можете использовать species, чтобы гарантировать, что результат выполненных операций также будет экземпляром вашего подкласса.
  • При клонировании объектов: если вы создаете собственный класс и хотите определить, какие конструкторы должны быть использованы при клонировании объектов, Symbol.species позволит настраивать создание новых объектов в соответствии с требуемыми условиями.
  • При использовании функция RegExp: если вы создаете собственные регулярные выражения с помощью конструктора RegExp, вы можете использовать Symbol.species, чтобы указать, какой конструктор должен использоваться при создании нового регулярного выражения.

Обращение к родительскому классу с помощью super

Super — это ключевое слово, которое используется для обращения к родительскому классу или его методам. Super может быть использовано в двух случаях:

В конструкторе дочернего класса для вызова конструктора родительского класса. При этом:

  • Super должно быть вызвано до использования ключевого слова this в конструкторе дочернего класса.
  • Super принимает аргументы для конструктора родительского класса.

В методе дочернего класса для вызова метода родительского класса с тем же именем. При этом:

  • Super вызывается как функция с аргументами для метода родительского класса.
  • Super также может быть использовано для обращения к свойствам родительского класса.
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(this.name + " издает звук");
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Вызываем конструктор родительского класса
    this.breed = breed; // Добавляем новое свойство
  }

  speak() {
    super.speak(); // Вызываем метод родительского класса
    console.log(this.name + " лает");
  }
}

let animal1 = new Animal("Animal");
let dog1 = new Dog("Rex", "Shepherd");

animal1.speak(); // Animal издает звук
dog1.speak(); // Rex издает звук
// Rex лает

В этом примере мы определили класс Animal, который имеет одно свойство name (имя) и один метод speak (говорить). Затем мы определили класс Dog, который наследует класс Animal и добавляет новое свойство breed (порода) и переопределяет метод speak. Потом создали два объекта animal1 и dog1 на основе классов Animal и Dog и вызвали их методы speak. Заметьте — метод speak класса Dog вызывает метод speak класса Animal с помощью super и добавляет свою логику.

Софья Пирогова

Софья Пирогова

Главный редактор / Автор статей
Георгий Бабаян

Георгий Бабаян

Основатель и CEO Эльбрус Буткемп