На практике нам часто надо создавать много объектов одного вида, например пользователей, товары или что-то ещё.
Как мы уже знаем, с этим может помочь new function.
Но в современном JavaScript есть и более продвинутая конструкция «class», которая предоставляет новые возможности, полезные для объектно-ориентированного программирования.
Затем используйте вызов new MyClass() для создания нового объекта со всеми перечисленными методами.
При этом автоматически вызывается метод constructor(), в нём мы можем инициализировать объект.
Что такое класс?
Итак, что же такое class? Это не полностью новая языковая сущность, как может показаться на первый взгляд.
Давайте развеем всю магию и посмотрим, что такое класс на самом деле. Это поможет в понимании многих сложных аспектов.
В JavaScript класс – это разновидность функции.
Наследование классов – это способ расширения одного класса другим классом.
Таким образом, мы можем добавить новый функционал к уже существующему.
Поскольку кролики – это животные, класс Rabbit должен быть основан на Animal, и иметь доступ к методам животных, так чтобы кролики могли делать то, что могут делать «общие» животные.
Синтаксис для расширения другого класса следующий: class Child extends Parent.
Давайте создадим class Rabbit, который наследуется от Animal.
Объект класса Rabbit имеет доступ как к методам Rabbit, таким как rabbit.hide(), так и к методам Animal, таким как rabbit.run().
Внутри ключевое слово extends работает по старой доброй механике прототипов. Оно устанавливает Rabbit.prototype.[[Prototype]] в Animal.prototype. Таким образом, если метода не оказалось в Rabbit.prototype, JavaScript берет его из Animal.prototype.
Например, чтобы найти метод rabbit.run, движок проверяет (снизу вверх на картинке):
Объект rabbit (не имеет run).
Его прототип, то есть Rabbit.prototype (имеет hide, но не имеет run).
Его прототип, то есть (вследствие extends) Animal.prototype, в котором, наконец, есть метод run.
Как мы помним из главы Встроенные прототипы, сам JavaScript использует наследование на прототипах для встроенных объектов. Например, Date.prototype.[[Prototype]] является Object.prototype, поэтому у дат есть универсальные методы объекта.
Мы также можем присвоить метод самому классу. Такие методы называются статическими.
В объявление класса они добавляются с помощью ключевого слова static:
Это фактически то же самое, что присвоить метод напрямую как свойство функции.
Значением this при вызове User.staticMethod() является сам конструктор класса User (правило «объект до точки»).
Обычно статические методы используются для реализации функций, принадлежащих классу целиком, вообще, и при этом не относящимся к каким-то отдельным объектам.
Звучит не очень понятно? Сейчас все встанет на свои места.
Например, есть объекты статей Article, и нужна функция для их сравнения.
Естественное решение – сделать для этого статический метод Article.compare
Другим примером может быть так называемый «фабричный» метод.
Скажем, нам нужно несколько способов создания статьи:
Создание через заданные параметры (title, date и т. д.).
Создание пустой статьи с сегодняшней датой.
…или как-то ещё.
Первый способ может быть реализован через конструктор. А для второго можно использовать статический метод класса.
Один из важнейших принципов объектно-ориентированного программирования – разделение внутреннего и внешнего интерфейсов.
Это обязательная практика в разработке чего-либо сложнее, чем «hello world».
Чтобы понять этот принцип, давайте на секунду забудем о программировании и обратим взгляд на реальный мир.
Устройства, которыми мы пользуемся, обычно довольно сложно устроены. Но разделение внутреннего и внешнего интерфейсов позволяет нам пользоваться ими без каких-либо проблем.
В объектно-ориентированном программировании свойства и методы разделены на 2 группы:>
- Внутренний интерфейс – методы и свойства, доступные из других методов класса, но не снаружи класса.
- Внешний интерфейс – методы и свойства, доступные снаружи класса.
Если мы продолжаем аналогию с кофеваркой – то, что скрыто внутри: трубка кипятильника, нагревательный элемент и т.д. – это внутренний интерфейс.
Внутренний интерфейс используется для работы объекта, его детали используют друг друга. Например, трубка кипятильника прикреплена к нагревательному элементу.
Но снаружи кофеварка закрыта защитным кожухом, так что никто не может добраться до сложных частей. Детали скрыты и недоступны. Мы можем использовать их функции через внешний интерфейс.
Итак, всё, что нам нужно для использования объекта, это знать его внешний интерфейс. Мы можем совершенно не знать, как это работает внутри, и это здорово.
Это было общее введение.
В JavaScript есть два типа полей (свойств и методов) объекта:
- Публичные: доступны отовсюду. Они составляют внешний интерфейс. До этого момента мы использовали только публичные свойства и методы.
- Приватные: доступны только внутри класса. Они для внутреннего интерфейса.
Во многих других языках также существуют «защищённые» поля, доступные только внутри класса или для дочерних классов (то есть, как приватные, но разрешён доступ для наследующих классов) и также полезны для внутреннего интерфейса. В некотором смысле они более распространены, чем приватные, потому что мы обычно хотим, чтобы наследующие классы получали доступ к внутренним полям.
Защищённые поля не реализованы в JavaScript на уровне языка, но на практике они очень удобны, поэтому их эмулируют.
А теперь давайте сделаем кофеварку на JavaScript со всеми этими типами свойств. Кофеварка имеет множество деталей, мы не будем их моделировать для простоты примера (хотя могли бы).
От встроенных классов, таких как Array, Map и других, тоже можно наследовать.
Обратите внимание на интересный момент: встроенные методы, такие как filter, map и другие возвращают новые объекты унаследованного класса PowerArray. Их внутренняя реализация такова, что для этого они используют свойство объекта constructor.
У встроенных объектов есть собственные статические методы, например Object.keys, Array.isArray и т. д.
Как мы уже знаем, встроенные классы расширяют друг друга.
Обычно, когда один класс наследует другой, то наследуются и статические методы. Это было подробно разъяснено в главе Статические свойства и методы.
Но встроенные классы – исключение. Они не наследуют статические методы друг друга.
Например, и Array, и Date наследуют от Object, так что в их экземплярах доступны методы из Object.prototype. Но Array.[[Prototype]] не ссылается на Object, поэтому нет методов Array.keys() или Date.keys().