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

Наследование классов в 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 мы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе.

Читайте так же:  Договор на вывоз мусора в мешках

Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и универсальный. А что касается красоты синтаксиса – она сильно лучше в новом стандарте 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 и пытался его сделать максимально похожим синтаксически.

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

UPD.:

Пример использования «классов» из стандарта ES6 .

Все методы, объявленные в «классе» автоматически помещаются в прототип созданных объектов, а поля — в сам объект.

Ну и прототип протитипа Auto будет ссылкать на Object.prototype

Код «класса» Auto (без методов) в стандарте ES5 будет выглядеть следующим образом:

Обычная функция-конструктор с проверкой (за счет вызова _classCallCheck ), чтобы нельзя вызывать Auto как функцию. Это все же класс 🙂

Для создания методов используются Object.defineProperty , а посмотреть как работает транспайлер для методов (переводит код из ES6 в ES5 и не только) можно тут: https://babeljs.io/repl/ (Babel — один из самых популярных транспайлеров).

Классическое ООП и JS прототипы на самом деле имеют мало различий:

Любая функция в JS (кроме кратких — стрелочных функций ES6) является в терминологии ООП классом: к любой функции JS можно применять оператор new , что делает её конструктором. Это уже расширяет позицию ООП, в котором объекты конструируют не функции, а классы. Далее вместо термина функция — использую термин класс.

Прототип — это класс-предок объекта, всё как в ООП — разница только в том, что прототип в JS — это уже сконструированный «готовый» объект, а в классическом ООП «прототип» — неотделим от самого класса-потомка: то есть не является ни объектом, ни чем-либо «физическим». Как работает наследование в JS, аналогичное обычному ООП наследованию — легко понять по такому примеру:

  1. Разве что прототипы могут больше, чем могут классы — например прототип(класс-предок говоря языком ООП) можно поменять/установить как для одного объекта, так и для класса в целом во время выполнения. Например можно типу-числу (Number) добавить поведение функции (вызов через скобки), см. скрещивание ужей с ежами в MDN. Где вы видели, чтоб в ООП можно было на лету подменить класс-предок?) При операции установки прототипа — происходит тоже самое, что произошло бы при подмене базового класса: а именно все свойства/методы объекта, не найденные у себя — ищутся уже в другом классе-предке, логика изменяется. Собственно эта возможность подмены/установки базового класса — и является главной фишкой прототипного программирования.
  2. Если нужно вызывать метод класса родителя, при том что он переопределён в текущем классе: до ES6 (в котором есть super ) можно было делать так this.__proto__.someMethod.apply(this, [arg1, arg2]); . Длиннее, чем обычно в ООП — но суть остаётся той-же. Доступ к родительскому конструктору также имеется через this.__proto__.constructor.apply(this,[. ]) . Указываю на это — т.к. не очевидно поначалу — и прототипы кажутся совсем унылыми в плане ООП.
  3. ES6 и его синтаксис определения классов — это просто декорации старых добрых прототипов: то есть ничего принципиально нового в нём нет. Разве что ООП в JS стало удобнее. То есть ИМХО — Прототипное программирование это просто красное словцо. Ну да — нету protected , тоже сначала плевался, а потом понял что и не надо: инкапсуляция в JS очень красиво делается на замыканиях. Зато никто не мешает изобрести своё ООП если уж хочется(я бы не рекомендовал, но порой встечается в фреймворках), а также делать объекты мутанты — например массив может быть одновременно функцией через setPrototypeOf .

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

Читайте так же:  Проживание алупка частный сектор

# 1 Прототипное наследование в Javascript

Всем привет. Сегодня мы с вами разберем как реализовываются классы в Javascript и что такое прототипное наследование. Вообще как таковых классов в Javascript нет. Их реализуют именно с помощью прототипного наследования.

Классы в Javascript

Для начала давайте напишем обычную функцию

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

вернет нам новый обьект, который является экземпляром класса.

Конструкторы в Javacript пишут с большой буквы, чтобы легко отличить их от обычных функций.

Итак, давайте запомним с вами следующие понятия. Все, что мы сейчас будем описывать называется класс, функция на которую мы вызываем оператор new — называется конструктор (т.е. наш Track это конструктор), то, что мы получаем в результате вызова new — является экземпляром класса.

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

Если мы выведем track01 и track02 в консоль — мы увидим, что у нас создались два пустых обьекта.

Теперь давайте в нашем конструкторе добавим аргумент params и выведем его в консоль

Как мы видим в консоли — у нас передаются параметры в конструктор и мы можем с ними дальше работать. Переменная this внутри конструктора является контекстом экземпляра и к полям экземпляра можно обращатся через точку. Поэтому мы можем присвоить this.name и this.url, чтобы они были доступны в наших экземплярах класса.

Теперь, если мы обновим страницу мы увидим, что у нас создались экземпляры класса Track с заполненными полями name и url.

Теперь мы можем обращаться с полям экземпляров, например, track01.name или track01.url.

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

Если мы попробуем вывести this внутри метода playTrack, то увидим что this является экземпляром нашего класса.

Мы можем, например, написать this.name для того, чтобы вывести имя трека.

Теперь давайте вызовем

Как мы видим нам вывелось в консоль

Если мы вызовем этот метод на второй трек, то получим

Метод класса можно обьявлять не только в конструкторе, как мы только что сделали, но и через свойство prototype.

Если мы попробуем запустить наш код, то мы увидим, что он работает абсолютно точно так же.

В чем же различие? Единственная разница в том, что когда метод обьявлен через prototype, а не внутри конструктора, то мы можем его переопределить.

Если мы напишем в консоли

то это выведет нам обьект с функцией playTrack внутри.

Если мы закомментируем код с prototype и раскомментируем код playTrack внутри конструктора, то мы увидим что Track.prototype в консоли — это пустой обьект, а значит функцию playTrack мы никак не можем изменить

В ООП нас конечно же интересует наследование. Давайте представим, что нам нужен класс YoutubeTrack, который имеет дополнительное поле image и наследуется от класса Track.

Мы хотим, чтобы наш конструктор YoutubeTrack выполнял все ту же логику, что и Track только с небольшими изменениями. Для этого воспользуемся методом apply. Для того, чтобы вызвать конструктор Track внутри YoutubeTrack напишем

Что это делает? Apply вызывает функцию Track (в данном случае это конструктор), передавая контекстом this, т.е. контекст YoutubeTrack и передаем все аргументы, которые будут переданы в наш YoutubeTrack.

Давайте выведем в консоль youtubeTrack01 и youtubeTrack02

Мы видим, что все работает точно так же и name и url выводятся как поля обьектов в консоль, которые у нас засетились из конструктора Track.

Если же мы закоментируем код с Track.apply, то мы увидим, что у нас создались пустые обьекты.

Теперь мы хотим добавить еще одно поле к YoutubeTrack. Назовем его image.

Так же при создании экземпляров класса YoutubeTrack добавим image

Как мы видим теперь в консоли у нас выводится еще и image в YoutubeTrack обьектах.

Теперь мы хотим, чтобы все методы, которые нам доступны в классе Track, были так же доступны в YoutubeTrack.

Как мы можем видеть, в экземплярах трека у нас есть функция playTrack и мы хотим, чтобы она была доступна в YoutubeTrack.

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

Мы присваеваем в YoutubeTrack.prototype обьект, которые мы создаем с Track.prototype.

Как мы видим в консоли, у нас появился метод playTrack у экземпляров YoutubeTrack

Теперь мы легко можем вызвать

и будет написано

Но есть один минус. Если мы напишем в консоли

мы увидим здесь не конструктор youtubeTrack, а конструктор Track. Как это исправить?

Этим мы возвращаем ссылку обратно на конструктор YoutubeTrack как нам необходимо. Теперь если мы посмотрим в консоль, то у нас вызывается правильный конструктор.

Как мы видим теперь у всех экземпляров класса есть метод playTrack и если нам необходимо — мы можем его переопределить.

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