JavaScript – это не классический объектно-ориентированный язык, основанный на классах, однако в нем есть способ для их реализации.
Согласно Википедии класс-ориентированное программирование – это стиль объектно-ориентированного программирования (ООП), в котором наследование происходит через определение классов объектов, а не через сами объекты.
Классовая модель – это самая популярная реализация объектно-ориентированного программирования. Однако ООП в JavaScript основано не на классах, а на прототипах. В этой модели для создания новых объектов используется шаблонный объект – прототип.
Посмотрим на пример кода:
В переменную names
мы записали объект, у которого есть два свойства – fname
и lname
– и ни одного метода. Откуда же взялся метод hasOwnProperty
?
Дело в том, что объект names не существует сам по себе, у него есть прототип – Object.prototype
.
Выведем эту переменную в консоль:
Вот что мы увидим:
Развернем загадочное свойство __proto__
:
Мы можем увидеть конструктор объекта names
– функцию Object()
– и множество методов под ним. Среди них – и hasOwnProperty
. Все эти методы хранятся в прототипе Object
, но доступны и самому объекту names
.
Другими словами, все объекты в JavaScript создаются с использованием прототипа Object.prototype
:
И каждый объект имеет доступ ко всем методам этого прототипа, не обладая ими напрямую.
Свойство __proto__
Каждый объект в JavaScript имеет свойство __proto__
, в котором хранится ссылка на другой объект, являющийся его прототипом.
Через это свойство, этот объект и получает доступ к свойствам и методам прототипа.
По умолчанию у всех объектов в свойство __proto__
записана ссылка на Object.prototype
. Однако мы можем изменить это – иначе говоря «унаследовать» объект от другого прототипа.
Изменение прототипа
Не следует менять свойство __proto__
напрямую, для этого существуют специальные методы.
Object.create()
Посмотрим на объект bingo
в консоли:
Теперь в свойстве __proto__
находится другая функция-конструктор и метод speak
.
Object.create
создает новый объект, автоматически устанавливая ему указанный прототип.
Ключевое слово new
Теперь свойство john.__proto__
ссылается на DogObject.prototype
. Обратите внимание – не на функцию DogObject
(она является только конструктором объектов), а на объект, записанный в ее свойстве prototype
.
Данные и методы хранятся только в объектах. Функции-конструкторы вродеDogObject
илиObject
только предлагают удобный способ конфигурации (через this) и создания новых объектов.Функция-конструктор имеет свойствоprototype
, в котором хранится объект-прототип. По его образу эта функция будет создавать новые объекты.
Прототип объекта, хранящийся в его свойстве__proto__
, и объект, хранящийся в свойствеprototype
конструктора – одно и то же.
В свою очередь прототип прототипа john
ссылается на уже знакомый нам Object.prototype
.
То, что мы видим, называется цепочкой прототипов. Это основа прототипно-ориентированного программирования. Объект может получить доступ к любому свойству или методу, которое есть у любого звена его прототипной цепочки.
Сколько бы прототипов ни было в этой цепочке, последний почти всегда будет наследовать от исходногоObject.prototype
. При необходимости можно избавиться от дефолтного прототипа, вызвав методObject.create
и передав емуnull
. Это бывает полезно для создания словарей, в которых не должно быть ничего лишнего.
Ключевое слово new
, которое превращает обычную функцию в конструктор объектов, делает по сути ту же самую вещь, что и Object.create()
, только проще.
Функции – тоже объекты
Если вас смутило наличие свойств у функции DogObject
, доступных через точку (prototype
), то вспомните, что в JS функции – это тоже объекты.
В качестве прототипа они имеют объект Function.prototype
, который в свою очередь наследует от Object.prototype
.
В Function.prototype содержится много дополнительных свойств и методов, которые наследует от него любая функция (call
, apply
, isPrototypeOf
).
Переходим к классам
Прототипная реализация ООП интересна и имеет свои преимущества, но она не так популярна, как классовая. Поэтому когда в ECMAScript 2015 появилось ключевое слово class
, разработчики были очень этому рады.
JS стал походить на классический привычный объектно-ориентированный язык. Однако не будем забывать, что классы в нем – всего лишь синтаксический сахар над теми самыми прототипами. На вид – классы, под капотом – по-прежнему прототипы.
Вот так выглядит обычное создание класса в JavaScript:
Посмотрим в консоль:
Ничего не напоминает?
Все то же свойство __proto__
, которое ссылается на Animals.prototype
(-> Object.prototype).
Мы видим, что собственные значения свойств (name
и specie
) определяются внутри метода constructor
. Кроме него создаются дополнительные функции sing
и dance
– методы прототипа.
Подкапотную реализацию этой конструкции мы разбирали только что – это функция-конструктор + ключевое слово new
.
Субклассирование
Субклассирование – это наследование и расширение родительского класса дочерним.
Предположим, вам нужно создать класс Cats
. Вы могли бы написать его с нуля, но зачем, если уже есть класс Animals
, который реализует часть нужного вам функционала. Вы можете просто унаследовать Cats
от Animals
и добавить то, чего не хватает (например, цвет усов).
Наследование в класс-ориентированном синтаксисе выглядит так:
Объект класса Cats
clara
может использовать свойства и методы как класса Cats
, так и класса Animals
.
Вот так clara
выглядит в консоли:
В свойстве clara.__proto__.constructor
лежит класс Cats
, через него осуществляется доступ к методу whiskers()
. Дальше в цепочке прототипов – класс Animals
, с методами sing()
и dance()
. name
и age
– это свойства самого объекта.
Перепишем этот код в прототипном стиле с использованием метода Object.create()
:
Метод Object.setPrototypeOf
принимает два аргумента (объект, которому нужно установить новый прототип, и собственно сам желаемый прототип).
Функция Animals
возвращает объект, прототипом которого является animalConstructor
. Функция Cats
создает объект с помощью конструктора Animals
, но принудительно меняет его прототип на catConstructor
, добавляя таким образом новые свойства. catConstructor
в свою очередь тоже получает прототипом animalConstructor
, чтобы образовалась цепочка прототипного наследования.
Таким образом, обычные животные будут иметь доступ только к методам animalConstructor
, а кошки – еще и к catConstructor
.
JavaScript оказался достаточно гибок, чтобы превратить свое прототипное ООП в классовое для удобства разработчиков.
Комментарии