Наследование javascript. Прототипы и наследование в JavaScript. Наследование на основе классов

JavaScript – это язык, основанный на прототипах. Это значит, что свойства и методы объектов можно повторно использовать посредством общих объектов, которые можно клонировать и расширять. Это называется наследованием прототипов и отличается от наследования классов. Среди популярных объектно-ориентированных языков программирования JavaScript относительно уникален, поскольку другие известные языки (PHP, Python и Java) являются языками на основе классов, которые в качестве макетов для объектов используют классы вместо прототипов.

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

Прототипы в JavaScript

Создайте новый массив:

Помните, что создать его можно также с помощью конструктора массива: let y = new Array().

Если посмотреть на [] нового массива y, вы увидите, что он имеет больше свойств и методов, чем объект x. Он унаследовал все это от Array.prototype.

y.__proto__;

Вы увидите свойство constructor в прототипе, для которого задано значение Array(). Свойство constructor возвращает функцию-конструктор объекта, которая является механизмом для построения объектов из функций.

Теперь можно объединить два прототипа, так как в этом случае цепочка прототипов будет длиннее. Он выглядит так: y-> Array -> Object.

y.__proto__.__proto__;
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Эта цепочка теперь относится к Object.prototype. Можно проверить внутренний [] на свойство prototype функции конструктора, чтобы увидеть, что они ссылаются на одно и то же.

y.__proto__ === Array.prototype; // true
y.__proto__.__proto__ === Object.prototype; // true

Также для этого можно использовать свойство isPrototypeOf():

Array.prototype.isPrototypeOf(y); // true
Object.prototype.isPrototypeOf(Array); // true

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

y instanceof Array; // true

Итак, все объекты JavaScript имеют скрытое внутреннее свойство [] (которое можно определить с помощью __proto__ в некоторых браузерах). Объекты могут быть расширены и наследуют свойства и методы от [] их конструктора.

Прототипы складываются в цепочки, и каждый дополнительный объект наследует все по этой цепочке. Цепочка заканчивается на Object.prototype.

Функции-конструкторы

Функции-конструкторы – это функции, которые используются для построения новых объектов. Оператор new используется для создания новых экземпляров на основе функции конструктора. Вы уже знаете некоторые встроенные конструкторы JavaScript (new Array() и new Date(), например); вы также можете создавать собственные пользовательские шаблоны для построения объектов.

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

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

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

// Initialize a constructor function for a new Hero
function Hero(name, level) {
this.name = name;
this.level = level;
}

Теперь у вас есть функция-конструктор Hero с двумя параметрами: name и level. Поскольку у каждого персонажа будет имя и уровень, для них имеет смысл наследовать эти свойства. Ключевое слово this будет ссылаться на новый созданный экземпляр; this.name в параметре name гарантирует, что новый объект будет иметь свойство name.

Создайте новый экземпляр с помощью new.

let hero1 = new Hero("Bjorn", 1);

Если запросить в консоли hero1, вы увидите новый объект с правильно установленными свойствами:

Hero {name: "Bjorn", level: 1}

Теперь, если запросить [] объекта hero1, вы увидите constructor Hero().

Object.getPrototypeOf(hero1);
constructor: ƒ Hero(name, level)

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

Мы можем добавить помощью prototype. Создайте метод greet().

// Add greet method to the Hero prototype


}

Поскольку greet() – это prototype в Hero, а hero1 является экземпляром Hero, метод будет доступен и для hero1:

hero1.greet();
"Bjorn says hello."

Если вы проверите [] в Hero, вы увидите доступную опцию greet().

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

С помощью метода call() скопируйте свойства одного конструктора в другой. Создайте конструкторы Warrior и Healer.

...
// Initialize Warrior constructor

// Chain constructor with call

// Add a new property
this.weapon = weapon;
}
// Initialize Healer constructor

Hero.call(this, name, level);
this.spell = spell;
}

Оба новых конструктора теперь обладают свойствами Hero и несколькими уникальными свойствами. Добавьте метод attack() в Warrior и метод heal() в Healer.

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

Tags:

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

Var a = {test: 11} b = a; b.test = 12; console.log(a.test); // Выведет 12!

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

Свойство .prototype - это объект. Когда вы выполняете код:

Bar.prototype = Foo.prototype;

вы присваиваете свойству 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.

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

Bar.prototype = new Foo();

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

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

Foo = function(a) { if (typeof a === "undefined") { throw new Error("You have to set the first argument."); } this.a = a; }

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

Bar.prototype = new Foo();

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

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

Foo = function(a) { console.log("Here I am!"); }

При использовании:

Bar.prototype = new Foo();

и дальнейшем:

Var Bar = function() { Foo.call(this); }

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

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

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

// Базовый конструктор var Foo = function() { // ... }; Foo.prototype.doSomething = function() { // ... }; // Дочерний конструктор var Bar = function() { // Вызываем базовый конструктор для текущего объекта. Foo.call(this); // ... }; // Устанавливаем правильное значение в цепочке прототипов. Bar.prototype = Object.create(Foo.prototype, { // Выставляем правильную функцию-конструктор для всех создаваемых // объектов. constructor: { value: Bar, enumerable: false, writable: true, configurable: true } }); // Расширяем прототип дочернего "класса". Этот шаг должен идти // СТРОГО ПОСЛЕ установки значения Bar.prototype. Bar.prototype.doAnotherAction = function() { // ... };

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

Var inherits = function(ctor, superCtor) { // Временный конструктор, который не делает ничего и нужен // только для разрыва прямой связи между прототипами ctor // и superCtor. Его использование позволяет менять прототип // дочернего конструктора, не боясь сломать родительский. var Tmp = function() {}; Tmp.prototype = superCtor.prototype; // Обратите внимание, вызов new Tmp() не имеет АБСОЛЮТНО // никаких побочных эффектов и не накладывает ограничений // на передаваемые значения. ctor.prototype = new Tmp(); // Выставляем правильную функцию-конструктор для всех // создаваемых объектов. ctor.prototype.constructor = ctor; };

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

Var inherits = (function() { if (typeof Object.create === "function") { // Используем более простой вариант, если Object.create существует. return function(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); }; } // Используем временный конструктор для старых браузеров return function(ctor, superCtor) { var Tmp = function() {}; Tmp.prototype = superCtor.prototype; ctor.prototype = new Tmp(); ctor.prototype.constructor = ctor; }; })();

В реализациях выше, после присваивания прототипа, задается свойство Function.prototype.constructor . Хотя это свойство редко используется на практике (лично я ни разу не видел в production коде), полноценная реализация наследования должна его выставлять.

Нет ничего более постоянного, чем временное.
Народная мудрость

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

В JavaScript каждый объект может иметь асоциацию с другим объектом - так называемый «прототип» (prototype). В случае, если поиск некоторого свойства (или метода - это одно и то же) в исходном объекте заканчивается неудачно, интерпретатор пытается найти одноименное свойство (метод) в его прототипе, затем - в прототипе прототипа и т. д. К примеру, если мы затребовали обращение к obj.prop (или, что абсолютно то же самое, obj["prop"]), JavaScript начнета искать свойство prop в самом объекте obj , затем - в прототипе obj , прототипе прототипа obj , и так до конца.

Секреты прототипов

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

Продемонстрируем «классическое» применение прототипов для реализации наследования в JavaScript.

Листинг 1

//** //** Базовый "класс" Car (Машина). //** function Car() { document.writeln("Вызван конструктор Car()."); } // Определяем новый метод "класса" Car. Car.prototype.drive = function() { document.writeln("Вызван Car.drive()"); } //** //** Производный "класс" Zaporojets (Запорожец - тоже Машина). //** function Zaporojets() { document.writeln("Вызван конструктор Zaporojets()."); } // Говорим, что прототип Car - "класс" Zaporojets. Zaporojets.prototype = new Car(); // Определяем новый метод "класса" Zaporojets. Zaporojets.prototype.crack = function() { document.writeln("Вызван Zaporojets.crack()"); } //** //** Основная программа. //** document.writeln("Программа запущена."); // Создаем объект производного "класса" Zaporojets. // (*) вызывается функция базового объекта var other = new Zaporojets(); vehicle.crack();

Запустив данный пример, можно заметить, что с точки зрения "обычного" ООП результат выглядит несколько необычно:

Листинг 2

Вызван конструктор Car(). Программа запущена. Вызван конструктор Zaporojets(). Вызван Car.drive() Вызван конструктор Zaporojets(). Вызван Zaporojets.crack()

В объектно-ориентированных языках с поддержкой классов (C++, Java, PHP, Perl, Python и т. д.) конструкторы базовых классов обычно вызываются непосредственно внутри конструкторов производных. В JavaScript, как было уже сказано в предыдущей набле , классов нет, есть только объекты. Здесь мы видим совершенно другую картину: конструктор Car запустился даже до вывода сообщения "Программа запущена"! Кроме того, при повторном создании объекта Zaporojets конструктор Car вызван не был, а значит, один и тот же объект Car «разделяется» многими объектами Zaporojets ! С точки зрения идеологии наследования это совершенно неправильно.

К сожалению, невозможно задать прототип для некоторого объекта, не создав предварительно объект базового класса. Если вы хотите присвоить Zaporojets.prototype новое значение, вы просто обязаны использовать оператор new Car() . Иными словами, создание подобъекта базового «класса» производится в JavaScript не в конструкторе производного (как во всех остальных объектно-ориентированных языках), а гораздо раньше, еще на этапе конструирования «класса-потомка», и при том однократно.

Подобное поведение, конечно, следует из того, как написана программа. Действительно, мы создали объект Car только один раз - при присваивании значения прототипу Zaporojets ; соответственно, и его конструктор был вызван в этот момент лишь однажды.

Вывод: в JavaScript «стандартное» наследование реализуется совсем не так, как в других, «класс-ориентированных» языках программирования. Понятие «конструктора» в нем - не то же самое, что конструктор в C++, Java или даже Perl.

Чем не являются прототипы?

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

Листинг 3

Var obj = { // Зато у него есть прототип... prototype: { // ...в котором данное свойство определяется... prop: 101 } // ...так что в итоге интерпрететор должен считать его. } // Проверим? alert("Значение свойства: " + obj.prop); // What a...

Увы и ах: данный пример не работает , выдавая: "Значение свойства: undefined". А следовательно, присваивание свойству prototype произвольного объекта нового значения ничего нам не дает!

Модифицируем теперь код программы:

Листинг 4

Var obj = { // В самом объекте свойства prop нет. } // Пробуем обратиться к прототипу по-другому. obj.constructor.prototype.prop = 101; // Проверим? alert("Значение свойства: " + obj.prop); // Он в этом-то объекте свойства быть не должно... var newObj = {}; // пустой хэш alert("Пустота: " + newObj.prop); // А это еще откуда?!

Результат "Значение свойства: 101" говорит нам, что программа заработала. Однако какой ценой? Свойство prop теперь появилось вообще в любом объекте, создаваемом когда-либо в программе, а не только в obj ! Убедиться в этом позволяет второй вызов alert() , гордо сообщающий, что «пустота», оказывается, является числом 101. («Просветлей сам - просветлятся все существа в мире.»)

Какие выводы можно сделать из примера?

  • В самом объекте свойства prototype не имеет никакого особого смысла.
  • К прототипу объекта следует обращаться через служебное свойство constructor , присутствующее в любом хэше.
  • Выражение obj.constructor.prototype (а не obj.prototype ! это важно!) означает прототип объекта.
  • Оператор new и obj.constructor

    Новый объект в JavaScript может быть создан только одним способом: применением оператора new:

    Листинг 5

    Var vehicle = new Car(); // создание нового объекта var hash = {}; // сокращенная запись для new Object() var array = ; // сокращенная запись для new Array()

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

    Листинг 6

    Var vehicle = new window.Car(); // можно и так... var vehicle = new self.Car(); // в браузере self==window

    или даже такому:

    Листинг 7

    Он также функционально не отличается от следующего примера:

    Листинг 8

    // Создание объекта стандартным способом. self.Car = function() { alert("Car") } var vehicle = new self.Car();

    Ну что, понравилось? Начали улавливать закономерности? Вот еще примеры:

    Листинг 9

    // Создаем "класс" на лету. var clazz = function() { alert("Динамическая!") } var obj = new clazz(); // А можно и без промежуточной переменной. var obj = new (function() { alert("Wow!") })();

    Иными словами, справа от new может стоять любое значение JavaScript. Это совсем не обязательно имя функции - к тому же, что такое функция, как не переменная, значение которой является ссылка на код?

    Так вот, после создания объекта интерпретатор присваивает его свойству constructor значение, равное величине, стоящей справа от оператора new . Таким образом, vehicle.constructor == self.Car , а obj.constructor в последнем примере вообще ссылается на функцию, не имеющую отдельного имени в глобальной области видимости (анонимную). Это настолько важно, что я приведу еще один поясняющий пример:

    Листинг 10

    // Создаем "класс" на лету. var clazz = function() { alert("Динамическая!") } var obj = new clazz(); alert(obj.constructor == clazz); // выводит true!

    Но позвольте, ведь справа от new не может стоять совсем уж все, что угодно. К примеру, там недопустимо число или строка... Следующий пример также не работает:

    Листинг 11

    Var clazz = {}; // clazz.constructor == self.Object var obj = new clazz(); // не работает!

    Что же можно использовать с оператором new ? Ответ прост: только функции (точнее, объекты, конструктор которых равен self.Function). А если еще точнее - разрешено использовать стандартные объекты JavaScript self.Array , self.String и т. д.

    Оказывается, что свойство prototype со специальным назначением есть только у таких объектов, которые могут быть использованы в правой части new ! Например, допустимы обращения к Function.prototype , String.prototype или Array.prototype .

    Теперь вы понимаете, почему JavaScript не рассматривает элемент obj.prototype произвольного хэша obj как специальный, но обращается к obj.constructor.prototype ? Ведь специальное назначение prototype имеет только для встроенного объекта, коим всегда является ссылка obj.constructor .

    Итак, вывод: прототипы объектов доступны по цепочке obj.constructor.prototype.constructor.prototype... , а не obj.prototype.prototype , как можно понять из многих руководств по JavaScript в Интернете. Конструктором объекта может быть только объект встроенного класса (обычно это Function).

    Заставляем конструкторы базовых классов работать

    Данная набла имеет циклический характер, и сейчас, хорошо понимая, как работают прототипы и конструкторы, мы снова возвращаемся к самому первому примеру. Речь пойдет о создании базового и производных объектов в стиле «класс-ориентированного» программирования.

    Итак, перед нами стоят следующие задачи:

    • Заставить конструкторы базовых объектов вызываться при создании производных.
    • Научиться получать доступ к методам, переопределенным в производных объектах под тем же именем.

    Если программировать на «чистом» JavaScript, данные две задачи выливаются в довольно громоздкий код. Чтобы каждый раз его не писать, я предлагаю вам использовать совсем небольшую библиотечку, обеспечивающую удобное применение рассматриваемых подходов. С ее использованием создание производных классов выглядит весьма просто:

    Листинг 12

    // Базовый "класс". Car = newClass(null, { constructor: function() { document.writeln("Вызван конструктор Car()."); }, drive: function() { document.writeln("Вызван Car.drive()"); } }); // Производный "класс". Zaporojets = newClass(Car, { constructor: function() { document.writeln("Вызван конструктор Zaporojets()."); this.constructor.prototype.constructor.call(this); }, crack: function() { document.writeln("Вызван Zaporojets.crack()"); }, drive: function() { document.writeln("Вызван Zaporojets.drive()"); return this.constructor.prototype.drive.call(this); } }); document.writeln("Программа запущена."); // Создаем объект производного "класса". var vehicle = new Zaporojets(); vehicle.drive(); // вызывается функция базового объекта // Создаем еще один объект того же класса. var vehicle = new Zaporojets(); vehicle.crack(); // функция производного объекта

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

    Листинг 13

    Программа запущена. Вызван конструктор Zaporojets(). Вызван конструктор Car(). Вызван Zaporojets.drive() Вызван Car.drive() Вызван конструктор Zaporojets(). Вызван конструктор Car(). Вызван Zaporojets.crack()

    Как видите, все работает так, как и ожидает программист на «класс-ориентированном» языке: конструктор Car() вызывается вместе с конструктором Zaporojets() . Однако запускать конструктор базового класса в конструкторе производного нужно явно (заодно приведено, как вызывать метод drive из базового объекта, если он был переопределен в производном):

    Листинг 14

    // Вызов конструктора базового объекта. this.constructor.prototype.constructor.call(this); // Вызов переопределенного метода базового объекта. this.constructor.prototype.drive.call(this); // У стандартного метода call() можно указывать // дополнительные аргументы (после this), которые // будут переданы функции-члену объекта.

    Библиотека Oop.js состоит из определения одной-единственной функции newClass . Она невелика, однако детальный разбор механизма ее работы, возможно, займет у вас немало времени (по крайней мере, я потратил не один час на эксперименты в разных браузерах). Могу сказать, что информации данной наблы должно быть вполне достаточно.

    Листинг 15

    // // Create proper-derivable "class". // // Version: 1.2 // function newClass(parent, prop) { // Dynamically create class constructor. var clazz = function() { // Stupid JS need exactly one "operator new" calling for parent // constructor just after class definition. if (clazz.preparing) return delete(clazz.preparing); // Call custom constructor. if (clazz.constr) { this.constructor = clazz; // we need it! clazz.constr.apply(this, arguments); } } clazz.prototype = {}; // no prototype by default if (parent) { parent.preparing = true; clazz.prototype = new parent; clazz.prototype.constructor = parent; clazz.constr = parent; // BY DEFAULT - parent constructor } if (prop) { var cname = "constructor"; for (var k in prop) { if (k != cname) clazz.prototype[k] = prop[k]; } if (prop && prop != Object) clazz.constr = prop; } return clazz; }

    (prototypal ). В любом случае, так как речь про объектно-ориентированное программирование, наследование - это про создание множества объектов, обладающих общими свойствами , на основе уже существующих .

    Рассмотрим пример на некотором вымышленном языке программирования удивительно похожем на JavaScript:

    // Простейший способ создать объект var greeterInstance = { person: null, greeting: function() { return "Hello " + this.person; } }; greeterInstance.person = "Alice"; // Некоторый код, который использует greeterInstance ... // А здесь нам нужен аналогичный объект, но для Bob"а. var greeterInstance2 = { person: null, greeting: function() { return "Hello " + this.person; } }; greeterInstance2.person = "Bob"; // ...

    Упс, похоже на copy/paste! Такой код будет трудно поддерживать, так как изменения придется вносить сразу во все места, где создается instance. Попробуем улучшить:

    Function createGreeter(person) { return { person: person, greeting: function() { return "Hello " + this.person; } }; } var aliceGreeter = createGreeter("Alice"); // Некоторый код, который использует aliceGreeter ... var bobGreeter = createGreeter("Bob"); // ...

    Отлично, теперь мы можем создавать множество похожих объектов , используя createGreeter() , и мы избавились от дублирования кода. Это уже наследование? Нет, так как никто ни от кого ничего не наследует. Это способ повторного использования кода - да.

    Function createGreeter(person) { return { person: person, greeting: function() { return "Hello " + this.person; } }; } function createGateKeeper(person) { var keeper = { opened: false, open: function() { this.opened = true; console.log(this.greeting()); } }; var greeter = createGreeter(person); for (var k in keeper) { greeter[k] = keeper[k]; } return greeter; } var gateKeeper = createGateKeeper("Alice"); gateKeeper.open();

    А вот это уже больше похоже на наследование, так как с помощью createGateKeeper() мы можем создавать множество похожих объектов , каждый из которых основывается на объектах типа Greeter .

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

    Рассмотрим аналогию из реального мира. Класс можно воспринимать, как чертеж, по которому создаются изделия на заводе (объекты). Чертеж изделия!= самому изделию. Это лишь информация о том, как построить изделие (создать объект). В основанном на классах наследовании, одни чертежи наследуют общие свойства от других чертежей. А затем, объекты, созданные на основе таких чертежей, обладают свойствами, определенными как в первых, так и во вторых. Рассмотрим пример на Python 3:

    Class Greeter: def __init__(self, person): self.person = person def greeting(self): return "Hello " + self.person aliceGreeter = Greeter("Alice") # Во многих языках (Java, C++, C#, etc) принято писать aliceGreeter = new Greeter(...). # Обратите внимание на ключевое слово new. Такую реализацию наследования # принято называть "классической". print(aliceGreeter.greeting()) # "Чертеж" для gate keeper"ов построен на основе чертежа для greeter"ов. # Т.е. наследует все его свойства. А также добавляет собственные. class GateKeeper(Greeter): def __init__(self, person): super().__init__(person) self.opened = False def open(self): self.opened = True print(self.greeting()) gateKeeper = GateKeeper("Alice") gateKeeper.open()

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

    В прототипном наследовании отсутствует понятие чертежа (класса). Тут речь скорее идет о некотором первородном объекте-образце. Этот объект используется для создания множества других, идентичных ему объектов, расширяющих его дополнительными свойствами. Т.е. это похоже на завод, где нет чертежей изделия, которое нужно производить, но есть образец в единственном экземпляре. Задача инженеров на таком заводе научиться воспроизводить копии такого изделия по образцу и встраивать в них новые функции. И да, само по себе изделие №0 также является полноправным изделием (вспомним, что класс!= объект этого класса).

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

    • копированием всех свойств основного объекта в создаваемый на этапе его построения
    • делегированием обращений к свойтвам, не заданным в создаваемом объекте, базовому объекту

    Оба способа имеют свои плюсы и минусы. Рассмотрим реализацию первого способа :

    Function createBaseObject() { return { foo: "bar", method1: function() { return 42; } }; } var base = createBaseObject(); function createChildObject() { var child = { baz: 9001, method2: function() { return 43; } }; for (var k in base) { child[k] = base[k]; } return child; }

    • Так как каждый child объект содержит копию свойств base требуется дополнительная память.
    • Дополнительное время на копирование свойств base в child при создании.
    • Скорость обращения к свойствам child"ов не страдает за счет делегирования (см. ниже).
    • Изменение base объекта после создания child"ов не влияет на уже созданные объекты (это может быть как плюсом, так и минусом).

    Рассмотрим реализацию на основе делегирования (обращаю внимание, что это не JavaScript, а некоторый вымышленный язык удивительно на него похожий):

    Function createBaseObject() { return { foo: "bar", method1: function() { return 42; } }; } var base = createBaseObject(); function createChildObject() { return { baz: 9001, method2: function() { return 43; }, __get__: function(prop) { assert !this.hasOwnProperty(prop) return base; // Делегирование обращения базовому объекту } }; }

    Предположим, что "магический" метод __get__ переопределяет поведение при обращении к свойтвам, которые не заданы у самого объекта. Т.е. внутри __get__ вызов this.hasOwnProperty(prop) всегда возращает false .

    • Скорость такого кода должна быть ниже (если не рассматривать различные оптимизации под капотом языка) за счет дополнительного уровня косвенности, вводимого методом __get__ .
    • Создание объектов происходит быстрее.
    • Требуется меньше дополнительной памяти.
    Прототипное наследование в JavaScript

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

    Для ссылки на базовый объект при обращении к свойствам, не заданным у текущего объекта, используется свойство [] . Т.е., для того, чтобы унаследовать один объект от другого, нужно каким-либо способом задать для наследника [] равным ссылке на базовый объект. Простейший (но не стандартизованный до ES6 и не самый быстрый при этом) способ - это использовать свойство __proto__ :

    Var base = { foo: "bar" }; var child = { baz: 42, __proto__: base }; console.log(child.baz); console.log(child.foo); // Делегирование обращения базовому объекту

    До ES6 было как минимум два "законных" способа сделать это. Первый и не самый прямолинейный - это использование ключевого слова new . Поговорим о нем чуть позже. Второй же - изобретенная Дугласом Крокфордом функция Object.create() (ссылка), которая в итоге была добавлена в сам язык.

    Код из примера выше можно переписать следующим образом:

    Var base = { foo: "bar" }; var child = Object.create(base); // создает новый объект с заданным прототипом child.baz = 42; console.log(child.baz); console.log(child.foo);

    Обернув две строки создание child в функцияю createChild() мы создадим удобную реализацию прототипного наследования от base .

    Запутывающая всех конструкция new

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

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

    Var base = { greeting: function() { return "Hello " + this.person; } }; function Greeter(person) { this.person = person; } Greeter.prototype = base; var greeter = new Greeter("Alice"); console.log(greeter.greeting()); // prints "Hello Alice" console.log(greeter.__proto__ === base); // prints "true"

    Функции наподобие Greeter в JavaScript называются конструкторами (а иногда не совсем корректно - классами). При вызове new Greeter() создается новый объект, this внутри конструктора ссылается на этот объект. А в качестве прототипа этого объекта задается объект Greeter.prototype . Таким образом, вводится дополнительный уровень косвенности.

    This indirection was intended to make the language seem more familiar to classically trained programmers, but failed to do that, as we can see from the very low opinion Java programmers have of JavaScript. JavaScript’s constructor pattern did not appeal to the classical crowd. It also obscured JavaScript’s true prototypal nature. As a result, there are very few programmers who know how to use the language effectively. (с) Дуглас Крокфорд

    Теперь, обладая этими знаниями, мы можем без труда понять первоначальную реализацию Object.create() :

    Object.create = function(o) { function F() {} F.prototype = o; return new F(); };

    В результате вызова Object.create() будет создан новый пустой объект (new F()), прототипом которого будет объект o . И достигается это за счет описанной выше особенности JavaScript.

  • JavaScript is a bit confusing for developers experienced in class-based languages (like Java or C++), as it is dynamic and does not provide a class implementation per se (the class keyword is introduced in ES2015, but is syntactical sugar, JavaScript remains prototype-based).

    When it comes to inheritance, JavaScript only has one construct: objects. Each object has a private property which holds a link to another object called its prototype . That prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. By definition, null has no prototype, and acts as the final link in this prototype chain .

    Inheriting "methods"

    JavaScript does not have "methods" in the form that class-based languages define them. In JavaScript, any function can be added to an object in the form of a property. An inherited function acts just as any other property, including property shadowing as shown above (in this case, a form of method overriding ).

    When an inherited function is executed, the value of this points to the inheriting object, not to the prototype object where the function is an own property.

    Var o = { a: 2, m: function() { return this.a + 1; } }; console.log(o.m()); // 3 // When calling o.m in this case, "this" refers to o var p = Object.create(o); // p is an object that inherits from o p.a = 4; // creates a property "a" on p console.log(p.m()); // 5 // when p.m is called, "this" refers to p. // So when p inherits the function m of o, // "this.a" means p.a, the property "a" of p

    Using prototypes in JavaScript

    Let"s look at what happens behind the scenes in a bit more detail.

    In JavaScript, as mentioned above, functions are able to have properties. All functions have a special property named prototype . Please note that the code below is free-standing (it is safe to assume there is no other JavaScript on the webpage other than the below code). For the best learning experience, it is highly recommended that you open a console (which, in Chrome and Firefox, can be done by pressing Ctrl+Shift+I), navigate to the "console" tab, copy-and-paste in the below JavaScript code, and run it by pressing the Enter/Return key.

    Function doSomething(){} console.log(doSomething.prototype); // It does not matter how you declare the function, a // function in JavaScript will always have a default // prototype property. var doSomething = function(){}; console.log(doSomething.prototype);

    As seen above, doSomething() has a default prototype property, as demonstrated by the console. After running this code, the console should have displayed an object that looks similar to this.

    { constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() } }

    We can add properties to the prototype of doSomething() , as shown below.

    Function doSomething(){} doSomething.prototype.foo = "bar"; console.log(doSomething.prototype);

    This results in:

    { foo: "bar", constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() } }

    We can now use the new operator to create an instance of doSomething() based on this prototype. To use the new operator, simply call the function normally except prefix it with new . Calling a function with the new operator returns an object that is an instance of the function. Properties can then be added onto this object.

    Try the following code:

    Function doSomething(){} doSomething.prototype.foo = "bar"; // add a property onto the prototype var doSomeInstancing = new doSomething(); doSomeInstancing.prop = "some value"; // add a property onto the object console.log(doSomeInstancing);

    This results in an output similar to the following:

    { prop: "some value", __proto__: { foo: "bar", constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf() } } }

    As seen above, the __proto__ of doSomeInstancing is doSomething.prototype . But, what does this do? When you access a property of doSomeInstancing , the browser first looks to see if doSomeInstancing has that property.

    If doSomeInstancing does not have the property, then the browser looks for the property in the __proto__ of doSomeInstancing (a.k.a. doSomething.prototype). If the __proto__ of doSomeInstancing has the property being looked for, then that property on the __proto__ of doSomeInstancing is used.

    Otherwise, if the __proto__ of doSomeInstancing does not have the property, then the __proto__ of the __proto__ of doSomeInstancing is checked for the property. By default, the __proto__ of any function"s prototype property is window.Object.prototype . So, the __proto__ of the __proto__ of doSomeInstancing (a.k.a. the __proto__ of doSomething.prototype (a.k.a. Object.prototype)) is then looked through for the property being searched for.

    If the property is not found in the __proto__ of the __proto__ of doSomeInstancing, then the __proto__ of the __proto__ of the __proto__ of doSomeInstancing is looked through. However, there is a problem: the __proto__ of the __proto__ of the __proto__ of doSomeInstancing does not exist. Then, and only then, after the entire prototype chain of __proto__ "s is looked through, and there are no more __proto__ s does the browser assert that the property does not exist and conclude that the value at the property is undefined .

    Let"s try entering some more code into the console:

    Function doSomething(){} doSomething.prototype.foo = "bar"; var doSomeInstancing = new doSomething(); doSomeInstancing.prop = "some value"; console.log("doSomeInstancing.prop: " + doSomeInstancing.prop); console.log("doSomeInstancing.foo: " + doSomeInstancing.foo); console.log("doSomething.prop: " + doSomething.prop); console.log("doSomething.foo: " + doSomething.foo); console.log("doSomething.prototype.prop: " + doSomething.prototype.prop); console.log("doSomething.prototype.foo: " + doSomething.prototype.foo);

    This results in the following:

    DoSomeInstancing.prop: some value doSomeInstancing.foo: bar doSomething.prop: undefined doSomething.foo: undefined doSomething.prototype.prop: undefined doSomething.prototype.foo: bar

    Different ways to create objects and the resulting prototype chain Objects created with syntax constructs var o = {a: 1}; // The newly created object o has Object.prototype as its [] // o has no own property named "hasOwnProperty" // hasOwnProperty is an own property of Object.prototype. // So o inherits hasOwnProperty from Object.prototype // Object.prototype has null as its prototype. // o ---> Object.prototype ---> null var b = ["yo", "whadup", "?"]; // Arrays inherit from Array.prototype // (which has methods indexOf, forEach, etc.) // The prototype chain looks like: // b ---> Array.prototype ---> Object.prototype ---> null function f() { return 2; } // Functions inherit from Function.prototype // (which has methods call, bind, etc.) // f ---> Function.prototype ---> Object.prototype ---> null With a constructor

    A "constructor" in JavaScript is "just" a function that happens to be called with the new operator .

    Function Graph() { this.vertices = ; this.edges = ; } Graph.prototype = { addVertex: function(v) { this.vertices.push(v); } }; var g = new Graph(); // g is an object with own properties "vertices" and "edges". // g.[] is the value of Graph.prototype when new Graph() is executed.

    With Object.create "use strict"; class Polygon { constructor(height, width) { this.height = height; this.width = width; } } class Square extends Polygon { constructor(sideLength) { super(sideLength, sideLength); } get area() { return this.height * this.width; } set sideLength(newLength) { this.height = newLength; this.width = newLength; } } var square = new Square(2); Performance

    The lookup time for properties that are high up on the prototype chain can have a negative impact on the performance, and this may be significant in the code where performance is critical. Additionally, trying to access nonexistent properties will always traverse the full prototype chain.

    Also, when iterating over the properties of an object, every enumerable property that is on the prototype chain will be enumerated. To check whether an object has a property defined on itself and not somewhere on its prototype chain, it is necessary to use the hasOwnProperty method which all objects inherit from Object.prototype . To give you a concrete example, let"s take the above graph example code to illustrate it:

    Console.log(g.hasOwnProperty("vertices")); // true console.log(g.hasOwnProperty("nope")); // false console.log(g.hasOwnProperty("addVertex")); // false console.log(g.__proto__.hasOwnProperty("addVertex")); // true

    hasOwnProperty is the only thing in JavaScript which deals with properties and does not traverse the prototype chain.

    Note: It is not enough to check whether a property is undefined . The property might very well exist, but its value just happens to be set to undefined .

    Bad practice: Extension of native prototypes

    One misfeature that is often used is to extend Object.prototype or one of the other built-in prototypes.

    This technique is called monkey patching and breaks encapsulation . While used by popular frameworks such as Prototype.js, there is still no good reason for cluttering built-in types with additional non-standard functionality.

    The only good reason for extending a built-in prototype is to backport the features of newer JavaScript engines, like Array.forEach .

    Summary of methods for extending the prototype chain

    Here are all 4 ways and their pros/cons. All of the examples listed below create exactly the same resulting inst object (thus logging the same results to the console), except in different ways for the purpose of illustration.

    Name Example(s) Pro(s) Con(s)
    New-initialization function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = new foo; proto.bar_prop = "bar val"; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); Supported in every browser imaginable (support goes all the way back to IE 5.5!). Also, it is very fast, very standard, and very JIST-optimizable. In order to use this method, the function in question must be initialized. During this initialization, the constructor may store unique information that must be generated per-object. However, this unique information would only be generated once, potentially leading to problems. Additionally, the initialization of the constructor may put unwanted methods onto the object. However, both these are generally not problems at all (in fact, usually beneficial) if it is all your own code and you know what does what where.
    Object.create function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = Object.create(foo.prototype); proto.bar_prop = "bar val"; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = Object.create(foo.prototype, { bar_prop: { value: "bar val" } }); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop) Support in all in-use-today browsers which are all non-microsoft browsers plus IE9 and up. Allows the direct setting of __proto__ in a way that is one-time-only so that the browser can better optimize the object. Also allows the creation of objects without a prototype via Object.create(null) . Not supported in IE8 and below. However, as Microsoft has discontinued extended support for systems running these old browsers, this should not be a concern for most applications. Additionally, the slow object initialization can be a performance black hole if using the second argument because each object-descriptor property has its own separate descriptor object. When dealing with hundreds of thousands of object descriptors in the form of object, there can arise a serious issue with lag.

    Object.setPrototypeOf

    function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = { bar_prop: "bar val" }; Object.setPrototypeOf(proto, foo.prototype); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto; proto=Object.setPrototypeOf({ bar_prop: "bar val" }, foo.prototype); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop) Support in all in-use-today browsers which are all non-microsoft browsers plus IE9 and up. Allows the dynamic manipulation of an objects prototype and can even force a prototype on a prototype-less object created with Object.create(null) . Should-be-deprecated and ill-performant. Making your Javascript run fast is completely out of the question if you dare use this in the final production code because many browsers optimize the prototype and try to guess the location of the method in the memory when calling an instance in advance, but setting the prototype dynamically disrupts all these optimizations and can even force some browsers to recompile for deoptimization your code just to make it work according to the specs. Not supported in IE8 and below.
    __proto__ function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = { bar_prop: "bar val", __proto__: foo.prototype }; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); var inst = { __proto__: { bar_prop: "bar val", __proto__: { foo_prop: "foo val", __proto__: Object.prototype } } }; console.log(inst.foo_prop); console.log(inst.bar_prop) Support in all in-use-today browsers which are all non-microsoft browsers plus IE11 and up. Setting __proto__ to something that is not an object only fails silently. It does not throw an exception. Grossly deprecated and non-performant. Making your Javascript run fast is completely out of the question if you dare use this in the final production code because many browsers optimize the prototype and try to guess the location of the method in the memory when calling an instance in advance, but setting the prototype dynamically disrupts all these optimizations and can even force some browsers to recompile for deoptimization your code just to make it work according to the specs. Not supported in IE10 and below.
    prototype and Object.getPrototypeOf

    JavaScript is a bit confusing for developers coming from Java or C++, as it"s all dynamic, all runtime, and it has no classes at all. It"s all just instances (objects). Even the "classes" we simulate are just a function object.

    You probably already noticed that our function A has a special property called prototype . This special property works with the JavaScript new operator. The reference to the prototype object is copied to the internal [] property of the new instance. For example, when you do var a1 = new A() , JavaScript (after creating the object in memory and before running function A() with this defined to it) sets a1.[] = A.prototype . When you then access properties of the instance, JavaScript first checks whether they exist on that object directly, and if not, it looks in [] . This means that all the stuff you define in prototype is effectively shared by all instances, and you can even later change parts of prototype and have the changes appear in all existing instances, if you wanted to.

    If, in the example above, you do var a1 = new A(); var a2 = new A(); then a1.doSomething would actually refer to Object.getPrototypeOf(a1).doSomething , which is the same as the A.prototype.doSomething you defined, i.e. Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething .

    In short, prototype is for types, while Object.getPrototypeOf() is the same for instances.

    [] is looked at recursively , i.e. a1.doSomething , Object.getPrototypeOf(a1).doSomething , Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething etc., until it"s found or Object.getPrototypeOf returns null.

    So, when you call

    Var o = new Foo();

    JavaScript actually just does

    Var o = new Object(); o.[] = Foo.prototype; Foo.call(o);

    (or something like that) and when you later do

    O.someProp;

    it checks whether o has a property someProp . If not, it checks Object.getPrototypeOf(o).someProp , and if that doesn"t exist it checks Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp , and so on.

    In conclusion

    It is essential to understand the prototypal inheritance model before writing complex code that makes use of it. Also, be aware of the length of the prototype chains in your code and break them up if necessary to avoid possible performance problems. Further, the native prototypes should never be extended unless it is for the sake of compatibility with newer JavaScript features.



  • Есть вопросы?

    Сообщить об опечатке

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