JavaScript – это язык, основанный на прототипах. Это значит, что свойства и методы объектов можно повторно использовать посредством общих объектов, которые можно клонировать и расширять. Это называется наследованием прототипов и отличается от наследования классов. Среди популярных объектно-ориентированных языков программирования JavaScript относительно уникален, поскольку другие известные языки (PHP, Python и Java) являются языками на основе классов, которые в качестве макетов для объектов используют классы вместо прототипов.
В этом мануале вы узнаете, что такое прототипы объектов, наследование и цепочки прототипов и как использовать функцию-конструктор для расширения прототипов в новых объектах.
Прототипы в JavaScript
В мануале Объекты в JavaScript вы уже ознакомились с объектами и научились создавать их, изменять и извлекать их свойства. Теперь вы научитесь использовать прототипы для расширения объектов.
Каждый объект в JavaScript имеет внутреннее свойство, называемое [[Prototype]]. Для примера попробуйте создать новый пустой объект.
let x = {};
Так создается объект обычно, но есть и другой способ сделать это – с помощью конструктора объекта: let x = new Object().
Примечание: Двойные квадратные скобки в [[Prototype]] означают, что свойство является внутренним и не может быть доступно непосредственно в коде.
Чтобы найти свойство [[Prototype]] этого нового объекта, нужно использовать метод getPrototypeOf ().
Object.getPrototypeOf(x);
Вывод будет состоять из нескольких встроенных свойств и методов.
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Еще один способ найти [[Prototype]] – это свойство __proto__, которое предоставляет внутренний [[Prototype]] объекта.
Примечание: Важно отметить, что .__proto__ является устаревшей функцией, которую не следует использоваться в производственном коде, и ее нет в современных браузерах. Однако в мануале она используется для демонстрации.
x.__proto__;
Это вернет такой же результат, что и getPrototypeOf().
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Важно, чтобы каждый объект JavaScript имел [[Prototype]], поскольку он позволяет связать два и более объекта.
Созданные вами объекты имеют [[Prototype]] так же, как и встроенные объекты, такие как Date и Array. Сослаться на это внутреннее свойство можно с помощью свойства prototype.
Наследование прототипов
Когда вы пытаетесь получить доступ к свойству или методу объекта, JavaScript сначала выполняет поиск по самому объекту, и если искомое не найдено, он будет искать объект [[Prototype]]. Если после поиска по объекту и его [[Prototype]] совпадения не найдено, JavaScript проверит прототип связанного объекта и продолжит поиск до тех пор, пока не достигнет конца цепочки прототипов.
В конце цепочки прототипов находится Object.prototype. Все объекты наследуют свойства и методы Object. Любая попытка поиска за пределами цепочки приводит к null.
В нашем примере x – пустой объект, который наследуется от Object. x может использовать любое свойство или метод, которые имеет Object, например toString().
x.toString();
[object Object]
Эта цепочка прототипов состоит из всего одной ссылки (x -> Object). Это понятно потому, что если вы попытаетесь связать два свойства [[Prototype]], получится null.
x.__proto__.__proto__;
null
Давайте рассмотрим другой тип объекта. Если у вас есть опыт работы с массивами JavaScript, вы знаете, что у них много встроенных методов (таких как pop() и push()). У вас есть доступ к этим методам при создании нового массива потому, что любой массив, который вы создаете, имеет доступ к свойствам и методам Array.prototype.
Читайте также: Работа с массивами в JavaScript
Создайте новый массив:
let y = [];
Помните, что создать его можно также с помощью конструктора массива: let y = new Array().
Если посмотреть на [[Prototype]] нового массива y, вы увидите, что он имеет больше свойств и методов, чем объект x. Он унаследовал все это от Array.prototype.
y.__proto__;
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]
Вы увидите свойство constructor в прототипе, для которого задано значение Array(). Свойство constructor возвращает функцию-конструктор объекта, которая является механизмом для построения объектов из функций.
Теперь можно объединить два прототипа, так как в этом случае цепочка прототипов будет длиннее. Он выглядит так: y-> Array -> Object.
y.__proto__.__proto__;
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Эта цепочка теперь относится к Object.prototype. Можно проверить внутренний [[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 имеют скрытое внутреннее свойство [[Prototype]] (которое можно определить с помощью __proto__ в некоторых браузерах). Объекты могут быть расширены и наследуют свойства и методы от [[Prototype]] их конструктора.
Прототипы складываются в цепочки, и каждый дополнительный объект наследует все по этой цепочке. Цепочка заканчивается на 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}
Теперь, если запросить [[Prototype]] объекта hero1, вы увидите constructor Hero().
Object.getPrototypeOf(hero1);
constructor: ƒ Hero(name, level)
Как видите, пока что в конструкторе определены только свойства, а не методы. В JavaScript методы прототипов обычно определяются для повышения эффективности и удобочитаемости кода.
Мы можем добавить помощью prototype. Создайте метод greet().
// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
return `${this.name} says hello.`;
}
Поскольку greet() – это prototype в Hero, а hero1 является экземпляром Hero, метод будет доступен и для hero1:
hero1.greet();
"Bjorn says hello."
Если вы проверите [[Prototype]] в Hero, вы увидите доступную опцию greet().
Теперь нужно создать классы персонажей. Вкладывать все способности для каждого класса в конструктор Hero не имеет смысла, потому что разные классы будут иметь разные способности. Нужно создать новые функции-конструкторы и связать их с оригинальным Hero.
С помощью метода call() скопируйте свойства одного конструктора в другой. Создайте конструкторы Warrior и Healer.
...
// Initialize Warrior constructor
function Warrior(name, level, weapon) {
// Chain constructor with call
Hero.call(this, name, level);
// Add a new property
this.weapon = weapon;
}
// Initialize Healer constructor
function Healer(name, level, spell) {
Hero.call(this, name, level);
this.spell = spell;
}
Оба новых конструктора теперь обладают свойствами Hero и несколькими уникальными свойствами. Добавьте метод attack() в Warrior и метод heal() в Healer.
[label characterSelect.js
...
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`;
}
Теперь можно создать персонажей с двумя новыми доступными классами:
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');
Теперь hero1 распознается как Warrior с новыми свойствами.
Warrior {name: "Bjorn", level: 1, weapon: "axe"}
Можно использовать новые методы, установленные в прототипе Warrior.
hero1.attack();
"Bjorn attacks with the axe."
Но что произойдет, если попробовать использовать следующие методы в цепочке прототипов?
hero1.greet();
Uncaught TypeError: hero1.greet is not a function
Свойства и методы прототипа не связываются автоматически, когда вы используете call() для создания цепочек. Используйте Object.create(), чтобы связать прототипы, прежде чем создавать и добавлять какие-либо дополнительные методы к прототипу.
...
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);
// All other prototype methods added below
...
Теперь можно использовать методы прототипа из Hero в экземплярах Warrior или Healer.
Вот полный код страницы создания персонажа.
// Initialize constructor functions
function Hero(name, level) {
this.name = name;
this.level = level;
}
function Warrior(name, level, weapon) {
Hero.call(this, name, level);
this.weapon = weapon;
}
function Healer(name, level, spell) {
Hero.call(this, name, level);
this.spell = spell;
}
// Link prototypes and add prototype methods
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);
Hero.prototype.greet = function () {
return `${this.name} says hello.`;
}
Warrior.prototype.attack = function () {
return `${this.name} attacks with the ${this.weapon}.`;
}
Healer.prototype.heal = function () {
return `${this.name} casts ${this.spell}.`;
}
// Initialize individual character instances
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');
В этом файле вы создали класс Hero с базовыми свойствами, два класса персонажей – Warrior и Healer – из исходного конструктора, добавили методы в прототипы и создали отдельные экземпляры персонажей.
Заключение
JavaScript – это язык, основанный на прототипах, и он функционирует иначе, чем традиционная парадигма на основе классов, используемая многими другими объектно-ориентированными языками.
В этом мануале вы узнали, как работают прототипы JavaScript и как связать свойства и методы объекта с помощью скрытого свойства [[Prototype]], которым обладают все объекты. Также вы теперь умеете создавать пользовательские функции-конструкторы и использовать наследование прототипов для передачи значений свойств и методов.