Главная страница » Что такое прототип в js

Что такое прототип в js

  • автор:

Прототипы и наследование в JavaScript

Прототипы и наследование в JavaScript

В этой статье мы изучим всё что касается прототипов в JavaScript. Разберём зачем они нужны, что такое наследование и цепочка прототипов, как работает this внутри методов, рассмотрим пример расширения классов и многое другое.

Что такое прототипы?

При создании объектов, например, с помощью конструктора, каждый из них будет содержать специальное внутреннее свойство [[Prototype]] , указывающее на его прототип . В JavaScript прототипы используются для организации наследования .

Допустим у нас имеется конструктор Box :

При объявлении конструктора или класса у него автоматически появится свойство prototype . Оно содержит прототип . Прототип – это объект. В данном случае им будет являться Box.prototype . Это очень важный момент.

Этот прототип будет автоматически назначаться всем объектам , которые будут создаваться с помощью этого конструктора:

Таким образом при создании объекта, в данном случае, box1 , он автоматически будет иметь ссылку на прототип, то есть на свойство Box.prototype .

Это очень легко проверить:

Прототип объекта в JavaScript

Получить прототип объекта в JavaScript можно с помощью статического метода Object.getPrototypeOf или специального свойства __proto__ . В этом примере показаны два этих способа. Кстати, свойство __proto__ не является стандартным, но оно поддерживается всеми браузерами.

Свойство prototype имеется у каждой функции за исключением стрелочных. Это свойство как мы уже отмечали выше в качестве значения имеет объект. По умолчанию в нём находится только одно свойство constructor , которое содержит ссылку на саму эту функцию:

То есть Box.prototype.constructor – это сам конструктор Box .

Если создать ещё один объект класса Box , то он тоже будет иметь точно такой же прототип. Как мы уже отмечали выше прототипы в JavaScript используются для организации наследования. То есть, если мы сейчас в Box.prototype добавим какой-нибудь, например, метод, то он будет доступен для всех экземпляров класса Box :

Прототип объекта на уровне родительского класса в JavaScript

Обратите внимание, что метода print нет у объектов box1 и box2 . Но если раскрыть значение свойства [[Prototype]] в консоли в веб-браузере, то вы увидите его. То есть этот метод находится на уровне класса Box и наследуется всеми его экземплярами.

Соответственно получается, что мы можем вызвать print как метод объектов box1 и box2 . Таким образом нам доступны не только собственные свойства и методы, но также наследуемые. А наследование как вы уже понимаете осуществляется в JavaScript на основе прототипов.

Так что же такое прототип? Прототип в JavaScript – это просто ссылка на объект, который используется для наследования .

Наследование

Что такое наследование? Если после переменной, содержащей некоторый объект поставить точку, то вы увидите все доступные для него свойства и методы:

Список доступных свойств и методов на уровне объекта box1

Здесь width и height – это его собственные свойства. Далее на уровне родительского класса находятся методы constructor и print . Т.е. вы можете вызвать метод print , потому что он наследуется всеми экземплярами класса Box . Кроме этого, здесь имеются методы класса Object , такие как hasOwnProperty , isPrototypeOf , toString и так далее. Эти методы тоже доступны, потому что Box.prototype наследует все свойства и методы Object.prototype .

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

Следовательно, в этом примере объект box1 имеет свои собственные свойства width и height , а также наследует все свойства и методы Box.prototype и Object.prototype .

Цепочка прототипов

В JavaScript наследование осуществляется только на уровне объектов через прототипы. То есть один объект имеет ссылку на другой через специальное внутреннее свойство [[Prototype]] . Тот в свою очередь тоже имеет ссылку и т.д. В результате получается цепочка прототипов.

Таким образом наследование, которые мы рассмотрели выше на примере объекта box1 происходит благодаря существованию следующей цепочки прототипов:

Заканчивается цепочка на прототипе глобального класса Object , потому что он не имеет прототипа, то есть его значение __proto__ равно null .

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

Если указанное свойство или метод не найден, то возвращается undefined .

Например, если метод print мы добавим в сам объект box1 , то будет использоваться уже он, а не тот, который находится в прототипе Box.prototype :

Почему? Потому что поиск сразу прекращается, как только указанный метод будет найден. А в данном случае он будет найден сразу в объекте, поэтому переход в прототип не осуществится.

Значение this внутри методов

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

Рассмотрим следующий пример:

Здесь мы вызываем up как метод объектов counter1 и counter2 . Данный метод не является собственным для этих объектов, он наследуется и находится на уровне класса Counter . Но на самом деле это не имеет значения. Единственное что важно для this – это только то, для какого объекта мы вызываем этот метод, то есть что стоит перед точкой. Это и будет this .

При вызове counter1.up() , this внутри этого метода будет указывать на counter1 :

На строчке перед точкой стоит counter2 , значит this внутри up будет указывать на него :

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

Установка прототипа объекту

Установить прототип объекту можно с помощью статического метода Object.setPrototypeOf() или свойства __proto__ .

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

Проверить что прототипом для person2 является person1 очень просто:

Установка объекта прототипа с помощью специального свойства __proto__

При этом метод printName становится наследуемым, то есть доступным для объекта person2 :

Пример установки прототипа с помощью Object.setPrototypeOf() :

Установка объекта прототипа с помощью Object.setPrototypeOf()

В этом примере мы в качестве прототипа для errorMessage установили message .

Чтобы было более понятно как работает метод Object.setPrototypeOf , рассмотрим его синтаксис:

  • obj – объект, для которого необходимо установить прототип;
  • prototype – объект, который будет использоваться в качестве прототипа для obj , или null , если у obj не должно быть прототипа.

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

Следовательно, мы получим ошибку, если попытаемся для message установить в качестве прототипа errorMessage :

Кроме этого, в JavaScript нет множественного наследования, то есть нельзя одному объекту назначить несколько прототипов.

Наследование классов

Допустим у нас имеется конструктор Person :

Создадим конструктор Student , который будет расширять класс Person :

Но на данном этапе, они сейчас полностью независимы. Но для того чтобы класс Student расширял Person нужно указать, что прототипом для Student.prototype является Person.prototype . Например, выполним это с помощью свойства __proto__ :

Создадим новый экземпляр класса Student :

Для этого объекта будет доступен как метод getSchoolName , так и getName .

Рассмотрим ещё один очень интересный пример с наследованием классов:

Здесь у нас имеются 3 класса: Rectangle , Square и Circle . Для того чтобы объекты класса Square наследовали свойства и методы Rectangle.prototype мы прописали следующую связь:

Похожим образом мы это сделали также для объектов класса Circle :

Таким образом, объекты, являющиеся экземплярами класса Circle наследуют свойства и методы Circle.prototype , Square.prototype , Rectangle.prototype и Object.prototype . Это происходит благодаря следующей цепочки прототипов:

Для вызова в Square родительского конструктора мы используем метод call . С помощью call мы задаём контекст, в котором нам нужно вызвать функцию. В данном случае мы вызываем Rectangle в контексте текущего создаваемого объекта.

Свойство constructor

У каждой функции, кроме стрелочных, как уже отмечали выше по умолчанию, имеется свойство prototype . Как мы уже отмечали выше это прототип, который автоматически будут иметь все объекты, если мы эту функцию будет использовать как конструктор, то есть для создания объектов.

По умолчанию свойство prototype функции содержит следующий объект:

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

Свойство constructor можно использовать для создания объектов:

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

Не рекомендуется полностью перезаписывать значение свойства prototype , потому что в этом случае вы потеряете constructor и его придётся добавлять вручную:

Если нужно что-то добавить в prototype , то делайте это как в примерах выше, то есть посредством простого добавления ему нужных свойств и методов:

Встроенные прототипы

В JavaScript практически всё является объектами. То есть функции, массивы, даты и так далее. Исключением являются только примитивные типы данных: строка, число и так далее.

Например, при создании объекта { name: ‘Tom’ } внутренне используется конструктор Object :

Прототипом такого объекта соответственно становится Object.prototype и в этом легко убедиться:

Поэтому нам на уровне этого объекта доступны различные методы, они берутся из Object.prototype . Например, метод hasOwnProperty :

Этот метод возвращает true , когда указанное свойство является для этого объекта родным. В противном случае – false .

При этом Object.prototype является корнем иерархии других встроенных прототипов. Но при этом он сам не имеет прототипа.

JavaScript - Object.prototype

На рисунке видно, что конструктор Object имеет по умолчанию свойство prototype . Это значение будет автоматически записываться в свойство [[Prototype]] объектов, которые будет создаваться с помощью этого конструктора. В Object.prototype имеется свойство constructor , которые указывает на сам конструктор. Эти связи между Object и Object.prototype показаны на схеме. Кроме этого Object.prototype не имеет прототипа. То есть его значение [[Prototype]] содержит null .

Теперь давайте рассмотрим, как выполняется создание даты в JavaScript . Осуществляется это очень просто посредством конструктора Date :

Методы прототипа конструктора Date

Следовательно, прототипом даты является Date.prototype :

Этот прототип содержит большое количество методов для работы с датой, например, такие как getDate , getHours и так далее. Их нет в now , но они доступны нам посредством наследования.

Объект Date.prototype имеет в качестве прототипа Object.prototype :

Следовательно, методы Object.prototype , которых нет в Date.prototype также доступны для now . Например, hasOwnProperty :

Таким образом можно нарисовать следующую схему:

JavaScript - Date.prototype

Другие встроенные объекты устроены подобным образом.

Метод Object.create

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

Создание объекта с прототипом Object.prototype :

Данный пример аналогичен этому:

Создание объекта без прототипа:

Во 2 аргументе мы можем объекту сразу передать необходимые свойства. Описываются они в полном формате с использованием специальных атрибутов как в Object.defineProperties :

Здесь мы описали два свойства: name и age . С помощью value мы устанавливаем значение свойству, а посредством аргумента writable задаем доступно ли свойство для изменения.

Name already in use

В главах 3 и 4 мы неоднократно упоминали цепочку [[Prototype]] , но не уточняли что это такое. Пришло время разобраться с тем, как работают прототипы.

Примечание: Любые попытки эмуляции копирования классов, упомянутые в главе 4 как «примеси», полностью обходят механизм цепочки [[Prototype]] , рассматриваемый в этой главе.

Объекты в JavaScript имеют внутреннее свойство, обозначенное в спецификации как [[Prototype]] , которое является всего лишь ссылкой на другой объект. Почти у всех объектов при создании это свойство получает не- null значение.

Примечание: Чуть ниже мы увидим, что объект может иметь пустую ссылку [[Prototype]] , хотя такой вариант встречается реже.

Для чего используется ссылка [[Prototype]] ? В главе 3 мы изучили операцию [[Get]] , которая вызывается когда вы ссылаетесь на свойство объекта, например myObject.a . Стандартная операция [[Get]] сначала проверяет, есть ли у объекта собственное свойство a , если да, то оно используется.

Примечание: Прокси-объекты ES6 выходят за рамки этой книги (мы увидим их в одной из следующих книг серии!), но имейте в виду, что обсуждаемое нами стандартное поведение [[Get]] и [[Put]] неприменимо если используются Proxy .

Но нас интересует то, что происходит, когда a отсутствует в myObject , т.к. именно здесь вступает в действие ссылка [[Prototype]] объекта.

Если стандартная операция [[Get]] не может найти запрашиваемое свойство в самом объекте, то она следует по ссылке [[Prototype]] этого объекта.

Примечание: Чуть позже мы объясним что делает Object.create(..) и как он работает. Пока же считайте, что создается объект со ссылкой [[Prototype]] на указанный объект.

Итак, у нас есть myObject , который теперь связан с anotherObject через ссылку [[Prototype]] . Очевидно, что myObject.a на самом деле не существует, однако обращение к свойству выполнилось успешно (свойство нашлось в anotherObject ) и действительно вернуло значение 2 .

Если бы a не нашлось и в объекте anotherObject , то теперь уже его цепочка [[Prototype]] использовалась бы для дальнейшего поиска.

Этот процесс продолжается до тех пор, пока либо не будет найдено свойство с совпадающим именем, либо не закончится цепочка [[Prototype]] . Если по достижении конца цепочки искомое свойство так и не будет найдено, операция [[Get]] вернет undefined .

По аналогии с этим процессом поиска по цепочке [[Prototype]] , если вы используете цикл for..in для итерации по объекту, будут перечислены все свойства, достижимые по его цепочке (при условии, что они перечислимые — см. enumerable в главе 3). Если вы используете оператор in для проверки существования свойства в объекте, то in проверит всю цепочку объекта (независимо от перечисляемости).

Итак, ссылки в цепочке [[Prototype]] используются одна за другой, когда вы тем или иным способом пытаетесь найти свойство. Поиск заканчивается при нахождении свойства или достижении конца цепочки.

Но где именно «заканчивается» цепочка [[Prototype]] ?

В конце каждой типичной цепочки [[Prototype]] находится встроенный объект Object.prototype . Этот объект содержит различные утилиты, используемые в JS повсеместно, поскольку все обычные (встроенные, не связанные с конкретной средой исполнения) объекты в JavaScript «происходят от» объекта Object.prototype (иными словами, имеют его на вершине своей цепочки [[Prototype]] ).

Некоторые утилиты этого объекта могут быть вам знакомы: .toString() и .valueOf() . В главе 3 мы видели еще одну: .hasOwnProperty(..) . Еще одна функция Object.prototype , о которой вы могли не знать, но узнаете далее в этой главе — это .isPrototypeOf(..) .

Установка и затенение свойств

Ранее в главе 3 мы отмечали, что установка свойств объекта происходит чуть сложнее, чем просто добавление к объекту нового свойства или изменение значения существующего свойства.

Если непосредственно у myObject есть обычное свойство доступа к данным с именем foo , то присваивание сводится к изменению значения существующего свойства.

Если непосредственно у myObject нет foo , то выполняется обход цепочки [[Prototype]] по аналогии с операцией [[Get]] . Если foo не будет найдено в цепочке, то свойство foo добавляется непосредственно к myObject и получает указанное значение, как мы того и ожидаем.

Однако если foo находится где-то выше по цепочке, то присваивание myObject.foo = «bar» может повлечь за собой более сложное (и даже неожиданное) поведение. Рассмотрим этот вопрос подробнее.

Если свойство с именем foo присутствует как у самого myObject , так и где-либо выше в цепочке [[Prototype]] , начинающейся с myObject , то такая ситуация называется затенением. Свойство foo самого myObject затеняет любые свойства foo , расположенные выше по цепочке, потому что поиск myObject.foo всегда находит свойство foo , ближайшее к началу цепочки.

Как уже отмечалось, затенение foo в myObject происходит не так просто, как может показаться. Мы рассмотрим три сценария присваивания myObject.foo = «bar» , когда foo не содержится непосредственно в myObject , а находится выше по цепочке [[Prototype]] объекта myObject :

  1. Если обычное свойство доступа к данным (см. главу 3) с именем foo находится где-либо выше по цепочке [[Prototype]] , и не отмечено как только для чтения ( writable:false ), то новое свойство foo добавляется непосредственно в объект myObject , и происходит затенение свойства.
  2. Если foo находится выше по цепочке [[Prototype]] , но отмечено как только для чтения ( writable:false ), то установка значения этого существующего свойства, равно как и создание затененного свойства у myObject , запрещены. Если код выполняется в strict mode , то будет выброшена ошибка, если нет, то попытка установить значение свойства будет проигнорирована. В любом случае, затенения не происходит.
  3. Если foo находится выше по цепочке [[Prototype]] и является сеттером (см. главу 3), то всегда будет вызываться сеттер. Свойство foo не будет добавлено в myObject , сеттер foo не будет переопределен.

Большинство разработчиков предполагает, что присваивание свойства ( [[Put]] ) всегда приводит к затенению, если свойство уже существует выше по цепочке [[Prototype]] , но как видите это является правдой лишь в одной (№1) из трех рассмотренных ситуаций.

Если вы хотите затенить foo в случаях №2 и №3, то вместо присваивания = нужно использовать Object.defineProperty(..) (см. главу 3) чтобы добавить foo в myObject .

Примечание: Ситуация №2 может показаться наиболее удивительной из трех. Наличие свойства только для чтения мешает нам неявно создать (затенить) свойство с таким же именем на более низком уровне цепочки [[Prototype]] . Причина такого ограничения по большей части кроется в желании поддержать иллюзию наследования свойств класса. Если представить, что foo с верхнего уровня цепочки наследуется (копируется) в myObject , то имеет смысл запретить изменение этого свойства foo в myObject . Но если перейти от иллюзий к фактам и согласиться с тем, что никакого наследования с копированием на самом деле не происходит (см. главы 4 и 5), то кажется немного странным, что myObject не может иметь свойство foo лишь потому, что у какого-то другого объекта есть неизменяемое свойство foo . Еще более странно то, что это ограничение действует только на присваивание = , но не распространяется на Object.defineProperty(..) .

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

Затенение может даже произойти неявно, поэтому если вы хотите его избежать, будьте бдительны. Например:

Хотя может показаться, что выражение myObject.a++ должно (через делегирование) найти и просто инкрементировать свойство anotherObject.a , вместо этого операция ++ соответствует выражению myObject.a = myObject.a + 1 . В результате [[Get]] ищет свойство a через [[Prototype]] и получает текущее значение 2 из anotherObject.a , далее это значение увеличивается на 1, после чего [[Put]] присваивает значение 3 новому затененному свойству a в myObject . Ой!

Будьте очень осторожны при работе с делегированными свойствами, пытаясь изменить их значение. Если вам нужно инкрементировать anotherObject.a , то вот единственно верный способ сделать это: anotherObject.a++ .

Вы уже могли задаться вопросом: «Зачем одному объекту нужна ссылка на другой объект?» Какой от этого толк? Это очень хороший вопрос, но сначала нам нужно выяснить, чем [[Prototype]] не является, прежде чем мы сможем понять и оценить то, чем он является, и какая от него польза.

В главе 4 мы выяснили, что в отличие от класс-ориентированных языков в JavaScript нет абстрактных шаблонов/схем объектов, называемых «классами». В JavaScript просто есть объекты.

На самом деле, JavaScript — практически уникальный язык, ведь пожалуй только он имеет право называться «объектно-ориентированным», т.к. относится к весьма немногочисленной группе языков, где объекты можно создавать напрямую, без классов.

В JavaScript классы не могут описывать поведение объекта (учитывая тот факт, что их вообще не существует!). Объект сам определяет собственное поведение. Есть только объект.

В JavaScript есть специфическое поведение, которым долгие годы цинично злоупотребляли для создания поделок, внешне похожих на «классы». Рассмотрим этот подход более подробно.

Специфическое поведение «как бы классов» основано на одной странной особенности: у всех функций по умолчанию есть публичное, неперечислимое (см. главу 3) свойство, называемое prototype , которое указывает на некий объект.

Этот объект часто называют «прототипом Foo», поскольку обращение к нему происходит через свойство с неудачно выбранным названием Foo.prototype . Как мы вскоре увидим, такая терминология обречена приводить людей в замешательство. Вместо этого, я буду называть его «объектом, ранее известным как прототип Foo». Ладно, шучу. Как насчет этого: «объект, условно называемый ‘Foo точка prototype'»?

Как бы мы не называли его, что же это за объект?

Проще всего объяснить так: у каждого объекта, создаваемого с помощью вызова new Foo() (см. главу 2), ссылка [[Prototype]] будет указывать на этот объект «Foo точка prototype».

Проиллюстрируем на примере:

Когда a создается путем вызова new Foo() , одним из результатов (все четыре шага см. в главе 2) будет создание в a внутренней ссылки [[Prototype]] на объект, на который указывает Foo.prototype .

Остановитесь на секунду и задумайтесь о смысле этого утверждения.

В класс-ориентированных языках множественные копии (или «экземпляры») создаются как детали, штампуемые на пресс-форме. Как мы видели в главе 4, так происходит потому что процесс создания экземпляра (или наследования от) класса означает «скопировать поведение из этого класса в физический объект», и это выполняется для каждого нового экземпляра.

Но в JavaScript такого копирования не происходит. Вы не создаете множественные экземпляры класса. Вы можете создать множество объектов, *связанных** ссылкой [[Prototype]] с общим объектом. Но по умолчанию никакого копирования не происходит, поэтому эти объекты не становятся полностью автономными и не соединенными друг с другом, напротив, они весьма ***связаны***.

Вызов new Foo() создает новый объект (мы назвали его a ), и этот новый объект a связан внутренней ссылкой [[Prototype]] с объектом Foo.prototype .

В результате получилось два объекта, связанных друг с другом. Вот и все. Мы не создали экземпляр класса. И мы уж точно не копировали никакого поведения из «класса» в реальный объект. Мы просто связали два объекта друг с другом.

На самом деле секрет, о котором не догадывается большинство JS разработчиков, состоит в том, что вызов функции new Foo() практически никак напрямую не связан с процессом создания ссылки. Это всегда было неким побочным эффектом. new Foo() — это косвенный, окольный путь к желаемому результату: новому объекту, связанному с другим объектом.

Можем ли мы добиться желаемого более прямым путем? Да! Герой дня — Object.create(..) . Но мы вернемся к нему чуть позже.

В JavaScript мы не делаем копии из одного объекта («класса») в другой («экземпляр»). Мы создаем ссылки между объектами. В механизме [[Prototype]] визуально стрелки идут справа налево и снизу вверх.

Этот механизм часто называют «прототипным наследованием» (мы подробнее рассмотрим код чуть ниже), которое обычно считается вариантом «классического наследования» для динамических языков. Это попытка воспользоваться общепринятым пониманием термина «наследование» в класс-ориентированных языках и *подогнать** знакомую семантику под динамический язык.

Термин «наследование» имеет очень четкий смысл (см. главу 4). Добавление перед ним слова «прототипное» чтобы обозначить на самом деле почти противоположное поведение привело к неразберихе в течение двух десятков лет.

Я люблю говорить, что использовать слово «прототипное» перед «наследованием» для придания существенно иного смысла — это как держать в одной руке апельсин, а в другой яблоко и настаивать на том, что яблоко — это «красный апельсин». Не важно, какое прилагательное я добавлю перед ним, это не изменит тот факт, что один фрукт — яблоко, а другой — апельсин.

Лучше называть яблоко яблоком, применяя аккуратную и точную терминологию. Это позволяет нам легко понимать их многочисленные сходства и отличия, потому что мы все имеем ясное представление что такое «яблоко».

Из-за всей этой неразберихи с терминами я считаю, что само название «прототипное наследование» (а также некорректное использование связанных с ним терминов, таких как «класс», «конструктор», «экземпляр», «полиморфизм» и т.д.) принесло больше вреда чем пользы в понимании того, как на самом деле работает JavaScript.

«Наследование» подразумевает операцию копирования, а JavaScript не копирует свойства объекта (по умолчанию). Вместо этого JS создает ссылку между двумя объектами, в результате один объект по сути делегирует доступ к свойствам/функциям другому объекту. «Делегирование» (см. главу 6) — более точный термин для описания механизма связывания объектов в JavaScript.

Другой термин, который иногда встречается в JavaScript — это «дифференциальное наследование». Суть в том, что мы описываем поведение объекта с точки зрения того, что отличается в нем от более общего описания. Например, вы можете сказать, что автомобиль является видом транспортного средства, у которого ровно 4 колеса, вместо того чтобы заново описывать все свойства транспортного средства (двигатель, и т.д.).

Если представить, что любой объект в JS является суммой всего поведения, которое доступно через делегирование, и мысленно объединить все это поведение в одну реальную *сущность**, то можно предположить, что «дифференциальное наследование» (вроде как) подходящий термин.

Но как и «прототипное наследование», «дифференциальное наследование» претендует на то, что мысленная модель важнее того, что физически происходит в языке. Здесь упускается из виду факт, что объект B на самом деле не создается дифференциально, а создается с конкретно заданными характеристиками, а также с «дырами», где ничего не задано. Эти дыры (пробелы или отсутствующие определения) могут заменяться механизмом делегирования, который на лету «заполняет их» делегированным поведением.

Объект по умолчанию не сворачивается в единый дифференциальный объект посредством копирования, как это подразумевается в мысленной модели «дифференциального наследования». Таким образом, «дифференциальное наследование» не слишком подходит для описания реального механизма работы [[Prototype]] в JavaScript.

Вы *можете** придерживаться терминологии и мысленной модели «дифференциального наследования», но нельзя отрицать тот факт, что это *лишь* упражнение ума в вашей голове, а не реальное поведение движка.

Вернемся к рассмотренному ранее коду:

Что именно заставляет нас подумать, что Foo является «классом»?

С одной стороны, мы видим использование ключевого слова new , совсем как в класс-ориентированных языках, когда создаются экземпляры классов. С другой стороны, кажется, что мы на самом деле выполняем метод конструктора класса, потому что метод Foo() на самом деле вызывается, так же как конструктор реального класса вызывается при создании экземпляра.

У объекта Foo.prototype есть еще один фокус, который усиливает недоразумение, связанное с семантикой «конструкторов». Посмотрите на этот код:

По умолчанию объект Foo.prototype (во время объявления в первой строке примера!) получает публичное неперечислимое (см. главу 3) свойство .constructor , и это свойство является обратной ссылкой на функцию (в данном случае Foo ), с которой связан этот объект. Более того, мы видим, что объект a , созданный путем вызова «конструктора» new Foo() , похоже тоже имеет свойство с именем .constructor , также указывающее на «функцию, создавшую его».

Примечание: На самом деле это неправда. У a нет свойства .constructor , и хотя a.constructor действительно разрешается в функцию Foo , «конструктор» на самом деле не значит «был сконструирован этой функцией». Мы разберемся с этим курьезом чуть позже.

Ах, да, к тому же. в мире JavaScript принято соглашение об именовании «классов» с заглавной буквы, поэтому тот факт что это Foo , а не foo является четким указанием, что мы хотим определить «класс». Это ведь абсолютно очевидно, не так ли!?

Примечание: Это соглашение имеет такое влияние, что многие JS линтеры возмущаются когда вы вызываете new с методом, имя которого состоит из строчных букв, или не вызываете new с функцией, начинающейся с заглавной буквы. Удивительно, что мы с таким трудом пытаемся добиться (фальшивой) «класс-ориентированности» в JavaScript, что даже создаем правила для линтеров, чтобы гарантировать использование заглавных букв, хотя заглавные буквы вообще ничего не значат для движка JS.

Конструктор или вызов?

В примере выше есть соблазн предположить, что Foo — это «конструктор», потому что мы вызываем её с new и видим, что она «конструирует» объект.

В действительности, Foo такой же «конструктор», как и любая другая функция в вашей программе. Функции сами по себе не являются конструкторами. Однако когда вы добавляете ключевое слово new перед обычным вызовом функции, это превращает вызов функции в «вызов конструктора». На самом деле new как бы перехватывает любую обычную функцию и вызывает её так, что в результате создается объект, а также выполняется код самой функции.

NothingSpecial — обычная функция, но когда она вызывается с new , то практически в качестве побочного эффекта создает объект, который мы присваиваем a . Этот вызов был вызовом конструктора, но сама по себе функция NothingSpecial не является конструктором.

Иначе говоря, в JavaScript «конструктор» — это любая функция, вызванная с ключевым словом new перед ней.

Функции не являются конструкторами, но вызовы функций являются «вызовами конструктора» тогда и только тогда, когда используется new .

Являются ли эти особенности единственными причинами многострадальных дискуссий о «классах» в JavaScript?

Не совсем. JS разработчики постарались симулировать поведение классов настолько, насколько это возможно:

Этот пример показывает два дополнительных трюка для «класс-ориентированности»:

this.name = name : свойство .name добавляется в каждый объект ( a и b , соответственно; см. главу 2 о привязке this ), аналогично тому как экземпляры классов инкапсулируют значения данных.

Foo.prototype.myName = . : возможно более интересный прием, добавляет свойство (функцию) в объект Foo.prototype . Теперь работает a.myName() , но каким образом?

В примере выше велик соблазн думать, что при создании a и b свойства/функции объекта Foo.prototype копируются в каждый из объектов a и b . Однако этого не происходит.

В начале этой главы мы изучали ссылку [[Prototype]] — часть стандартного алгоритма [[Get]] , которая предоставляет запасной вариант поиска, если ссылка на свойство отсутствует в самом объекте.

В силу того, как создаются a и b , оба объекта получают внутреннюю ссылку [[Prototype]] на Foo.prototype . Когда myName не находится в a или b соответственно, она обнаруживается (через делегирование, см. главу 6) в Foo.prototype .

И снова о «конструкторе»

Вспомните наше обсуждение свойства .constructor . Кажется, что a.constructor === Foo означает, что в a есть реальное свойство .constructor , указывающее на Foo , верно? Не верно.

Это всего лишь путаница. На самом деле ссылка .constructor также делегируется вверх по цепочке в Foo.prototype , у которого, так уж случилось, по умолчанию есть свойство .constructor , указывающее на Foo .

Кажется ужасно удобным, что у объекта a , «созданного» Foo , будет доступ к свойству .constructor , которое указывает на Foo . Но это ложное чувство безопасности. Лишь по счастливой случайности a.constructor указывает на Foo через делегирование [[Prototype]] по умолчанию. На самом деле есть несколько способов наломать дров, предполагая что .constructor означает «использовался для создания».

Начнем с того, что свойство .constructor в Foo.prototype по умолчанию есть лишь у объекта, создаваемого в момент объявления функции Foo . Если создать новый объект и заменить у функции ссылку на стандартный объект .prototype , то новый объект по умолчанию не получит свойства .constructor .

Object(..) не был «конструктором» a1 , не так ли? Выглядит так, будто «конструктором» объекта должна быть Foo() . Многие разработчики думают, что Foo() создает объект, но эта идея трещит по швам, когда вы думаете что «constructor» значит «был создан при помощи». Ведь в таком случае a1.constructor должен указывать на Foo , но это не так!

Что же происходит? У a1 нет свойства .constructor , поэтому он делегирует вверх по цепочке [[Prototype]] к Foo.prototype . Но и у этого объекта нет .constructor (в отличие от стандартного объекта Foo.prototype !), поэтому делегирование идет дальше, на этот раз до Object.prototype — вершины цепочки делегирования. У этого объекта действительно есть .constructor , который указывает на встроенную функцию Object(..) .

Разрушаем заблуждение.

Конечно, можно вернуть объекту Foo.prototype свойство .constructor , но это придется сделать вручную, особенно если вы хотите, чтобы свойство соответствовало стандартному поведению и было не перечислимым (см. главу 3).

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

Факт в том, что .constructor объекта по умолчанию указывает на функцию, которая, в свою очередь, имеет обратную ссылку на объект — ссылку .prototype . Слова «конструктор» и «прототип» лишь наделяются по умолчанию ненадежным смыслом, который позднее может оказаться неверным. Лучше всего постоянно напоминать себе, что «конструктор не значит используется для создания».

.constructor — это не магическое неизменяемое свойство. Оно является, неперечисляемым (см. пример выше), но его значение доступно для записи (может быть изменено), и более того, вы можете добавить или перезаписать (намеренно или случайно) свойство с именем constructor в любом объекте любой цепочки [[Prototype]] , задав ему любое подходящее вам значение.

В силу того как алгоритм [[Get]] обходит цепочку [[Prototype]] , ссылка на свойство .constructor , найденная в любом узле цепочки, может получать значение, весьма отличающееся от ожидаемого.

Видите, насколько произвольным является его значение?

Что в итоге? Некая произвольная ссылка на свойство объекта, например a1.constructor , не может считаться надежной ссылкой на функцию по умолчанию. Более того, как мы вскоре увидим, в результате небольшого упущения a1.constructor может вообще указывать на весьма странное и бессмысленное значение.

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

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

На самом деле мы уже видели механизм под названием «прототипное наследование», когда объект a «унаследовал от» Foo.prototype функцию myName() . Но обычно под «наследованием» подразумевается отношение между двумя «классами», а не между «классом» и «экземпляром».

Мы уже видели эту схему, на которой показано не только делегирование от объекта (или «экземпляра») a1 к объекту Foo.prototype , но и от Bar.prototype к Foo.prototype , что отчасти напоминает концепцию наследования классов родитель-потомок. Вот только направление стрелок здесь другое, поскольку изображены делегирующие ссылки, а не операции копирования.

Вот типичный пример кода в «прототипном стиле», где создаются такие ссылки:

Примечание: Чтобы понять, почему this указывает на a , см. главу 2.

Самая важная строка здесь Bar.prototype = Object.create( Foo.prototype ) . Object.create(..) создает «новый» объект из ничего, и связывает внутреннюю ссылку [[Prototype]] этого объекта с указанным объектом (в данном случае Foo.prototype ).

Другими словами, эта строка означает: «создать новый объект ‘Bar точка prototype’, связанный с ‘Foo точка prototype'».

При объявлении function Bar() < .. >функция Bar , как и любая другая, получает ссылку .prototype на объект по умолчанию. Но этот объект не ссылается на Foo.prototype , как мы того хотим. Поэтому мы создаем *новый** объект, который *имеет* нужную ссылку, и отбрасываем исходный, неправильно связанный объект.

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

Bar.prototype = Foo.prototype не создает новый объект, на который ссылалось бы Bar.prototype . Вместо этого Bar.prototype становится еще одной ссылкой на Foo.prototype , и в результате Bar напрямую связывается с тем же самым объектом, что и Foo : Foo.prototype . Это значит, что когда вы начнете присваивать значения, например Bar.prototype.myLabel = . , вы будете изменять не отдельный объект, а общий объект Foo.prototype , что повлияет на любые объекты, привязанные к Foo.prototype . Это наверняка не то, чего вы хотите. В противном случае вам вообще не нужен Bar , и стоит использовать только Foo , сделав код проще.

Bar.prototype = new Foo() действительно создает новый объект, корректно привязанный к Foo.prototype . Но для этого используется «вызов конструктора» Foo(..) . Если эта функция имеет какие-либо побочные эффекты (логирование, изменение состояния, регистрация в других объектах, добавление свойств в this , и т.д.), то эти побочные эффекты сработают во время привязывания (и возможно в отношении неправильного объекта!), а не только при создании конечных «потомков» Bar , как можно было бы ожидать.

Поэтому, для правильного привязывания нового объекта без побочных эффектов от вызова Foo(..) у нас остается лишь Object.create(..) . Небольшой недостаток состоит в том, что нам приходится создавать новый объект и выбрасывать старый, вместо того чтобы модифицировать существующий стандартный объект.

Было бы здорово если бы существовал стандартный и надежный способ поменять привязку существующего объекта. До ES6 был нестандартный и не полностью кроссбраузерный способ через свойство .__proto__ , которое можно изменять. В ES6 добавлена вспомогательная утилита Object.setPrototypeOf(..) , которая проделывает нужный трюк стандартным и предсказуемым способом.

Сравните пред-ES6 и стандартизованный в ES6 способ привязки Bar.prototype к Foo.prototype :

Если отбросить небольшой проигрыш в производительности (выбрасывание объекта, который позже удаляется сборщиком мусора), то способ с Object.create(..) немного короче и может даже читабельнее, чем подход ES6+. Хотя это всего лишь пустые разговоры о синтаксисе.

Инспектируем связи между «классами»

Что если у вас есть объект a и вы хотите выяснить, какому объекту он делегирует? Инспектирование экземпляра (объект в JS) с целью найти его предка (делегирующая связь в JS) в традиционных класс-ориентированных языках часто называют интроспекцией (или рефлексией).

Как выполнить интроспекцию a , чтобы найти его «предка» (делегирующую связь)? Первый подход использует путаницу с «классами»:

Оператор instanceof принимает в качестве операнда слева обычный объект, а в качества операнда справа — функцию. instanceof отвечает на следующий вопрос: присутствует ли где-либо в цепочке [[Prototype]] объекта a объект, на который указывает Foo.prototype ?

К сожалению, это значит, что вы можете получить сведения о «происхождении» некоторого объекта ( a ) только имея некоторую функцию ( Foo c её ссылкой .prototype ). Если у вас есть два произвольных объекта, например a и b , и вы хотите узнать, связаны ли сами эти объекты друг с другом через цепочку [[Prototype]] , одного instanceof будет недостаточно.

Примечание: Если вы используете встроенную утилиту .bind(..) для создания жестко привязанной функции (см. главу 2), то у созданной функции не будет свойства .prototype . При использовании instanceof с такой функцией прозрачно подставляется .prototype целевой функции, из которой была создана жестко привязанная функция.

Использование функции с жесткой привязкой для «вызова конструктора» крайне маловероятно, но если вы сделаете это, то она будет вести себя так, как если бы вы вызвали целевую функцию. Это значит, что вызов instanceof с жестко привязанной функцией также ведет себя в соответствии с оригинальной функцией.

Этот фрагмент кода показывает нелепость попыток рассуждать об отношениях между двумя объектами используя семантику «классов» и instanceof :

Внутри isRelatedTo(..) мы временно используем функцию F , меняя значение её свойства .prototype на объект o2 , а затем спрашиваем, является ли o1 «экземпляром» F . Ясно, что o1 на самом деле не унаследован от и даже не создан с помощью F , поэтому должно быть понятно, что подобные приемы бессмысленны и сбивают с толку. Проблема сводится к несуразности семантики классов, навязываемой JavaScript, что наглядно показано на примере косвенной семантики instanceof .

Второй подход к рефлексии [[Prototype]] более наглядный:

Заметьте, что в этом случае нам неинтересна (и даже не нужна) Foo . Нам просто нужен объект (в данном случае произвольный объект с именем Foo.prototype ) для сопоставления его с другим объектом. isPrototypeOf(..) отвечает на вопрос: присутствует ли где-либо в цепочке [[Prototype]] объекта a объект Foo.prototype ?

Тот же вопрос, и в точности такой же ответ. Но во втором подходе нам не нужна функция ( Foo ) для косвенного обращения к её свойству .prototype .

Нам просто нужны два объекта для выявления связи между ними. Например:

Заметьте, что в этом подходе вообще не требуется функция («класс»). Используются прямые ссылки на объекты b и c , чтобы выяснить нет ли между ними связи. Другими словами, наша утилита isRelatedTo(..) уже встроена в язык и называется isPrototypeOf(..) .

Мы можем напрямую получить [[Prototype]] объекта. В ES5 появился стандартный способ сделать это:

Здесь видно, что ссылка на объект является тем, что мы и ожидаем:

В большинстве браузеров (но не во всех!) давно добавлена поддержка нестандартного альтернативного способа доступа к [[Prototype]] :

Загадочное свойство .__proto__ (стандартизовано лишь в ES6!) «магически» возвращает ссылку на внутреннее свойство [[Prototype]] объекта, что весьма полезно, если вы хотите напрямую проинспектировать (или даже обойти: .__proto__.__proto__. ) цепочку.

Аналогично рассмотренному ранее .constructor , свойство .__proto__ отсутствует у инспектируемого объекта ( a в нашем примере). На самом деле оно есть (и является неперечисляемым, см. главу 2) у встроенного Object.prototype , наряду с другими известными утилитами ( .toString() , .isPrototypeOf(..) , и т.д.).

Более того, .__proto__ выглядит как свойство, но правильнее думать о нем как о геттере/сеттере (см. главу 3).

Грубо говоря, можно представить, что .__proto__ реализовано так (см. главу 3 об определениях свойств объекта):

Таким образом, когда мы обращаемся (получаем значение) к a.__proto__ , это похоже на вызов a.__proto__() (вызов функции геттера). В *этом** вызове функции this указывает на a , несмотря на то что функция геттера находится в объекте Object.prototype (см. главу 2 о правилах привязки this ), так что это равносильно Object.getPrototypeOf( a ) .

Значение .__proto__ можно также изменять, например с помощью функции Object.setPrototypeOf(..) в ES6, как показано выше. Однако обычно не следует изменять [[Prototype]] существующего объекта.

Глубоко внутри некоторых фреймворков можно встретить сложные, продвинутые механизмы, позволяющие выполнять трюки наподобие «создания производного класса» от Array , но обычно подобные вещи не поощряются в повседневной практике, поскольку такой код гораздо труднее понимать и сопровождать.

Примечание: В ES6 добавлено ключевое слово class , с помощью которого можно делать вещи, напоминающие «создание производных классов» от встроенных объектов, таких как Array . Обсуждение синтаксиса class из ES6 см. в Приложении А.

В остальном же единственным исключением будет установка свойства [[Prototype]] стандартного объекта .prototype функции, чтобы оно ссылалось на какой-то другой объект (помимо Object.prototype ). Это позволит избежать замены этого стандартного объекта новым объектом. В любой другой ситуации лучше всего считать ссылку [[Prototype]] доступной только для чтения, чтобы облегчить чтение вашего кода в будущем.

Примечание: В сообществе JavaScript неофициально закрепилось название двойного подчеркивания, особенно перед именами свойств (как __proto__ ): «dunder». Поэтому в мире JavaScript «крутые ребята» обычно произносят __proto__ как «dunder proto».

Как мы уже видели, механизм [[Prototype]] является внутренней ссылкой, существующей у объекта, который ссылается на другой объект.

Переход по этой ссылке выполняется (в основном) когда происходит обращение к свойству/методу первого объекта, и это свойство/метод отсутствует. В таком случае ссылка [[Prototype]] указывает движку, что свойство/метод нужно искать в связанном объекте. Если и в этом объекте ничего не находится, то происходит переход по его ссылке [[Prototype]] , и так далее. Эта последовательность ссылок между объектами образует то, что называется «цепочкой прототипов».

Мы подробно объяснили, почему механизм [[Prototype]] в JavaScript не похож на «классы», и увидели, что вместо этого создаются ссылки между подходящими объектами.

В чем смысл механизма [[Prototype]] ? Почему многие JS разработчики прикладывают так много усилий (эммулируя классы), чтобы настроить эти ссылки?

Помните, как в начале этой главы мы сказали, что Object.create(..) станет нашим героем? Теперь вы готовы это увидеть.

Object.create(..) создает новый объект ( bar ), связанный с объектом, который мы указали ( foo ), и это дает нам всю мощь (делегирование) механизма [[Prototype]] , но без ненужных сложностей вроде функции new , выступающей в роли классов и вызовов конструктора, сбивающих с толку ссылок .prototype и .constructor , и прочих лишних вещей.

Примечание: Object.create(null) создает объект с пустой (или null ) ссылкой [[Prototype]] , поэтому этот объект не сможет ничего делегировать. Поскольку у такого объекта нет цепочки прототипов, оператору instanceof (рассмотренному ранее) нечего проверять, и он всегда вернет false . Эти специальные объекты с пустым [[Prototype]] часто называют «словарями», поскольку они обычно используются исключительно для хранения данных в свойствах, потому что у них не может быть никаких побочных эффектов от делегируемых свойств/функций цепочки [[Prototype]] , и они являются абсолютно плоскими хранилищами данных.

Для создания продуманных связей между двумя объектами нам не нужны классы. Нам нужно только лишь связать объекты друг с другом для делегирования, и Object.create(..) дает нам эту связь без лишней возни с классами.

Полифилл для Object.create()

Object.create(..) была добавлена в ES5. Вам может понадобиться поддержка пред-ES5 окружения (например, старые версии IE), поэтому давайте рассмотрим простенький частичный полифилл для Object.create(..) :

В этом полифилле используется временно создаваемая функция F , и её свойство .prototype переопределяется так, чтобы указывать на объект, с которым нужно создать связь. Затем мы используем new F() , чтобы создать новый объект, который будет привязан нужным нам образом.

Такой вариант использования Object.create(..) встречается в подавляющем большинстве случаев, поскольку эту часть можно заменить полифиллом. В ES5 стандартная функция Object.create(..) предоставляет дополнительную функциональность, которую нельзя заменить полифиллом в пред-ES5. Для полноты картины рассмотрим, в чем она заключается:

Второй аргумент Object.create(..) указывает свойства, которые будут добавлены в создаваемый объект, объявляя дескриптор каждого нового свойства (см. главу 3). Поскольку полифиллинг дескрипторов свойств в пред-ES5 невозможен, эту дополнительную функциональность Object.create(..) также невозможно реализовать в виде полифилла.

В большинстве случаев используется лишь та часть функциональности Object.create(..) , которую можно заменить полифиллом, поэтому большинство разработчиков устраивает использование частичного полифилла в пред-ES5 окружениях.

Некоторые разработчики придерживаются более строгого подхода, считая, что можно использовать только полные полифиллы. Поскольку Object.create(..) — одна из тех утилит, что нельзя полностью заменить полифиллом, такой строгий подход предписывает, что если вы хотите использовать Object.create(..) в пред-ES5 окружении, то вместо полифилла следует применить собственную утилиту, и вообще воздержаться от использования имени Object.create . Вместо этого можно определить свою собственную утилиту:

Я не разделяю такой подход. Меня полностью устраивает показанный выше частичный полифилл Object.create(..) и его использование в коде даже в пред-ES5. Решайте сами, какой подход вам ближе.

Ссылки в роли запасных свойств?

Существует соблазн думать, что эти ссылки между объектами в основном предоставляют что-то вроде запасного варианта на случай «отсутствующих» свойств или методов. И хотя такой вывод допустим, я не считаю что это верный способ размышления о [[Prototype]] .

Этот код работает благодаря [[Prototype]] , но если вы написали его так, что anotherObject играет роль запасного варианта на случай если myObject не сможет обработать обращение к некоторому свойству/методу, то вероятно ваше ПО будет содержать больше «магии» и будет сложнее для понимания и сопровождения.

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

Примечание: В ES6 добавлена продвинутая функциональность, называемая Proxy , с помощью которой можно реализовать что-то наподобие поведения «отсутствующих методов». Proxy выходит за рамки этой книги, но будет подробно рассмотрена в одной из следующих книг серии «Вы не знаете JS».

Не упустите одну важную, но едва уловимую мысль.

Явно проектируя ПО таким образом, что разработчик может вызвать, к примеру, myObject.cool() , и это будет работать даже при отсутствии метода cool() у myObject , вы добавляете немного «магии» в дизайн вашего API, что может в будущем преподнести сюрприз другим разработчикам, которые будут поддерживать ваш код.

Но вы можете спроектировать API и без подобной «магии», не отказываясь при этом от преимуществ ссылки [[Prototype]] .

Здесь мы вызываем myObject.doCool() — метод, который действительно есть у объекта myObject , делая наш API более явным (менее «магическим»). Внутри наша реализация следует шаблону делегирования (см. главу 6), используя делегирование [[Prototype]] к anotherObject.cool() .

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

При попытке обратиться к несуществующему свойству объекта внутренняя ссылка [[Prototype]] этого объекта задает дальнейшее направление поиска для операции [[Get]] (см. главу 3). Этот каскад ссылок от объекта к объекту образует «цепочку прототипов» (чем то похожую на цепочку вложенных областей видимости) для обхода при разрешении свойства.

У обычных объектов есть встроенный объект Object.prototype на конце цепочки прототипов (похоже на глобальную область видимости при поиске по цепочке областей видимости), где процесс разрешения свойства остановится, если свойство не будет найдено в предыдущих звеньях цепочки. У этого объекта есть утилиты toString() , valueOf() и несколько других, благодаря чему все объекты в языке имеют доступ к ним.

Наиболее популярный способ связать два объекта друг с другом — использовать ключевое слово new с вызовом функции, что помимо четырех шагов (см. главу 2) создаст новый объект, привязанный к другому объекту.

Этим «другим объектом» является объект, на который указывает свойство .prototype функции, вызванной с new . Функции, вызываемые с new , часто называют «конструкторами», несмотря на то что они не создают экземпляры классов, как это делают конструкторы в традиционных класс-ориентированных языках.

Хотя эти механизмы JavaScript могут напоминать «создание экземпляров классов» и «наследование классов» из традиционных класс-ориентированных языков, ключевое отличие в том, что в JavaScript не создаются копии. Вместо этого объекты связываются друг с другом через внутреннюю цепочку [[Prototype]] .

По множеству причин, среди которых не последнюю роль играет терминологический прецедент, «наследование» (и «прототипное наследование») и все остальные ОО-термины не имеют смысла, учитывая то как на самом деле работает JavaScript.

Более подходящим термином является «делегирование», поскольку эти связи являются не копиями, а делегирующими ссылками.

Объектно-ориентированное программирование в JavaScript. Прототипы

Объектно-ориентированное программирование – способ создания контента, базирующийся на разнообразных элементах. Их можно визуализировать, что значительно упрощает программирование. Логика и «голый код» здесь не выступают в качестве основополагающих.

JavaScript – это скриптовый язык программирования. Используется для разнообразных целей – от написания аналитических утилит до игр. Основное его применение – это веб-утилиты и браузерный контент. Но элементы ООП в нем все равно присутствуют.

В данной статье будет рассказано о том, что такое прототипы, как ими пользоваться. А еще – раскрыты ключевые особенности JS как способа ООП. Полученные знания пригодятся всем новичкам-разработчикам.

ООП – что такое

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

Общий объект – это план. Может также носить название проекта или схемы. Создаваемые посредством оного элементы – экземпляры.

Аспекты

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

  1. Каждый экземпляр класса обладает свойствами, которые наследуются от родителей. Также есть собственные параметры.
  2. Структурированный код предусматривает возможность размещения нескольких уровней в проекте. Процедура носит название наследования или классификации. Во втором случае целесообразно говорить о создании подклассов.
  3. Инкапсуляция помогает скрывать детали реализации кодификации от сторонних глаз. Это приводит к тому, что функции и переменные, а также иные важные объекты приложения становятся не доступными извне. Таковая концепция шаблонов проектирования «Фасад» и «Модуль».

Если хотите выучить JavaScript и его особенности, стоит изначально обратить внимание на общие сведения. А именно – терминологию. Она едина для всех языков программирования. Помогает не запутаться при углубленном изучении тех или иных элементов, параметров и функций.

Чуть-чуть терминологии – ключевые понятия

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

  • алгоритм – набор принципов и правил, которые применяются программером для решения поставленной ранее задачи;
  • программа – код, который обработан и реализован устройством;
  • объект – набор связанных переменных, констант и иных структур информации, которая выбирается и обрабатывается совместно;
  • класс – связанные между собой объекты с общими свойствами;
  • интерфейс командной строки – интерфейс пользовательского типа, базирующийся на основе текста;
  • компиляция – процедура создания исполняемого приложения через код, написанный на скомпилированном языке программирования;
  • константа – неизменная;
  • тип данных – способ классификации информации того или иного характера;
  • массив – группа или список схожих типов значений информации, которые можно группировать;
  • итерация – один проход через некий заранее определенный набор операций кода;
  • ключевое слово – слово, которое зарезервировано языком программирования для описания специальных объектов/функций/операций/команд;
  • оператор – элемент, который способен управлять разнообразными операндами;
  • операнд – объект, подлежащий манипулированию через специальные команды – операторы;
  • переменные – хранилища информации в приложении;
  • пакет – модуль связанных интерфейсов и классов.

Этого новичкам будет более чем достаточно. Теперь можно рассмотреть объекты в JavaScript более подробно, особое внимание уделив таким элементам, как прототипы.

Информация об объектах – что и как

Java Script – это скриптовый язык программирования, который предусматривает весьма мощный функционал. Если реализовывать его грамотно и правильно, можно создавать не только небольшие веб-проекты, но и решать сложные масштабные задачи.

Немаловажную роль в процессе коддинга играют объекты. Существуют различные способы их создания. А именно:

  • функция – конструктор;
  • класс;
  • связывание уже имеющихся объектов;
  • фабричная функция.

Каждый вариант обладает собственными преимуществами и недостатками. Все перечисленные приемы будут рассмотрены ниже. Они тесно связаны с прототипами и наследованием.

Функция – конструктор

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

This дает возможность получения доступа и сохранения уникальных значений создаваемых экземпляров. Экземпляр можно «добавить» посредством ключевика new.

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

Классы в помощь

Следующий вариант развития событий – это использование классов. Они в JS выступают в качестве абстракций (неким «синтаксическим сахаром) над функциями-конструкторами. Посредством соответствующих элементов удается быстрее и проще создать экземпляры:

Стоит обратить внимание на следующие моменты:

  1. Constructor имеет такой же код, как и функция-конструктор, рассмотренная ранее. Такой прием необходим для инициализации this.
  2. Опустить constructor можно, если не нужно присваивать начальные значения.
  3. Добавление экземпляров происходит тоже при помощи ключевого слова new.

Изначально может показаться, что работать с функциями-конструкторами проще, но это не так. Классы в JS имеют немало сильных сторон. Они пригодятся любому разработчику при создании контента.

Связка

Третий подход к созданию объектов в Java Script – это использование связывания ранее имеющихся в кодификации оставляющих. Метод был предложен Кейли Симпсон. Здесь проект определяется в виде обычного объекта. Далее за счет метода (он чаще всего носит название init) происходит инициализация экземпляра.

Для того, чтобы создать экземпляр, необходимо применить Object.create. После реализации задачи происходит вызов init.

Для того, чтобы улучшить исходную кодификацию, можно использовать возврат this в init.

Но это еще не все. Посмотрим на еще один, последний подход к созданию объектов в JS.

Фабричные функции

Так называют функции, которые осуществляют возврат объекта. Возможно применение ко всем элементам программного кода. Допускается возврат экземпляра класса или связывания объектов.

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

Теперь можно выяснить, как создавать свойства и методы. Без этого не удастся получать качественный собственный контент.

О методах и свойствах

Метод – функция, которая объявлена в качестве свойства того или иного объекта в кодификации JS.

Определений свойств и методов в объектно-ориентированном программировании несколько. А именно:

  • через экземпляры;
  • путем работы с прототипом.

Когда и какой вариант использовать, должен знать каждый разработчик. Особенно если программер хочет создавать собственные игры и сложные утилиты. Это поможет ускорить работу кодификации.

Прототип – это что

Прототип – элемент JS, который позволяет другим составляющих кода наследовать свойства и методы. Изначально каждый объект обладает собственным прототипом. Если у искомого элемента не хватает параметров и свойств, они будут искаться в prototype. Когда у ближайшего прототипа соответствующие характеристики отсутствуют, поиск переходит далее по иерархии – ниже. Описанный принцип – наследование прототипов в JS.

Управление соответствующими составляющими не слишком трудное. В самом верху цепочки прототипов расположен последний (стандартный объект ДжаваСкрипт). Выше него совершенно ничего нет.

Прототип объекта

Стоит обратить внимание на очень важные моменты программирования объектно-ориентированного характера. А именно – на prototype. Он может относиться к функции или объекту.

Во втором случае рекомендуется рассмотреть наглядный пример:

Здесь происходит следующее:

  1. Для того, чтобы увидеть __photo__ в консоли разработчика, создается составляющая под названием «машина».
  2. В консоли выводится тип данных – object. Внутри него расположена ссылка __photo__.
  3. Последняя приведет к прототипу объекта car.
  4. Внутри ссылки отображается вся цепочка prototypes. Она рано или поздно получит значение null (пусто, ничего).

Описанный принцип очень хорошо показывает принципы и механизмы наследования прототипов в ДжаваСкрипт. На практике подобные приемы практически не встречаются – в них нет смысла. Программеры чаще всего прописывают свойства вручную.

Прототипы функций

А вот еще один весьма важный момент – прототипы функций. У каждой подобной составляющей есть свойство под названием prototype.

Здесь происходит следующее:

  1. Создается новый элемент user вторым рассматриваемым способом.
  2. Внутри соответствующей составляющей прописываются два свойства и одна функция.
  3. Последняя отвечает за вывод в консоль строчки Privet.
  4. Теперь в консоль нужно вывести объект user с ранее знакомой ссылкой __photo__.
  5. Если открыть соответствующую ссылку, среди предложенных методов отобразится toString. Это значит, что метод перешел к «юзеру» посредством наследования.

У всех новых элементов всегда есть прототип. Вот пример обращения к свойству prototype глобального класса Object. Также предложенный ниже код поможет создать новое поле – функцию sayHello.

В консоли необходимо вызвать новую функцию у user. Ошибок не будет. Функция пройдет обработку, после чего выведет на экран Hello.

Стоит обратить внимание на то, что у user изначально не было функции sayHello. Она возникла у родителя оного. Отсюда следует, что user получил ее в качестве «наследства» от родителя – прототипа Object.

Определение свойств и методов в конструкторе

После того, как нашли полезную информацию о прототипах и изучили ее, можно рассмотреть определение свойств и методов JS более подробно. Первый вариант – в конструкторе.

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

Управление методами определяется непосредственно в прототипах. Этот прием помогает избежать создания функций для каждого экземпляра. Для всех подобных составляющих можно применять одну функцию. Она носит название общей или распределенной.

Добавление свойства в прототипы предусматривает использование prototype.

Несколько методов бывает не так легко добавить. Этот процесс отнимает немало времени. Все зависит от того, сколько именно подобных составляющих требуется добавить в кодификацию.

Облегчить управление методами (добавление оных) удается через Object.assign.

Выше представлен код, который делает манипулирование созданием методов и свойств более быстрым и простым.

Свойства и методы в классе

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

Здесь необходимо запомнить следующую информацию:

  • Свойства экземпляра определяются в constructor.
  • Свойства прототипа будут определяться после конструктора в качестве «обычной» функции.
  • Создавать несколько методов в классе проще, чем через конструктор. Для этого не нужно использоваться Object.assign. Достаточно просто произвести добавление иных функций.

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

Свойства и методы при связывании элементов

Пока утилита находится в стадии разработки, важно определиться с тем, как прописывать свойства и методы. Есть вариант «при связывании элементов кодификации».

Для того, чтобы определить свойства экземпляра, требуется добавить его к this.

Метод прототипа будет определяться в качестве обычного объекта.

Кодификация выше демонстрирует принцип реализации оного.

Определение в фабричных функциях

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

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

Выше представлен пример кода, который лучше не использовать, выполняя различные задания по разработке.

Как быстро выучить материал

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

Такой вариант имеет немало преимуществ. Среди них выделяют:

  • удобство – можно совмещать с семьей, работой или «основной» учебой;
  • доступность;
  • дистанционная организация образовательного процесса;
  • сопровождение опытными разработчиками-кураторами;
  • море практики;
  • хорошо составленная программа;
  • наличие предложений для пользователей с разным уровнем знаний и навыков;
  • возможность сконцентрироваться на одном или нескольких направлениях/языках.

Прохождение курсов по JS позволит получить электронный сертификат, подтверждающий знания. Программы рассчитаны на срок до года.

Объектно-ориентированный JavaScript

Хотя JavaScript не является языком, основанным на классах, и не реализует объектно-ориентированное программирование в традиционном смысле, он предоставляет возможности и шаблоны, которые позволяют использовать концепции объектно-ориентированного программирования. Это можно назвать прототипным наследованием.

Что такое прототип в JavaScript?

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

В JavaScript прототип можно рассматривать как проект, шаблон или базовый объект, содержащий методы, к которым может обратиться любой созданный экземпляр объекта. Если бы не было прототипа, то пришлось бы определять методы для каждого отдельного экземпляра объекта. Этот процесс неэффективен и занимает много памяти.

Разберем два прототипа: свойство prototype и сам прототип объекта. Начнем со свойства prototype .

Свойство prototype

Когда мы используем методы JavaScript, такие как pop в отношении массива или include при работе со строками, мы применяем встроенные методы JavaScript. Рассмотрим это на примере. Начнем с простого доступа к свойству prototype в конструкторе прототипов Array .

Теперь создадим массив и посмотрим, как можно просматривать свойство prototype внутри массива.

В приведенном выше примере переменная ourArray объявляется и инициализируется массивом, содержащим значения 1, 2 и 3. Позже нужно будет рассмотреть массив ourArray . Чтобы удалить элемент из конца массива, можно использовать метод pop . Но откуда он берется? Взглянем на массив ourArray еще раз.

Каждый объект в JavaScript имеет свойство [[Prototype]] . Это внутреннее свойство, которое обозначается двойными скобками, в которые оно заключено. Выбрав свойство [[Prototype]] , мы видим доступные методы, которые в данном примере наследуются от конструктора Array . Мы также можем использовать метод getPrototypeOf , который вернет свойство [[Prototype]] данного объекта (помните, что массивы — это особый тип объектов).

Вы также можете встретить примеры, в которых используется свойство __proto__ . Это унаследованная функция, и в eslint есть правило (no-proto), защищающее от ее использования. Свойство __proto__ раскрывает внутренний [[Prototype]] объекта. Лучше использовать getPrototypeOf , как показано ниже.

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

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

Как это возможно?

Объект, который является значением свойства [[prototype]] , — это прототип для того объекта, который мы рассматриваем. Если он не существует, его значение будет равно null . Когда мы используем метод pop на ourArray , интерпретатор JavaScript сначала будет искать этот метод в ourArray . Если он не найдет его там, то будет искать в прототипе.

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

В приведенном выше коде мы объявляем функцию-конструктор под названием GameTracker с параметрами name и result . Если эта функция хранится в памяти, она хранится как функция с объектом.

У объекта есть свойство prototype , которое представляет собой пустой объект. Любые функции, которые создаются с помощью конструктора GameTracker , будут иметь доступ к свойству prototype . С его помощью внутри функции мы устанавливаем параметры name и result , чтобы они ссылались на текущий экземпляр объекта.

Далее мы создаем переменную playerOne , изначально неинициализированную. Затем с помощью ключевого слова new создается новый экземпляр GameTracker . Когда мы используем ключевое слово new , устанавливается свойство prototype , которое будет ссылкой на объект GameTracker . Если мы рассмотрим переменную playerOne , то увидим следующее:

Теперь попробуем определиться с конструктором playerOne :

Используя прототип, можно добавить метод к прототипу. Добавим метод для запуска игры:

Теперь снова посмотрим на объект playerOne . Мы видим, что функция start хранится в прототипе.

Мы можем использовать функцию start для playerOne . Попробуем это сделать:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *