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

Наследование классов в 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.

learn.javascript.ru

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

Прочитав очередную умную книжку про javascript, стал разбираться в методах реализации в нём наследования. Ну, то есть всем конечно понятно, что в реальном, большом, проекте лучше всего для этого использовать функцию из какой-нибудь библиотеки( благо их много ), но ведь хочется понять, как это вообще работает.

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

1. Итак, а в чем собственно проблема?

Проблема в том, что в javascript нет привычного наследования. Максимум что можно сделать, это создать конструктор класса:

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

Этого в большинстве случаев хватает, но иногда хочется большего.

2. А если очень хочется.

… то можно это реализовать. Классический способ выглядит так:

И его даже можно использовать:

У меня с ним, правда, есть одна маленькая проблема — обычно, для создания прототипа, я использую объектную нотацию, как было показано немного выше:

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

3. Метод посвящается всем любящим объектные литералы

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

И консоль со мной согласна:

Отлично. А теперь представим гипотетическую ситуацию, когда родительских классов много, и нужно, например, вызвать какой-нибудь метод, который был перегружен 2-3 класса назад. В такой ситуации, конечно, лучше еще раз хорошенько присмотреться к архитектуре своего приложения. Теперь допустим, что там все работает, как задумано, тогда чертовски неудобно писать, например, так:

4. Но это тоже решаемо

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

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

Класс BadProgrammer воспользовался мыслями самого первого звена нашей эволюционной цепочки классов, и, теперь, думает совсем не о том, о чём обычно думают программисты 😉

5. И еще кое что

Не представляю в какиx случаях это может пригодится, но, возможно, когда-нибудь, кому-нибудь может понадобится множественное наследование. Его тоже вполне можно реализовать:

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

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

Вот так все это и работает. Весь код и примеры положил сюда — вдруг пригодится.

UPD. Как справедливо заметил один хороший человек, во всех методах, кроме первого, instanceof отказывается признавать класс экземпляром родительского класса. При множественном наследовании этот недостаток сохранился, но для остальных случаев я его устранил.

m.habr.com

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

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

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

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

Что такое класс? Тип объекта. А задать тип объекта можно без prototype. Как сделать ссылки на родителей? Первое, что пришло в голову, это скопировать все его методы и переменные. Наверняка пострадает производительность.

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

Наследование идет слева направо и не может быть вложенным. Если конструктора нет в дочернем классе, вызывается все конструкторы родительских по очереди слева направо. Вызов родительских методов можно реализовать через переменную __bases:

На этом я свои изыскания закончил. Хочу выслушать мнения по данному способу создавать объекты. В чем по вашему минусы данного подхода?

m.habr.com

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

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

Это происходит потому, что объекты в 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 (старые барузеры) вы можете либо использовать один из существующих полифилов, либо сделать все ручками(через анонимный конструктор):

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

ru.stackoverflow.com

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

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

ru.stackoverflow.com

Смотрите так же:

  • Решение системы по правилу крамера Метод Крамера. Примеры решения систем линейных алгебраических уравнений методом Крамера. Метод Крамера предназначен для решения тех систем линейных алгебраических уравнений (СЛАУ), у которых определитель матрицы системы отличен от нуля. […]
  • Как включить разрешение для adobe flash player Как включить Adobe Flash Player в браузере Google Chrome Adobe Flash Player – популярный проигрыватель для воспроизведения flash-содержимого, который и по сей день остается актуальным. В веб-обозреватель Google Chrome по умолчанию уже […]
  • Правило падежей местоимений Личные местоимения в английском. Правила и упражнения Личные местоимения в английском языке встречаются в двух падежах – именительном (nominative case) и объектном (objective case). В данной статье Вы познакомитесь и с теми, и с другими, […]
  • Php определение разрешения Как должен выглядеть код PHP для запроса разрешение браузера клиента? Добрый день, помогите пожалуйста у меня задача, узнать у клиента/посетителя ширину браузера, если она меньше или равна 1024 то нужно вернуть TRUE Т.е. php код со […]
  • Утилита почистить реестр Windows Cleaner — бесплатная программа для очистки и оптимизации системы Windows XP / Vista / 7 / 8 / 10 Версия: 2.2.29 от 8 ноября 2016 Размер файла: 5,8 МБ Качественная очистка операционной системы Windows Бесплатная программа […]
  • Live lived правило Секреты английского языка Сайт для самостоятельного изучения английского языка онлайн Разница в употреблении to live, life, live, alive, living Posted on 2014-03-17 by admin in Всякая всячина // 6 Comments Слова live, life, live, alive, […]

Обсуждение закрыто.