Классы на прототипах как работает ООП в JavaScript

Классы на прототипах как работает ООП в JavaScript

JavaScript – это не классический объектно-ориентированный язык, основанный на классах, однако в нем есть способ для их реализации.

Согласно Википедии класс-ориентированное программирование – это стиль объектно-ориентированного программирования (ООП), в котором наследование происходит через определение классов объектов, а не через сами объекты.

Классовая модель – это самая популярная реализация объектно-ориентированного программирования. Однако ООП в JavaScript основано не на классах, а на прототипах. В этой модели для создания новых объектов используется шаблонный объект – прототип.

Посмотрим на пример кода:

В переменную names мы записали объект, у которого есть два свойства – fname и lname – и ни одного метода. Откуда же взялся метод hasOwnProperty ?

Дело в том, что объект names не существует сам по себе, у него есть прототип – Object.prototype .

Выведем эту переменную в консоль:

Вот что мы увидим:

Классы на прототипах как работает ООП в JavaScript

Развернем загадочное свойство __proto__ :

Классы на прототипах как работает ООП в JavaScript

Мы можем увидеть конструктор объекта names – функцию Object() – и множество методов под ним. Среди них – и hasOwnProperty . Все эти методы хранятся в прототипе Object , но доступны и самому объекту names .

Другими словами, все объекты в JavaScript создаются с использованием прототипа Object.prototype :

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

Свойство __proto__

Каждый объект в JavaScript имеет свойство __proto__ , в котором хранится ссылка на другой объект, являющийся его прототипом.

Через это свойство, этот объект и получает доступ к свойствам и методам прототипа.

По умолчанию у всех объектов в свойство __proto__ записана ссылка на Object.prototype . Однако мы можем изменить это – иначе говоря «унаследовать» объект от другого прототипа.

Изменение прототипа

Не следует менять свойство __proto__ напрямую, для этого существуют специальные методы.

Object.create()

Посмотрим на объект bingo в консоли:

Классы на прототипах как работает ООП в JavaScript

Теперь в свойстве __proto__ находится другая функция-конструктор и метод speak .

Object.create создает новый объект, автоматически устанавливая ему указанный прототип.

Ключевое слово new

Теперь свойство john.__proto__ ссылается на DogObject.prototype . Обратите внимание – не на функцию DogObject (она является только конструктором объектов), а на объект, записанный в ее свойстве prototype .

В свою очередь прототип прототипа john ссылается на уже знакомый нам Object.prototype .

То, что мы видим, называется цепочкой прототипов. Это основа прототипно-ориентированного программирования. Объект может получить доступ к любому свойству или методу, которое есть у любого звена его прототипной цепочки.

Ключевое слово new , которое превращает обычную функцию в конструктор объектов, делает по сути ту же самую вещь, что и Object.create() , только проще.

Функции – тоже объекты

Если вас смутило наличие свойств у функции DogObject , доступных через точку ( prototype ), то вспомните, что в JS функции – это тоже объекты.

В качестве прототипа они имеют объект Function.prototype , который в свою очередь наследует от Object.prototype .

В Function.prototype содержится много дополнительных свойств и методов, которые наследует от него любая функция ( call , apply , isPrototypeOf ).

Переходим к классам

Прототипная реализация ООП интересна и имеет свои преимущества, но она не так популярна, как классовая. Поэтому когда в ECMAScript 2015 появилось ключевое слово class , разработчики были очень этому рады.

JS стал походить на классический привычный объектно-ориентированный язык. Однако не будем забывать, что классы в нем – всего лишь синтаксический сахар над теми самыми прототипами. На вид – классы, под капотом – по-прежнему прототипы.

Вот так выглядит обычное создание класса в JavaScript:

Посмотрим в консоль:

Классы на прототипах как работает ООП в JavaScript

Ничего не напоминает?

Все то же свойство __proto__ , которое ссылается на Animals.prototype (-> Object.prototype).

Мы видим, что собственные значения свойств ( name и specie ) определяются внутри метода constructor . Кроме него создаются дополнительные функции sing и dance – методы прототипа.

Подкапотную реализацию этой конструкции мы разбирали только что – это функция-конструктор + ключевое слово new .

Субклассирование

Субклассирование – это наследование и расширение родительского класса дочерним.

Предположим, вам нужно создать класс Cats . Вы могли бы написать его с нуля, но зачем, если уже есть класс Animals , который реализует часть нужного вам функционала. Вы можете просто унаследовать Cats от Animals и добавить то, чего не хватает (например, цвет усов).

Наследование в класс-ориентированном синтаксисе выглядит так:

Объект класса Cats clara может использовать свойства и методы как класса Cats , так и класса Animals .

Вот так clara выглядит в консоли:

Классы на прототипах как работает ООП в JavaScript

В свойстве clara.__proto__.constructor лежит класс Cats , через него осуществляется доступ к методу whiskers() . Дальше в цепочке прототипов – класс Animals , с методами sing() и dance() . name и age – это свойства самого объекта.

Перепишем этот код в прототипном стиле с использованием метода Object.create() :

Метод Object.setPrototypeOf принимает два аргумента (объект, которому нужно установить новый прототип, и собственно сам желаемый прототип).

Функция Animals возвращает объект, прототипом которого является animalConstructor . Функция Cats создает объект с помощью конструктора Animals , но принудительно меняет его прототип на catConstructor , добавляя таким образом новые свойства. catConstructor в свою очередь тоже получает прототипом animalConstructor , чтобы образовалась цепочка прототипного наследования.

Таким образом, обычные животные будут иметь доступ только к методам animalConstructor , а кошки – еще и к catConstructor .

JavaScript оказался достаточно гибок, чтобы превратить свое прототипное ООП в классовое для удобства разработчиков.