Понимание прототипов и наследования в JavaScript
Введение
JavaScript — это язык, основанный на прототипах, а это означает, что свойства и методы объектов могут совместно использоваться через обобщенные объекты, которые можно клонировать и расширять. Это известно как наследование прототипов и отличается от наследования классов. Среди популярных объектно-ориентированных языков программирования JavaScript относительно уникален, поскольку другие известные языки, такие как PHP, Python и Java, являются языками, основанными на классах, которые вместо этого определяют классы как чертежи для объектов.
В этом руководстве мы узнаем, что такое прототипы объектов и как использовать функцию конструктора для расширения прототипов до новых объектов. Мы также узнаем о наследовании и цепочке прототипов.
JavaScript-прототипы
В разделе «Понимание объектов в JavaScript» мы рассмотрели тип данных объекта, как создать объект и как получить доступ к свойствам объекта и изменить их. Теперь мы узнаем, как можно использовать прототипы для расширения объектов.
Каждый объект в JavaScript имеет внутреннее свойство с именем [[Prototype]]
. Мы можем продемонстрировать это, создав новый пустой объект.
let x = {};
Это способ, которым мы обычно создаем объект, но обратите внимание, что это можно сделать и с помощью конструктора объекта: let x=new Object()
.
Двойные квадратные скобки, заключающие [[Prototype]]
, означают, что это внутреннее свойство, к которому нельзя получить доступ непосредственно в коде.
Чтобы найти [[Prototype]]
этого вновь созданного объекта, мы будем использовать метод getPrototypeOf()
.
Object.getPrototypeOf(x);
Вывод будет состоять из нескольких встроенных свойств и методов.
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Другой способ найти [[Prototype]]
— использовать свойство __proto__
. __proto__
— это свойство, раскрывающее внутренний [[Prototype]]
объекта.
Важно отметить, что .__proto__
— это устаревшая функция, которую не следует использовать в рабочем коде, и она присутствует не во всех современных браузерах. Тем не менее, мы можем использовать его в этой статье в демонстрационных целях.
x.__proto__;
Вывод будет таким же, как если бы вы использовали getPrototypeOf()
.
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}
Важно, чтобы у каждого объекта в JavaScript был [[Prototype]]
, так как он позволяет связать любые два или более объектов.
Создаваемые вами объекты имеют [[Prototype]]
, как и встроенные объекты, такие как Date
и Array
. Ссылку на это внутреннее свойство можно сделать из одного объекта в другой через свойство prototype
, как мы увидим позже в этом руководстве.
Наследование прототипа
Когда вы пытаетесь получить доступ к свойству или методу объекта, JavaScript сначала будет искать сам объект, и если он не будет найден, он будет искать [[Prototype]]
объекта. Если после проверки как объекта, так и его [[Prototype]]
соответствие по-прежнему не найдено, JavaScript проверит прототип связанного объекта и продолжит поиск, пока не будет достигнут конец цепочки прототипов.
В конце цепочки прототипов находится Object
. Любая попытка поиска за пределами конца цепочки приводит к null
.
В нашем примере x
— это пустой объект, который наследуется от Object
. x
может использовать любое свойство или метод, который есть у Object
, например toString()
.
x.toString();
Output[object Object]
Эта цепочка прототипов состоит всего из одного звена. x
-> Объект
. Мы это знаем, потому что если мы попытаемся соединить два свойства [[Prototype]]
вместе, это будет null
.
x.__proto__.__proto__;
Outputnull
Давайте посмотрим на другой тип объекта. Если у вас есть опыт работы с массивами в JavaScript, вы знаете, что у них есть много встроенных методов, таких как pop()
и push()
. Причина, по которой у вас есть доступ к этим методам при создании нового массива, заключается в том, что любой создаваемый вами массив имеет доступ к свойствам и методам в Array.prototype
.
Мы можем проверить это, создав новый массив.
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__;
Output{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
, мы увидим, что был создан новый объект с новыми свойствами, установленными, как и ожидалось.
OutputHero {name: "Bjorn", level: 1}
Теперь, если мы получим [[Prototype]]
hero1
, мы сможем увидеть конструктор
как Hero()
. (Помните, что это тот же ввод, что и hero1.__proto__
, но это правильный метод для использования.)
Object.getPrototypeOf(hero1);
Outputconstructor: ƒ Hero(name, level)
Вы можете заметить, что мы определили только свойства, а не методы в конструкторе. В JavaScript принято определять методы в прототипе для повышения эффективности и удобочитаемости кода.
Мы можем добавить метод к Hero
, используя prototype
. Мы создадим метод greet()
.
...
// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
return `${this.name} says hello.`;
}
Поскольку greet()
находится в прототипе
Hero
, а hero1
является экземпляром Hero
этот метод доступен для hero1
.
hero1.greet();
Output"Bjorn says hello."
Если вы проверите [[Prototype]]
Hero, вы увидите greet()
в качестве доступной опции.
Это хорошо, но теперь мы хотим создать классы персонажей для использования героями. Не имеет смысла помещать все способности для каждого класса в конструктор Hero
, потому что разные классы будут иметь разные способности. Мы хотим создать новые функции-конструкторы, но мы также хотим, чтобы они были связаны с исходным Hero
.
Мы можем использовать метод call()
для копирования свойств из одного конструктора в другой конструктор. Давайте создадим конструктор Воина и Целителя.
...
// 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
.
...
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
с новыми свойствами.
OutputWarrior {name: "Bjorn", level: 1, weapon: "axe"}
Мы можем использовать новые методы, которые мы установили в прототипе Warrior
.
hero1.attack();
Console"Bjorn attacks with the axe."
Но что произойдет, если мы попытаемся использовать методы дальше по цепочке прототипов?
hero1.greet();
OutputUncaught TypeError: hero1.greet is not a function
Свойства и методы прототипа не связываются автоматически, когда вы используете call()
для цепочки конструкторов. Мы будем использовать Object.setPropertyOf()
, чтобы связать свойства в конструкторе Hero
с конструкторами Warrior
и Healer
. , обязательно поместив его перед любыми дополнительными методами.
...
Object.setPrototypeOf(Warrior.prototype, Hero.prototype);
Object.setPrototypeOf(Healer.prototype, Hero.prototype);
// All other prototype methods added below
...
Теперь мы можем успешно использовать методы прототипа из Hero
в экземпляре Warrior
или Healer
.
hero1.greet();
Output"Bjorn says hello."
Вот полный код страницы создания нашего персонажа.
// 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
Object.setPrototypeOf(Warrior.prototype, Hero.prototype);
Object.setPrototypeOf(Healer.prototype, 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]]
, которое используется всеми объектами. Мы также узнали, как создавать собственные функции-конструкторы и как работает наследование прототипов для передачи значений свойств и методов.