Javascript наследование. JavaScript — шаблоны наследования

02.04.2019

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

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 является одной из самых тяжелых для новичков. С добавлением нового синтаксиса с ключевым словом class, понимание наследования явно не стало проще, хотя кардинально нового ничего не появилось. В данной статье не затрагиваются нюансы реализации прототипного наследования в JavaScript, поэтому если у читателя возникли вопросы, то рекомендую прочитать следующие статьи: Основы и заблуждения насчет JavaScript и Понимание ООП в JavaScript [Часть 1]

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

JavaScript является очень мощным языком. Настолько мощным, что в нем сосуществует множество различных способов проектирования и создания объектов. У каждого способа есть свои плюсы и минусы и я бы хотел помочь новичкам разобраться в этом. Это продолжение моего предыдущего поста, Хватит «классифицировать» JavaScript . Я получил много вопросов и комментариев с просьбами привести примеры, и для именно этой цели я решил написать эту статью.

JavaScript использует прототипное наследование

Это означает, что в JavaScript объекты наследуются от других объектов. Простые объекты в JavaScript, созданные с использованием {} фигурных скобок, имеют только один прототип: Object.prototype . Object.prototype , в свою очередь тоже объект, и все свойства и методы Object.prototype доступны для всех объектов.

Массивы, созданные с помощью квадратных скобок, имеют несколько прототипов, в том числе Object.prototype и Array.prototype . Это означает, что все свойства и методы Object.prototype и Array.prototype доступны для всех массивов. Одноименные свойства и методы, например .valueOf и .ToString , вызываются из ближайшего прототипа, в этом случае из Array.prototype .

Определения прототипа и создание объектов

Способ 1: Шаблон конструктор

JavaScript имеет особый тип функции называемых конструкторами, которые действуют так же, как и конструкторы в других языках. Функции-конструкторы вызываются только с помощью ключевого слова new и связывают создаваемый объект с контекстом функции-конструктора через ключевое слово this . Типичный конструктор может выглядеть следующим образом:
function Animal(type){ this.type = type; } Animal.isAnimal = function(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; }; function Dog(name, breed){ Animal.call(this, "dog"); this.name = name; this.breed = breed; } Object.setPrototypeOf(Dog.prototype, Animal.prototype); Dog.prototype.bark = function(){ console.log("ruff, ruff"); }; Dog.prototype.print = function(){ console.log("The dog " + this.name + " is a " + this.breed); }; Dog.isDog = function(obj){ return Animal.isAnimal(obj, "dog"); };
Использование этого конструктора выглядит также как и создание объекта в других языках:
var sparkie = new Dog("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true
bark и print методы прототипа, которые применяются для всех объектов созданных с помощью конструктора Dog . Свойства name и breed инициализируются в конструкторе. Это общепринятая практика, когда все методы определяются в прототипе, а свойства инициализируются конструктором.

Способ 2: Определение класса в ES2015 (ES6)

Ключевое слово class было зарезервировано в JavaScript с самого начала и вот наконец-то пришло время его использовать. Определения классов в JavaScript схоже с другими языками.
class Animal { constructor(type){ this.type = type; } static isAnimal(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; } } class Dog extends Animal { constructor(name, breed){ super("dog"); this.name = name; this.breed = breed; } bark(){ console.log("ruff, ruff"); } print(){ console.log("The dog " + this.name + " is a " + this.breed); } static isDog(obj){ return Animal.isAnimal(obj, "dog"); } }
Многие люди считают этот синтаксис удобным, потому что он объединяет в одном блоке конструктор и объявление статичных и прототипных методов. Использование точно такое же, как и в предыдущем способе.
var sparkie = new Dog("Sparkie", "Border Collie");

Способ 3: Явное объявление прототипа, Object.create, фабричный метод

Этот способ показывает, что на самом деле новый синтаксис с ключевым словом class использует прототипное наследование. Также этот способ позволяет создать новый объект без использования оператора new .
var Animal = { create(type){ var animal = Object.create(Animal.prototype); animal.type = type; return animal; }, isAnimal(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; }, prototype: {} }; var Dog = { create(name, breed){ var proto = Object.assign(Animal.create("dog"), Dog.prototype); var dog = Object.create(proto); dog.name = name; dog.breed = breed; return dog; }, isDog(obj){ return Animal.isAnimal(obj, "dog"); }, prototype: { bark(){ console.log("ruff, ruff"); }, print(){ console.log("The dog " + this.name + " is a " + this.breed); } } };
Этот синтаксис удобен, потому что прототип объявляется явно. Понятно что определено в прототипе, а что определено в самом объекте. Метод Object.create удобен, потому что он позволяет создать объект от указанного прототипа. Проверка с помощью .isPrototypeOf по-прежнему работает в обоих случаях. Использование разнообразно, но не чрезмерно:
var sparkie = Dog.create("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true

Способ 4: Object.create, фабрика верхнего уровня, отложенный прототип

Этот способ является небольшим изменение способа 3, где сам класс является фабрикой, в отличии от случая когда класс является объектом с фабричным методом. Похоже, на пример конструктора (способ 1), но использует фабричный метод и Object.create .
function Animal(type){ var animal = Object.create(Animal.prototype); animal.type = type; return animal; } Animal.isAnimal = function(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; }; Animal.prototype = {}; function Dog(name, breed){ var proto = Object.assign(Animal("dog"), Dog.prototype); var dog = Object.create(proto); dog.name = name; dog.breed = breed; return dog; } Dog.isDog = function(obj){ return Animal.isAnimal(obj, "dog"); }; Dog.prototype = { bark(){ console.log("ruff, ruff"); }, print(){ console.log("The dog " + this.name + " is a " + this.breed); } };
Этот способ интересен тем, что похож на первой способ, но не требует ключевого слова new и работает с оператором instanceOf . Использование такое же, как и в первом способе, но без использования ключевого слова new :
var sparkie = Dog("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true

Сравнение

Способ 1 против Способа 4

Существует довольно мало причин, для того чтобы использовать Способ 1 вместо Способа 4. Способ 1 требует либо использование ключевого слова new , либо добавление следующей проверки в конструкторе:
if(!(this instanceof Foo)){ return new Foo(a, b, c); }
В этом случае проще использовать Object.create с фабричным методом. Вы также не можете использовать функции Function#call или Function#apply с функциями-конструкторами, потому что они переопределяют контекст ключевого слова this . Проверка выше, может решить и эту проблему, но если вам нужно работать с неизвестным заранее количеством аргументов, вы должны использовать фабричный метод.

Способ 2 против Способа 3

Те же рассуждения о конструкторах и операторе new , что были упомянуты выше, применимы и в этом случае. Проверка с помощью instanceof необходима, если используется новый синтаксис class без использования оператора new или используются Function#call или Function#apply .

Мое мнение

Программист должен стремиться к ясности своего кода. Синтаксис Способа 3 очень четко показывает, что именно происходит на самом деле. Он также позволяет легко использовать множественное наследование и стековое наследования. Так как оператор new нарушает принцип открытости/закрытости из-за несовместимости с apply или call , его следует избегать. Ключевое слово class скрывает прототипный характер наследования в JavaScript за маской системы классов.
«Простое лучше мудреного», и использование классов, потому что оно считается более «изощренным» является просто ненужной, технической головомойкой.

Использование Object.create является более выразительным и ясным, чем использование связки new и this . Кроме того, прототип хранится в объекте, который может быть вне контекста самой фабрики, и таким образом может быть более легко изменен и расширен добавлением методов . Прям как классы в ES6.
Ключевое слово class , возможно будет наиболее пагубной чертой в JavaScript. Я испытываю огромное уважение к блестящим и очень трудолюбивым людям, которые были вовлечены в процесс написания стандарта, но даже блестящие люди иногда делают неправильные вещи. - Eric Elliott

Добавление чего-то ненужного и возможно пагубного, противоречащего самой природе языка является необдуманным и ошибочным.
Если вы решите использовать class , я искренне надеюсь, что мне никогда не придется работать с вашим кодом. На мой взгляд, разработчики должны избегать использования конструкторов, class и new , и использовать методы, которые более естественны парадигме и архитектуре языка.

Глоссарий

Object.assign(a, b) копирует все перечислимые (enumerable) свойства объекта b в объект a , а затем возвращает объект a
Object.create(proto) создает новый объект от указанного прототипа proto
Object.setPrototypeOf(obj, proto) меняет внутреннее свойство [] объекта obj на proto

Теги: Добавить метки

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:

После выхода окончательной версии спецификации ECMA Script 2015 (ES2015 ) сообщество получило возможность двигаться в направлении ее реализации в движках JavaScript .

ES2015 предлагает для существующих функций множество новых полезных возможностей и более чистый синтаксис. Например, ключевое слово class , а также улучшенный синтаксис для JavaScript prototype .

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

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

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

Наследование прототипов в JavaScript предполагает, что один объект наследуется от другого объекта, вместо того, чтобы одна спецификация наследовалась от другой. Даже ключевое слово нового класса является некорректным, потому что подразумевает спецификацию. Но на самом деле один объект наследуется от другого. Синтаксис в более ранних версиях JavaScript был слишком сложным, и ему трудно было следовать. Поэтому, как только разработчики принимают наследование от объекта к объекту, возникает вторая задача. Она состоит в том, чтобы улучшить синтаксис JavaScript prototype наследования — ввести классы ES2015 .

ES2015 классы в JavaScript

Данная спецификация обеспечивает более четкий синтаксис для определения структур классов, создания функций конструктора, расширения классов, вызова конструктора и функций в супер классе, а также предоставляет статические функции. Также ES2015 улучшает синтаксис для создания стиля ES5 получателя / установщика дескриптора свойств, что позволяет разработчикам использовать эти малоизвестные возможности спецификации.

Определения классов

JavaScript не содержит классов. Даже классы ES2015 это не совсем классы в традиционном смысле этого слова. А всего лишь «вычищенный » синтаксис для создания наследования прототипов между объектами. Но поскольку ES2015 использует термин «класс » для объектов, созданных с помощью функции конструктора (функция-конструктор является конечным результатом ключевого слова class ), в этой статье мы будем использовать термин «класс «, чтобы описать не только классы ES2015 , но и ES5 .

В версии ES5 и более ранних функции конструктора определяли «классы » следующим образом:

function MyClass() { } var myClass = new MyClass();

В ES2015 был введен новый синтаксис, с использованием ключевого слова class :

class MyClass { constructor() { } } var myClass = new MyClass();

Функция конструктора осталась той же, что определена в ES5 . В обернутом блоке ключевого слова class определяются свойства для JavaScript function prototype . Синтаксис ключевого слова new для установки нового экземпляра класса остался неизменным.

С введением ключевого слова class появляется объект функции, который используется ES5 . Рассмотрим следующий выходной результат среды Node.js REPL . Во-первых, мы определяем новый класс, а затем оператор TypeOf перечисляет типы объекта класса:

> class MyClass { constructor() {} } class MyClass { constructor() {} } > typeof MyClass "function" >

В ES2015 роль и назначение функции конструктора не пересматривались, для нее просто был «вычищен » синтаксис.

Что такое конструкторы в JavaScript?

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

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

function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }

Аналог функции конструктора с синтаксисом ES2015 выглядит следующим образом:

// имя функции конструктора ES5 - // это имя класса ES2015 class Person { // обратите внимание, что здесь нет ключевого слова "function" // также используется слово "constructor", а не "Person" constructor(firstName, lastName) { // этот код представляет новый созданный и // инициализированный объект this.firstName = firstName; this.lastName = lastName; } }

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

Чтобы установить объект с тем же синтаксисом, код должен быть тот же:

var person = new Person("Bob", "Smith"); // выводит "Bob" console.log(person.firstName); // выводит "Smith" console.log(person.lastName);

Нажмите здесь, чтобы загрузить код

Расширение классов

До ES2015 большинство разработчиков не понимали, как реализовать наследования между объектами и использовать JavaScript prototype . Пообщавшись с разработчиками на C ++ , Java или C # , вы поймете, с какой легкостью они настраивают наследование одного класса от другого, а затем создают экземпляр объекта из подкласса. Попросите JavaScript разработчика продемонстрировать, как происходит наследование между двумя объектами, и в ответ вы увидите пустой взгляд.

Настройка наследования прототипов является непростым делом, а понятие наследования прототипа является неизвестным для большинства JavaScript разработчиков. Вот несколько примеров кода с комментариями, которые поясняют процесс настройки наследования:

// вызывается с оператором "new", // создается новый объект Person function Person(firstName, lastName) { // оператор "new" устанавливает связь // от "this" к новому объекту this.firstName = firstName; this.lastName = lastName; } // это свойство, связывающее функцию, // конфигурируется для объекта прототипа Person, // и наследуется Student Person.prototype.getFullName = function() { return this.firstName + " " + this.lastName; }; // Когда функция конструктора Student // вызывается с оператором "new", // создается новый объект Student function Student(studentId, firstName, lastName) { // оператор "new" устанавливает связь от "this" к // новому объекту, новый объект затем передается в // функцию конструктора Person через использование вызова, // таким образом могут быть установлены свойства имени и фамилии this._super.call(this, firstName, lastName); this.studentId = studentId; } // Student наследуются от нового объекта, // который наследуется от родительского Student.prototype = Object.create(Person.prototype); // устанавливаем свойства конструктора обратно для // функции конструктора Student Student.prototype.constructor = Student; // "_super" НЕ является частью ES5, его конвенция, определенная // разработчиком, устанавливает // "_super" для функции конструктора Person Student.prototype._super = Person; // это будет существовать в прототипе объекта студента Student.prototype.getStudentInfo = function() { return this.studentId + " " + this.lastName + ", " + this.firstName; }; // устанавливаем новый объект Student var student = new Student(1, "Bob", "Smith"); // вызываем функцию в выводе родительского // прототипа "Bob Smith" console.log(student.getFullName()); // вызываем функцию в выводе родительского // прототипа "1 Smith, Bob" console.log(student.getStudentInfo());

Нажмите здесь, чтобы загрузить код

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

Чтобы решить эту проблему, в новом синтаксисе структуры классов в ES2015 было введено ключевое слово extends . В следующем коде продемонстрировано то же наследование, что и в первом примере кода, но с использованием синтаксиса ES2015 для JavaScript object prototype :

"use strict"; class Person { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } getFullName() { return this.firstName + " " + this.lastName; } } class Student extends Person { constructor(studentId, firstName, lastName) { super(firstName, lastName); this.studentId = studentId; } getStudentInfo() { return this.studentId + " " + this.lastName + ", " + this.firstName; } } var student = new Student(1, "Bob", "Smith"); console.log(student.getFullName()); console.log(student.getStudentInfo());

Нажмите здесь, чтобы загрузить код

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

Другой способ изучить, как это работает — рассмотреть код наследования ES5 , сгенерированный TypeScript . TypeScript – это препроцессорный язык, который оптимизирует JavaScript через строгую типизацию и транспиллинг кода ES2015 в код ES5 . Транспилинг — это процесс компиляции исходного кода одного языка программирования в исходный код другого языка.

Функция _extends в JavaScript

Для поддержки наследования классов ES2015 TypeScript транспилирует функционал ключевого слова extends в функцию с именем __extends , которая запускает код, необходимый для настройки наследования. Вот код функции __extends :

var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); };

Приведенный выше код немного труден, поэтому ниже приводится его расширенная, задокументированная версия. Чтобы понять назначение каждой строки кода, прочтите комментарии, добавленные в исходный код JavaScript prototype . Функция __extends работает с любой парой родительских и дочерних объектов:

// объявляем переменную, чтобы связать функцию extends var __extends; if (this && this.__extends) { // функция extends уже определена в контексте // этого кода, поэтому используйте существующую функцию __extends __extends = this.__extends; } else {

Остальное содержимое блока — это реализация функции __extends . Она использует как шаблон примеси и JavaScript prototype наследование, чтобы построить взаимосвязь наследования между родительским и дочерним объектами. Шаблон примеси копирует свойства из одного объекта в другой. Приведенный ниже код обрабатывается через функцию __extends :

// функция extends еще не определена в текущем контексте; // поэтому определяем ее __extends = function (child, parent) { // шаблон примеси для копирования свойств функции родительского конструктора // в качестве статических свойств для свойств функции дочернего конструктора // в функции конструктора часто называют статическим свойством for (var parentPropertyName in parent) { // только скопированные свойства отдельно определяются для родителя if (parent.hasOwnProperty(parentPropertyName)) { // для простейших типов этот код копирует значения, // для типов объектов этот код копирует только связи child = parent; } } // функция конструктора для объекта, который установил дочерний объект, // наследуемый из этой функции, // является уникальной внутри контекста каждого вызова extend function __() { this.constructor = child; } if (parent === null) { // объект, установленный с помощью дочерней функции конструктора, // наследуется от объекта, который в свою очередь не наследуется ни от чего, // даже не от встроенного JavaScript Object child.prototype = Object.create(parent); } else { // назначаем свойства прототипа родительской функции конструктора // свойствам прототипа функции конструктора, определенной выше __.prototype = parent.prototype; // создаем объект, от которого наследуются все дочерние экземпляры, // и назначаем его свойству прототипа дочерней функции // конструктора child.prototype = new __(); } };

Следующие две строки кода сбивают с толку многих разработчиков:

// назначаем свойство прототипа родительской функции конструктора // свойству прототипа функции конструктора, определенной выше __.prototype = parent.prototype; // создаем объект, от которого наследуются все дочерние экземпляры // и назначаем его свойству прототипа дочерней // функции конструктора child.prototype = new __();

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

// Этот код не даст нужного результата child.prototype = parent.prototype;

Разработчики ошибочно полагают, что дочерний объект теперь будет наследоваться от объекта прототипа родительской функции конструктора. Но на самом деле объекты, созданные с помощью родительской функции конструктора, а также объекты, созданные с помощью дочерней функции конструктора, наследуются от точно такого же JavaScript object prototype . Это нежелательно, так как свойство прототипа дочерней функции конструктора не может быть изменено без одновременного изменения свойства прототипа родительской функции конструктора. Поэтому все изменения, внесенные в дочерний объект, будут также применены к родителю. Это некорректное наследование:

При создании нового экземпляра объекта с помощью оператора new и родительской или дочерней функции конструктора, полученные объекты будут наследоваться от того же объекта-прототипа (РРО ). Установленные родительские и дочерние объекты являются объектами одного уровня с РРО в качестве родителя. Дочерний объект не наследуется от родителя.

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

Prototype = parent.prototype; child.prototype = new __();


С помощью этой новой структуры новые дочерние объекты наследуются от CPO (объект прототипа потомков ), который наследуется от РРО . Новые свойства могут быть добавлены в CPO , который не влияет на РРО . Новые родительские объекты наследуются от РРО , и не зависят от изменений в СРО . Изменения в РРО будут унаследованы объектом, созданным как с помощью родительской, так и с помощью дочерней функций конструктора. С помощью этой новой структуры дочерние объекты наследуются от родителя.

И в конце закрывающая фигурная скобка относится к изначальному блоку if :

Нажмите здесь, чтобы загрузить код

Синтаксис ES2015 для расширения классов гораздо более прост для понимания JavaScript prototype . Он содержит два новых ключевых слов: extends и super . Ключевое слово extends устанавливает отношения наследования прототипа между родительскими и дочерними классами. Ключевое слово super вызывает конструктор для класса родителя (он же суперкласс ). Вызов функции super требуется, даже если родительский объект не содержит конфигурацию.

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.