Наследование на уровне объектов в JavaScript, как мы видели, реализуется через ссылку __proto__
.
Теперь поговорим о наследовании на уровне классов, то есть когда объекты, создаваемые, к примеру, через new Admin
, должны иметь все методы, которые есть у объектов, создаваемых через new User
, и ещё какие-то свои.
Наследование Array от Object
Для реализации наследования в наших классах мы будем использовать тот же подход, который принят внутри JavaScript.
Взглянем на него ещё раз на примере Array
, который наследует от Object
:

- Методы массивов
Array
хранятся вArray.prototype
. Array.prototype
имеет прототипомObject.prototype
.
Поэтому когда экземпляры класса Array
хотят получить метод массива – они берут его из своего прототипа, например Array.prototype.slice
.
Если же нужен метод объекта, например, hasOwnProperty
, то его в Array.prototype
нет, и он берётся из Object.prototype
.
Отличный способ «потрогать это руками» – запустить в консоли команду console.dir([1,2,3])
.
Вывод в Chrome будет примерно таким:

Здесь отчётливо видно, что сами данные и length
находятся в массиве, дальше в __proto__
идут методы для массивов concat
, то есть Array.prototype
, а далее – Object.prototype
.
console.dir
для доступа к свойствамОбратите внимание, я использовал именно console.dir
, а не console.log
, поскольку log
зачастую выводит объект в виде строки, без доступа к свойствам.
Наследование в наших классах
Применим тот же подход для наших классов: объявим класс Rabbit
, который будет наследовать от Animal
.
Вначале создадим два этих класса по отдельности, они пока что будут совершенно независимы.
Animal
:
function Animal(name) {
this.name = name;
this.speed = 0;
}
Animal.prototype.run = function(speed) {
this.speed += speed;
alert( this.name + ' бежит, скорость ' + this.speed );
};
Animal.prototype.stop = function() {
this.speed = 0;
alert( this.name + ' стоит' );
};
Rabbit
:
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
Rabbit.prototype.jump = function() {
this.speed++;
alert( this.name + ' прыгает' );
};
var rabbit = new Rabbit('Кроль');
Для того, чтобы наследование работало, объект rabbit = new Rabbit
должен использовать свойства и методы из своего прототипа Rabbit.prototype
, а если их там нет, то – свойства и методы родителя, которые хранятся в Animal.prototype
.
Если ещё короче – порядок поиска свойств и методов должен быть таким: rabbit -> Rabbit.prototype -> Animal.prototype
, по аналогии с тем, как это сделано для объектов и массивов.
Для этого можно поставить ссылку __proto__
с Rabbit.prototype
на Animal.prototype
.
Можно сделать это так:
Rabbit.prototype.__proto__ = Animal.prototype;
Однако, прямой доступ к __proto__
не поддерживается в IE10-, поэтому для поддержки этих браузеров мы используем функцию Object.create
. Она либо встроена либо легко эмулируется во всех браузерах.
Класс Animal
остаётся без изменений, а Rabbit.prototype
мы будем создавать с нужным прототипом, используя Object.create
:
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
// задаём наследование
Rabbit.prototype = Object.create(Animal.prototype);
// и добавим свой метод (или методы...)
Rabbit.prototype.jump = function() { ... };
Теперь выглядеть иерархия будет так:

В prototype
по умолчанию всегда находится свойство constructor
, указывающее на функцию-конструктор. В частности, Rabbit.prototype.constructor == Rabbit
. Если мы рассчитываем использовать это свойство, то при замене prototype
через Object.create
нужно его явно сохранить:
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;
Полный код наследования
Для наглядности – вот итоговый код с двумя классами Animal
и Rabbit
:
// 1. Конструктор Animal
function Animal(name) {
this.name = name;
this.speed = 0;
}
// 1.1. Методы -- в прототип
Animal.prototype.stop = function() {
this.speed = 0;
alert( this.name + ' стоит' );
}
Animal.prototype.run = function(speed) {
this.speed += speed;
alert( this.name + ' бежит, скорость ' + this.speed );
};
// 2. Конструктор Rabbit
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
// 2.1. Наследование
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;
// 2.2. Методы Rabbit
Rabbit.prototype.jump = function() {
this.speed++;
alert( this.name + ' прыгает, скорость ' + this.speed );
}
Как видно, наследование задаётся всего одной строчкой, поставленной в правильном месте.
Обратим внимание: Rabbit.prototype = Object.create(Animal.prototype)
присваивается сразу после объявления конструктора, иначе он перезатрёт уже записанные в прототип методы.
Rabbit.prototype = new Animal
В некоторых устаревших руководствах предлагают вместо Object.create(Animal.prototype)
записывать в прототип new Animal
, вот так:
// вместо Rabbit.prototype = Object.create(Animal.prototype)
Rabbit.prototype = new Animal();
Частично, он рабочий, поскольку иерархия прототипов будет такая же, ведь new Animal
– это объект с прототипом Animal.prototype
, как и Object.create(Animal.prototype)
. Они в этом плане идентичны.
Но у этого подхода важный недостаток. Как правило мы не хотим создавать Animal
, а хотим только унаследовать его методы!
Более того, на практике создание объекта может требовать обязательных аргументов, влиять на страницу в браузере, делать запросы к серверу и что-то ещё, чего мы хотели бы избежать. Поэтому рекомендуется использовать вариант с Object.create
.
Вызов конструктора родителя
Посмотрим внимательно на конструкторы Animal
и Rabbit
из примеров выше:
function Animal(name) {
this.name = name;
this.speed = 0;
}
function Rabbit(name) {
this.name = name;
this.speed = 0;
}
Как видно, объект Rabbit
не добавляет никакой особенной логики при создании, которой не было в Animal
.
Чтобы упростить поддержку кода, имеет смысл не дублировать код конструктора Animal
, а напрямую вызвать его:
function Rabbit(name) {
Animal.apply(this, arguments);
}
Такой вызов запустит функцию Animal
в контексте текущего объекта, со всеми аргументами, она выполнится и запишет в this
всё, что нужно.
Здесь можно было бы использовать и Animal.call(this, name)
, но apply
надёжнее, так как работает с любым количеством аргументов.
Переопределение метода
Итак, Rabbit
наследует Animal
. Теперь если какого-то метода нет в Rabbit.prototype
– он будет взят из Animal.prototype
.
В Rabbit
может понадобиться задать какие-то методы, которые у родителя уже есть. Например, кролики бегают не так, как остальные животные, поэтому переопределим метод run()
:
Rabbit.prototype.run = function(speed) {
this.speed++;
this.jump();
};
Вызов rabbit.run()
теперь будет брать run
из своего прототипа:

Вызов метода родителя внутри своего
Более частая ситуация – когда мы хотим не просто заменить метод на свой, а взять метод родителя и расширить его. Скажем, кролик бежит так же, как и другие звери, но время от времени подпрыгивает.
Для вызова метода родителя можно обратиться к нему напрямую, взяв из прототипа:
Rabbit.prototype.run = function() {
// вызвать метод родителя, передав ему текущие аргументы
Animal.prototype.run.apply(this, arguments);
this.jump();
}
Обратите внимание на вызов через apply
и явное указание контекста.
Если вызвать просто Animal.prototype.run()
, то в качестве this
функция run
получит Animal.prototype
, а это неверно, нужен текущий объект.
Итого
-
Для наследования нужно, чтобы «склад методов потомка» (
Child.prototype
) наследовал от «склада метода родителей» (Parent.prototype
).Это можно сделать при помощи
Object.create
:Код:
Rabbit.prototype = Object.create(Animal.prototype);
-
Для того, чтобы наследник создавался так же, как и родитель, он вызывает конструктор родителя в своём контексте, используя
apply(this, arguments)
, вот так:function Rabbit(...) { Animal.apply(this, arguments); }
-
При переопределении метода родителя в потомке, к исходному методу можно обратиться, взяв его напрямую из прототипа:
Rabbit.prototype.run = function() { var result = Animal.prototype.run.apply(this, ...); // result -- результат вызова метода родителя }
Структура наследования полностью:
// --------- Класс-Родитель ------------
// Конструктор родителя пишет свойства конкретного объекта
function Animal(name) {
this.name = name;
this.speed = 0;
}
// Методы хранятся в прототипе
Animal.prototype.run = function() {
alert(this.name + " бежит!");
}
// --------- Класс-потомок -----------
// Конструктор потомка
function Rabbit(name) {
Animal.apply(this, arguments);
}
// Унаследовать
Rabbit.prototype = Object.create(Animal.prototype);
// Желательно и constructor сохранить
Rabbit.prototype.constructor = Rabbit;
// Методы потомка
Rabbit.prototype.run = function() {
// Вызов метода родителя внутри своего
Animal.prototype.run.apply(this);
alert( this.name + " подпрыгивает!" );
};
// Готово, можно создавать объекты
var rabbit = new Rabbit('Кроль');
rabbit.run();
Такое наследование лучше функционального стиля, так как не дублирует методы в каждом объекте.
Кроме того, есть ещё неявное, но очень важное архитектурное отличие.
Зачастую вызов конструктора имеет какие-то побочные эффекты, например влияет на документ. Если конструктор родителя имеет какое-то поведение, которое нужно переопределить в потомке, то в функциональном стиле это невозможно.
Иначе говоря, в функциональном стиле в процессе создания Rabbit
нужно обязательно вызывать Animal.apply(this, arguments)
, чтобы получить методы родителя – и если этот Animal.apply
кроме добавления методов говорит: «Му-у-у!», то это проблема:
function Animal() {
this.walk = function() {
alert('walk');
};
alert( 'Му-у-у!' );
}
function Rabbit() {
Animal.apply(this, arguments); // как избавиться от мычания, но получить walk?
}
…Которой нет в прототипном подходе, потому что в процессе создания new Rabbit
мы вовсе не обязаны вызывать конструктор родителя. Ведь методы находятся в прототипе.
Поэтому прототипный подход стоит предпочитать функциональному как более быстрый и универсальный. А что касается красоты синтаксиса – она сильно лучше в новом стандарте ES6, которым можно пользоваться уже сейчас, если взять транслятор babeljs.
Комментарии
<code>
, для нескольких строк кода — тег<pre>
, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)