Что такое прототипное наследование

Оглавление:

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

Наследование на уровне объектов в JavaScript, как мы видели, реализуется через ссылку __proto__ .

Теперь поговорим о наследовании на уровне классов, то есть когда объекты, создаваемые, к примеру, через new Admin , должны иметь все методы, которые есть у объектов, создаваемых через new User , и ещё какие-то свои.

Наследование Array от Object

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

Взглянем на него ещё раз на примере Array , который наследует от Object :

  • Методы массивов Array хранятся в Array.prototype .
  • Array.prototype имеет прототипом Object.prototype .

Поэтому когда экземпляры класса Array хотят получить метод массива – они берут его из своего прототипа, например Array.prototype.slice .

Если же нужен метод объекта, например, hasOwnProperty , то его в Array.prototype нет, и он берётся из Object.prototype .

Отличный способ «потрогать это руками» – запустить в консоли команду console.dir([1,2,3]) .

Вывод в Chrome будет примерно таким:

Здесь отчётливо видно, что сами данные и length находятся в массиве, дальше в __proto__ идут методы для массивов concat , то есть Array.prototype , а далее – Object.prototype .

Обратите внимание, я использовал именно console.dir , а не console.log , поскольку log зачастую выводит объект в виде строки, без доступа к свойствам.

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

Применим тот же подход для наших классов: объявим класс Rabbit , который будет наследовать от Animal .

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

Для того, чтобы наследование работало, объект rabbit = new Rabbit должен использовать свойства и методы из своего прототипа Rabbit.prototype , а если их там нет, то – свойства и методы родителя, которые хранятся в Animal.prototype .

Если ещё короче – порядок поиска свойств и методов должен быть таким: rabbit -> Rabbit.prototype -> Animal.prototype , по аналогии с тем, как это сделано для объектов и массивов.

Для этого можно поставить ссылку __proto__ с Rabbit.prototype на Animal.prototype .

Можно сделать это так:

Однако, прямой доступ к __proto__ не поддерживается в IE10-, поэтому для поддержки этих браузеров мы используем функцию Object.create . Она либо встроена либо легко эмулируется во всех браузерах.

Класс Animal остаётся без изменений, а Rabbit.prototype мы будем создавать с нужным прототипом, используя Object.create :

Теперь выглядеть иерархия будет так:

В prototype по умолчанию всегда находится свойство constructor , указывающее на функцию-конструктор. В частности, Rabbit.prototype.constructor == Rabbit . Если мы рассчитываем использовать это свойство, то при замене prototype через Object.create нужно его явно сохранить:

Полный код наследования

Для наглядности – вот итоговый код с двумя классами Animal и Rabbit :

Как видно, наследование задаётся всего одной строчкой, поставленной в правильном месте.

Обратим внимание: Rabbit.prototype = Object.create(Animal.prototype) присваивается сразу после объявления конструктора, иначе он перезатрёт уже записанные в прототип методы.

В некоторых устаревших руководствах предлагают вместо Object.create(Animal.prototype) записывать в прототип new Animal , вот так:

Частично, он рабочий, поскольку иерархия прототипов будет такая же, ведь new Animal – это объект с прототипом Animal.prototype , как и Object.create(Animal.prototype) . Они в этом плане идентичны.

Но у этого подхода важный недостаток. Как правило мы не хотим создавать Animal , а хотим только унаследовать его методы!

Более того, на практике создание объекта может требовать обязательных аргументов, влиять на страницу в браузере, делать запросы к серверу и что-то ещё, чего мы хотели бы избежать. Поэтому рекомендуется использовать вариант с Object.create .

Вызов конструктора родителя

Посмотрим внимательно на конструкторы Animal и Rabbit из примеров выше:

Как видно, объект Rabbit не добавляет никакой особенной логики при создании, которой не было в Animal .

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

Такой вызов запустит функцию Animal в контексте текущего объекта, со всеми аргументами, она выполнится и запишет в this всё, что нужно.

Здесь можно было бы использовать и Animal.call(this, name) , но apply надёжнее, так как работает с любым количеством аргументов.

Переопределение метода

Итак, Rabbit наследует Animal . Теперь если какого-то метода нет в Rabbit.prototype – он будет взят из Animal.prototype .

В Rabbit может понадобиться задать какие-то методы, которые у родителя уже есть. Например, кролики бегают не так, как остальные животные, поэтому переопределим метод run() :

Вызов rabbit.run() теперь будет брать run из своего прототипа:

Вызов метода родителя внутри своего

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

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

Обратите внимание на вызов через apply и явное указание контекста.

Если вызвать просто Animal.prototype.run() , то в качестве this функция run получит Animal.prototype , а это неверно, нужен текущий объект.

Для наследования нужно, чтобы «склад методов потомка» ( Child.prototype ) наследовал от «склада метода родителей» ( Parent.prototype ).

Это можно сделать при помощи Object.create :

Для того, чтобы наследник создавался так же, как и родитель, он вызывает конструктор родителя в своём контексте, используя apply(this, arguments) , вот так:

При переопределении метода родителя в потомке, к исходному методу можно обратиться, взяв его напрямую из прототипа:

Структура наследования полностью:

Такое наследование лучше функционального стиля, так как не дублирует методы в каждом объекте.

Кроме того, есть ещё неявное, но очень важное архитектурное отличие.

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

Иначе говоря, в функциональном стиле в процессе создания Rabbit нужно обязательно вызывать Animal.apply(this, arguments) , чтобы получить методы родителя – и если этот Animal.apply кроме добавления методов говорит: «Му-у-у!», то это проблема:

…Которой нет в прототипном подходе, потому что в процессе создания new Rabbit мы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе.

Читайте так же:  Статья 176 ук состав

Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и универсальный. А что касается красоты синтаксиса – она сильно лучше в новом стандарте ES6, которым можно пользоваться уже сейчас, если взять транслятор babeljs.

Что такое прототипное наследование

На самом деле в примере в вопросе нет наследования.

__proto__ до недавнего времени не был стандартизирован.

В последних спецификациях можно найти, что getter __proto__ , обертка над вызовом внутренней функции [[GetPrototypeOf]] .

Если применительно к приведенному коду:

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

Отсюда можно сделать вывод, что если есть доступ к конструктору, прототип лучше менять непосредственное у него.

Пример, когда приведенный код выдаст разные значения:

Свойство __proto__

Абсолютно любой объект в JavaScript имеет свойство proto. Это скрытое системное свойство, и не во всех реализациях языка оно доступно пользователю.

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

Но если его там нет, поиск происходит в свойстве proto:

Если его нет и там, оно ищется дальше по цепочке:

Эта цепочка называется цепочкой прототипов (prototype chain).

proto любого значения (кроме null и undefined) ссылается на prototype соответствующего ему типу данных:

Все типы данных наследуются от Object, это означает что:

И наконец, завершение цепочки:

Свойство prototype

Это обычное свойство, ничем не отличающиеся от любых других свойств. За исключением двух особенностей:

1) Функции в JavaScript имеют свойство prototype. Оно по умолчанию является объектом с единственным свойством constructor, которое ссылается на саму функцию.

2) Свойство prototype используется при создании новых объектов оператором new.

Этот оператор делает следущее:

Создает пустой объект:

Устанавливает proto этому объекту ссылкой на prototype функции-класса:

Применяет функцию-класс к нашему новосозданному объекту:

т.е. исполняет функцию FnClass, передавая ей instance в качестве this и аргументы в виде массива arguments.

Возвращает экземпляр функции-класса, но если FnClass нам вернул обьект, тогда его:

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

Что такое прототипное наследование

Добрый День. Изучаю способы организации наследования в JavaScript и написал небольшой пример :

Вопрос возник на строке :

Пытаясь понять разницу между

набрел на статью, в которой говориться

Bar.prototype = Foo.prototype doesn’t create a new object for Bar.prototype to be linked to. It just makes Bar.prototype be another reference to Foo.prototype, which effectively links Bar directly to the same object as Foo links to: Foo.prototype. This means when you start assigning, like Bar.prototype.myLabel = . you’re modifying not a separate object but the shared Foo.prototype object itself, which would affect any objects linked to Foo.prototype.

Вопрос заключается в последнем предложении. Почему при добавлении прототипу свойства Bar, мы автоматически меняем и прототип объекта Foo ? Если я правильно понял, то как раз при добавлении свойства или метода в объект Foo, должен измениться и объект Bar, т.к. он ссылается на прототип Foo. Помогите разобраться пожалуйста.

Давайте начнем с отвлеченного примера:

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

вы присваиваете свойству Bar.prototype ссылку на объект Foo.prototype . Как следствие, любое изменение свойства Bar.prototype приводит к изменению Foo.prototype , о чем и говорится в приведнной цитате:

This means when you start assigning, like Bar.prototype.myLabel = . you’re modifying not a separate object but the shared Foo.prototype object itself, which would affect any objects linked to Foo.prototype.

Небольшое лирическое отступление.

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

а всех тех, кто вам это советует — смело отправляйте учить основы JS. Вся соль в том, что вызывая new Foo() вы вызываете конструктор объекта. При этом сам конструктор может с одной стороны накладывать ограничения на передаваемые аргументы, а с другой иметь побочные действия. Разберем каждый из этих случаев отдельно.

Предположим, у вас есть вот такой конструктор, накладывающий ограничения на свои аргументы:

В этом случае вы уже не можете просто взять и выполнить:

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

Теперь предположим, что родительский конструктор имеет побочные эффекты:

строка » Here I am! » будет выведена даважды. Согласитесь, это не всегда желаемое поведение системы.

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

Приведу, для справки, правильную реализацию наследования в JS:

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

С учетом всего выше сказанного универсальная функции наследования может иметь вид:

Что такое прототипное наследование

Сначала я думал, что разобрался с прототипным наследованием в JS (ведь оно такое простое), а теперь мне кажется, что я не понимаю, зачем все это нужно.

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

Но зачем в JS люди пихают в прототип методы, которые можно определить в самом классе? Ведь здесь нет полиморфизма. В чем профит? Как такое наследование в JS выглядит глобально, какие задачи решает, для чего оно?

Что такое прототипное наследование

Прошу помощи в объяснении данного текста.. Пытаюсь перейти паралельно на веб, сложно осваивается подход к Java Script после Java.

Что такое прототипное программирование ?

Читайте так же:  Трудоустройство и занятость трудовой кодекс

«это модель ООП которая не использует классы, а вместо этого сначала выполняет поведение класса» Чего .

С пониманием объектно ориентированного программирования нету проблем.

В отличие от большинства других ОО-языков (Java, C#), объектная система в JavaScript основана на прототипах, а не классах. Классы, которые вы знаете по таким языкам, как Java, технически не существуют в JavaScript (JS).

Вся иерархия объектов строиться на цепочках — прототипах. Object.prototype — объект, от которого «наследуются» все остальные объекты. Он содержит такие методы, как toString() или valueOf(). Прототип у него равен null . Замечу, что Object это просто функция-конструктор для создания объектов:

prototype , который используется в примере, применим только к функциям, а для созданных объектов используется __proto__ (или [[Prototype]] ).

Все методы массивов ( slice() , splice() ) хранятся в объекте Array.prototype , а прототип этого объекта узакывает на Object.prototype .

Получается: arr -> Array.prototype -> Object.prototype -> null

Так же с другими встроенными функциями-конструкторами, например Function , Date , Number и т.д.

Сейчас все усложнилось ещё тем, что в новом стандарте (ES6) разработчики JS ввели class – удобный «синтаксический сахар» для задания конструктора вместе с прототипом. Насколько мне известно, его ввели в том числе специально для разработчиков, которые хотят писать на JS, но которых смущает, что в них нет классов :). На самом деле class в JS это обычная функция:

Поэтому я до сих пор считаю, что понятие класс и классическое наследование немного некорректны в отношении JS.

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

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

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

Повторюсь, prototype , который используется в примере, применим только к функциям, а для созданных объектов используется __proto__ (или [[Prototype]] ). Метод max будет находится в прототипе созданных объектов ( ins1.__proto__.max ), а прототипы у них указывают на один и тот же объект:

Другие достоинства, которые можно отметить: простота, мощность данного подхода, меньше избыточного кода, динамичность.

Если интересует момент, почему прототипное наследование в JS похоже на классическое (по синтаксису, например, использование оператора new ), то это легко объяснить. Создатель JS, Brendan Eich, хотел чтобы JavaScript стал младшим братом Java и пытался его сделать максимально похожим синтаксически.

В общем я надеюсь, что вас ещё больше не запутал, просто нужно изучать изучать и ещё раз изучать