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

В этой статье мы изучим всё что касается прототипов в JavaScript. Разберём зачем они нужны, что такое наследование и цепочка прототипов, как работает this внутри методов, рассмотрим пример расширения классов и многое другое.
Что такое прототипы?
При создании объектов, например, с помощью конструктора, каждый из них будет содержать специальное внутреннее свойство [[Prototype]]
, указывающее на его прототип. В JavaScript прототипы используются для организации наследования.
Допустим у нас имеется конструктор Box
:
// конструктор Box
function Box(width, height) {
this.width = width;
this.height = height;
}
При объявлении конструктора или класса у него автоматически появится свойство prototype
. Оно содержит прототип. Прототип – это объект. В данном случае им будет являться Box.prototype
. Это очень важный момент.
Этот прототип будет автоматически назначаться всем объектам, которые будут создаваться с помощью этого конструктора:
// создание объекта с помощью конструктора Box
const box1 = new Box(25, 30);
Таким образом при создании объекта, в данном случае, box1
, он автоматически будет иметь ссылку на прототип, то есть на свойство Box.prototype
.
Это очень легко проверить:
Object.getPrototypeOf(box1) === Box.prototype // true
box1.__proto__ === Box.prototype // true

Получить прототип объекта в JavaScript можно с помощью статического метода Object.getPrototypeOf
или специального свойства __proto__
. В этом примере показаны два этих способа. Кстати, свойство __proto__
не является стандартным, но оно поддерживается всеми браузерами.
Свойство prototype
имеется у каждой функции за исключением стрелочных. Это свойство как мы уже отмечали выше в качестве значения имеет объект. По умолчанию в нём находится только одно свойство constructor
, которое содержит ссылку на саму эту функцию:
Box.prototype.constructor = Box // true
То есть Box.prototype.constructor
– это сам конструктор Box
.
Если создать ещё один объект класса Box
, то он тоже будет иметь точно такой же прототип. Как мы уже отмечали выше прототипы в JavaScript используются для организации наследования. То есть, если мы сейчас в Box.prototype
добавим какой-нибудь, например, метод, то он будет доступен для всех экземпляров класса Box
:
// создадим конструктор Box
function Box(width, height) {
this.width = width;
this.height = height;
}
// добавим метод print в прототип Box
Box.prototype.print = function () {
return `Box Size: ${this.width} x ${this.height}`
}
// создадим объекты
const box1 = new Box(25, 30);
const box2 = new Box(50, 70);
// выведем размеры ящиков в консоль
console.log(box1.print()); // Box Size: 25 x 30
console.log(box2.print()); // Box Size: 50 x 70

Обратите внимание, что метода print
нет у объектов box1
и box2
. Но если раскрыть значение свойства [[Prototype]]
в консоли в веб-браузере, то вы увидите его. То есть этот метод находится на уровне класса Box
и наследуется всеми его экземплярами.
Соответственно получается, что мы можем вызвать print
как метод объектов box1
и box2
. Таким образом нам доступны не только собственные свойства и методы, но также наследуемые. А наследование, как вы уже понимаете, осуществляется в JavaScript на основе прототипов.
Так что же такое прототип? Прототип в JavaScript – это просто ссылка на объект, который используется для наследования.
Наследование
Что такое наследование? Если после переменной, содержащей некоторый объект поставить точку, то вы увидите все доступные для него свойства и методы:

Здесь width
и height
– это его собственные свойства. Далее на уровне родительского класса находятся методы constructor
и print
. Т.е. вы можете вызвать метод print
, потому что он наследуется всеми экземплярами класса Box
. Кроме этого, здесь имеются методы класса Object
, такие как hasOwnProperty
, isPrototypeOf
, toString
и так далее. Эти методы тоже доступны, потому что Box.prototype
наследует все свойства и методы Object.prototype
.
Таким образом, кроме собственных свойств и методов объекту также доступны свойства и методы из прототипов. Такие свойства и методы называются наследованными.
Следовательно, в этом примере объект box1
имеет свои собственные свойства width
и height
, а также наследует все свойства и методы Box.prototype
и Object.prototype
.
Цепочка прототипов
В JavaScript наследование осуществляется только на уровне объектов через прототипы. То есть один объект имеет ссылку на другой через специальное внутреннее свойство [[Prototype]]
. Тот в свою очередь тоже имеет ссылку и т.д. В результате получается цепочка прототипов.
Таким образом наследование, которые мы рассмотрели выше на примере объекта box1
происходит благодаря существованию следующей цепочки прототипов:
box1 -> Box.prototype -> Object.prototype
Заканчивается цепочка на прототипе глобального класса Object
, потому что он не имеет прототипа, то есть его значение __proto__
равно null
.
При этом, когда мы пытаемся получить доступ к некоторому свойству или методу этого объекта, поиск всегда начинается с самого объекта. Если данного свойства или метода у него нет, то поиск перемещается в прототип, потом в прототип прототипа и так далее.
Если указанное свойство или метод не найден, то возвращается undefined
.
Например, если метод print
мы добавим в сам объект box1
, то будет использоваться уже он, а не тот, который находится в прототипе Box.prototype
:
box1.print = function() {
return `Размеры коробки: ${this.width} x ${this height}`;
}
Почему? Потому что поиск сразу прекращается, как только указанный метод будет найден. А в данном случае он будет найден сразу в объекте, поэтому переход в прототип не осуществится.
Значение this внутри методов
Значение this
внутри методов определяется только тем для какого объекта мы его вызываем.
Рассмотрим следующий пример:
function Counter() {
this.value = 0;
}
Counter.prototype.up = function() {
this.value++;
return this.value;
}
const counter1 = new Counter();
const counter2 = new Counter();
counter1.up(); // 1
counter1.up(); // 2
counter2.up(); // 1
Здесь мы вызываем up
как метод объектов counter1
и counter2
. Данный метод не является собственным для этих объектов, он наследуется и находится на уровне класса Counter
. Но на самом деле это не имеет значения. Единственное, что важно для this
– это только то, для какого объекта мы вызываем этот метод, то есть что стоит перед точкой. Это и будет this
.
При вызове counter1.up()
, this
внутри этого метода будет указывать на counter1
:
counter1.up();
На строчке перед точкой стоит counter2
, значит this
внутри up
будет указывать на него
:
counter2.up();
Таким образом, не важно, где находится метод, this
внутри него всегда будет указывать на объект, для которого он вызывается, и, следовательно, с помощью this
мы всегда сначала будем получать доступ к собственным свойствам и методам этого объекта.
Установка прототипа объекту
Установить прототип объекту можно с помощью статического метода Object.setPrototypeOf()
или свойства __proto__
.
Например, создадим два объекта и установим для второго объекта в качестве прототипа первый объект с помощью __proto__
:
const person1 = {
name: 'Tom',
printName() {
return `Name: ${this.name}`
}
}
const person2 = {
name: 'Bob',
__proto__: person1
}
Проверить что прототипом для person2
является person1
очень просто:
person2.__proto__ === person1 // true
Object.getPrototypeOf(person2) === person1 // true

При этом метод printName
становится наследуемым, то есть доступным для объекта person2
:
person2.printName(); // "Name: Bob"
Пример установки прототипа с помощью Object.setPrototypeOf()
:
const message = {
text: 'Сообщение 1...',
color: 'black',
getText() {
return `<div style="color: ${this.color};">${this.text}</div>`;
}
}
const errorMessage = {
text: 'Сообщение 2...',
color: 'red'
}
Object.setPrototypeOf(errorMessage, message);

В этом примере мы в качестве прототипа для errorMessage
установили message
.
Чтобы было более понятно как работает метод Object.setPrototypeOf
, рассмотрим его синтаксис:
Object.setPrototypeOf(obj, prototype)
Где:
obj
– объект, для которого необходимо установить прототип;prototype
– объект, который будет использоваться в качестве прототипа дляobj
, илиnull
, если уobj
не должно быть прототипа.
Очень важный момент заключается в том, что мы не можем указать в качестве прототипа объект, который уже имеется в цепочке, то есть замкнуть её.
Следовательно, мы получим ошибку, если попытаемся для message
установить в качестве прототипа errorMessage
:
Object.setPrototypeOf(message, errorMessage);
Кроме этого, в JavaScript нет множественного наследования, то есть нельзя одному объекту назначить несколько прототипов.
Наследование классов
Допустим у нас имеется конструктор Person
:
function Person(name, age) {
this.name = name;
this.age = age;
}
person.prototype.getName = function() {
return this.name;
}
Создадим конструктор Student
, который будет расширять класс Person
:
function Student(name, age, schoolName) {
// вызываем функцию, передавая ей в качестве this текущий объект
Person.call(this, name, age);
this.schoolName = schoolName;
}
Student.prototype.getSchoolName = function() {
return this.schoolName;
}
Но на данном этапе, они сейчас полностью независимы. Но для того чтобы класс Student
расширял Person
нужно указать, что прототипом для Student.prototype
является Person.prototype
. Например, выполним это с помощью свойства __proto__
:
Student.prototype.__proto__ = Person.prototype;
Создадим новый экземпляр класса Student
:
const student = new Student('Bob', 15, 'ABC School');
Для этого объекта будет доступен как метод getSchoolName
, так и getName
.
Рассмотрим ещё один очень интересный пример с наследованием классов:
// конструктор для создания объектов класса Rectangle
function Rectangle(x1, y1, x2, y2, bgColor) {
Rectangle.counter++;
this.id = `${this.constructor.name.toLowerCase()}-${Rectangle.counter}`;
[this.left, this.top] = [Math.min(x1, x2), Math.min(y1, y2)];
[this.width, this.height] = [Math.max(x1, x2) - this.left, Math.max(y1, y2) - this.top];
this.bgColor = bgColor;
}
// конструктор для создания объектов класса Square
function Square(x, y, side, bgColor) {
// вызываем конструктор Rectangle в контексте текущего создаваемого объекта
Rectangle.call(this, x, y, x + side, y + side, bgColor);
}
// конструктор для создания объектов класса Circle
function Circle(x, y, r, bgColor) {
Square.call(this, x, y, r, bgColor);
this.radius = r;
}
// счетчик количества созданных объектов
Rectangle.counter = 0;
// установим для Square.prototype прототип Rectangle.prototype
Square.prototype.__proto__ = Rectangle.prototype;
// установим для Circle.prototype прототип Square.prototype
Circle.prototype.__proto__ = Square.prototype;
// объявляем метод draw в прототипе Rectangle
Rectangle.prototype.draw = function() {
document.body.insertAdjacentHTML('beforeend', `<div id="${this.id}" style="position: absolute; left: ${this.left}px; top: ${this.top}px; width: ${this.width}px; height: ${this.height}px; background-color: ${this.bgColor};"></div>`);
}
// переопределяем метод draw в прототипе Circle
Circle.prototype.draw = function() {
const el = Rectangle.prototype.draw.call(this);
document.querySelector(`#${this.id}`).style.borderRadius = `${this.radius}px`;
}
// создадим новые объекты
const rectangle = new Rectangle(70, 80, 200, 300, '#673ab7');
const square = new Square(250, 50, 150, '#ff9800');
const circle = new Circle(450, 140, 90, '#4caf50');
// вызовем для каждого из них метод draw()
rectangle.draw();
square.draw();
circle.draw();
Здесь у нас имеются 3 класса: Rectangle
, Square
и Circle
. Для того чтобы объекты класса Square
наследовали свойства и методы Rectangle.prototype
мы прописали следующую связь:
Square.prototype.__proto__ = Rectangle.prototype;
Похожим образом мы это сделали также для объектов класса Circle
:
Circle.prototype.__proto__ = Square.prototype;
Таким образом, объекты, являющиеся экземплярами класса Circle
наследуют свойства и методы Circle.prototype
, Square.prototype
, Rectangle.prototype
и Object.prototype
. Это происходит благодаря следующей цепочки прототипов:
circle -> Circle.prototype -> Square.prototype -> Rectangle.prototype -> Object.prototype
Для вызова в Square
родительского конструктора мы используем метод call
. С помощью call
мы задаём контекст, в котором нам нужно вызвать функцию. В данном случае мы вызываем Rectangle
в контексте текущего создаваемого объекта.
Свойство constructor
У каждой функции, кроме стрелочных, как уже отмечали выше по умолчанию, имеется свойство prototype
. Как мы уже отмечали выше это прототип, который автоматически будут иметь все объекты, если мы эту функцию будет использовать как конструктор, то есть для создания объектов.
По умолчанию свойство prototype
функции содержит следующий объект:
function Article(title) {
this.title = title;
}
Article.prototype = {
constructor: Article
}
Здесь мы свойству prototype
присвоили объект вручную, но точно такой же генерируется автоматически. Этот объект изначально содержит только свойство constructor
, которое указывает на сам конструктор, то есть на функцию.
Свойство constructor
можно использовать для создания объектов:
const article1 = new Article('Про витамины');
const article2 = new article1.constructor('Про мёд');
Это может пригодиться, когда, например, объект был сделан вне вашего кода и вам необходимо создать новый подобный ему, то есть с использованием этого же конструктора.
Не рекомендуется полностью перезаписывать значение свойства prototype
, потому что в этом случае вы потеряете constructor
и его придётся добавлять вручную:
// так делать не нужно
Article.prototype = {
getTitle() {
return this.title;
}
}
Если нужно что-то добавить в prototype
, то делайте это как в примерах выше, то есть посредством простого добавления ему нужных свойств и методов:
Article.prototype.getTitle = function() {
return this.title;
}
Встроенные прототипы
В JavaScript практически всё является объектами. То есть функции, массивы, даты и так далее. Исключением являются только примитивные типы данных: строка, число и так далее.
Например, при создании объекта { name: 'Tom' }
внутренне используется конструктор Object
:
const person = { name: 'Tom' }
Прототипом такого объекта соответственно становится Object.prototype
и в этом легко убедиться:
person.__proto__ === Object.prototype // true
Поэтому нам на уровне этого объекта доступны различные методы, они берутся из Object.prototype
. Например, метод hasOwnProperty
:
person.hasOwnProperty('name'); // true
Этот метод возвращает true
, когда указанное свойство является для этого объекта родным. В противном случае – false
.
При этом Object.prototype
является корнем иерархии других встроенных прототипов. Но при этом он сам не имеет прототипа.

На рисунке видно, что конструктор Object
имеет по умолчанию свойство prototype
. Это значение будет автоматически записываться в свойство [[Prototype]]
объектов, которые будет создаваться с помощью этого конструктора. В Object.prototype
имеется свойство constructor
, которые указывает на сам конструктор. Эти связи между Object
и Object.prototype
показаны на схеме. Кроме этого Object.prototype
не имеет прототипа. То есть его значение [[Prototype]]
содержит null
.
Теперь давайте рассмотрим, как выполняется создание даты в JavaScript. Осуществляется это очень просто посредством конструктора Date
:
const now = new Date();

Следовательно, прототипом даты является Date.prototype
:
now.__proto__ === Date.prototype // true
Этот прототип содержит большое количество методов для работы с датой, например, такие как getDate
, getHours
и так далее. Их нет в now
, но они доступны нам посредством наследования.
Объект Date.prototype
имеет в качестве прототипа Object.prototype
:
Date.prototype.__proto__ === Object.prototype // true
// или так
now.__proto__.__proto__ === Object.prototype // true
Следовательно, методы Object.prototype
, которых нет в Date.prototype
также доступны для now
. Например, hasOwnProperty
:
now.hasOwnProperty('getYear') // false
Таким образом можно нарисовать следующую схему:

Другие встроенные объекты устроены подобным образом.
Метод Object.create
Object.create
предназначен для создания нового объекта, который будет иметь в качестве прототипа объект, переданный в этот метод в качестве аргумента:
const rect1 = {
a: 8,
b: 5,
calcArea() {
return this.a * this.b
}
}
// создали новый объект, который будет иметь в качестве прототипа rect1
const rect2 = Object.create(rect1);
rect2.a = 10;
rect2.b = 5;
rect2.calcArea(); // 50
Создание объекта с прототипом Object.prototype
:
const obj = Object.create(Object.prototype);
Данный пример аналогичен этому:
const obj = {};
Создание объекта без прототипа:
const obj = Object.create(null);
Во 2 аргументе мы можем объекту сразу передать необходимые свойства. Описываются они в полном формате с использованием специальных атрибутов как в Object.defineProperties
:
const person = Object.create(null, {
name: {
value: 'John'
},
age: {
value: '18',
writable: true
}
});
Здесь мы описали два свойства: name
и age
. С помощью value
мы устанавливаем значение свойству, а посредством аргумента writable
задаем доступно ли свойство для изменения.
Пример, в котором мы с помощью Object.create
установим для Book.prototype
в качестве прототипа объект Product.prototype
:
// конструктор Product
function Product(params) {
this.name = params.name;
this.price = params.price;
this.discount = params.discount;
}
// конструктор Book
function Book(params) {
Product.call(this, params);
this.isbn = params.isbn;
this.author = params.author;
this.totalPages = params.totalPages;
}
// устанавливаем для Book.prototype в качестве прототипа объект Product.prototype
Book.prototype = Object.create(Product.prototype, {
constructor: {
value: Book
},
// геттер
fullName: {
get() {
return `${this.author}: ${this.name}`;
}
}
});
Product.prototype.calcDiscountedPrice = function() {
return (this.price * (1 - this.discount / 100)).toFixed(2);
}
// создадим новый объект
const book = new Book({
name: 'Краткая история времени',
author: 'Стивен Хокинг',
isbn: '978-5-17102-284-6',
totalPages: 232,
price: 611.00,
discount: 5
});
// цена книги со скидкой
console.log(book.calcDiscountedPrice()); // 580.45
Здесь мы Object.create
используем для создания нового объекта, который будет использоваться в качестве прототипа для Book.prototype
. При этом свою очередь этот новый объект будет иметь в качестве прототипа Product.prototype
. Для этого мы передаем его в качестве аргумента методу Object.create
. Кроме этого к этому объекту мы сразу же добавили два свойства: constructor
и fullName
. С помощью constructor
мы восстановили конструктор, который был в прототипе пока мы ему не присвоили новое значение. А динамическое свойство fullName
, представляющее собой геттер, будем использовать для получения имени книги с автором.