Что такое прототипное наследование
Наследование классов в 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 и пытался его сделать максимально похожим синтаксически.
В общем я надеюсь, что вас ещё больше не запутал, просто нужно изучать изучать и ещё раз изучать