Поиск по сайту:

Понимание прототипов и наследования в 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__;
Output
null

Давайте посмотрим на другой тип объекта. Если у вас есть опыт работы с массивами в 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, мы увидим, что был создан новый объект с новыми свойствами, установленными, как и ожидалось.

Output
Hero {name: "Bjorn", level: 1}

Теперь, если мы получим [[Prototype]] hero1, мы сможем увидеть конструктор как Hero(). (Помните, что это тот же ввод, что и hero1.__proto__, но это правильный метод для использования.)

Object.getPrototypeOf(hero1);
Output
constructor: ƒ 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 с новыми свойствами.

Output
Warrior {name: "Bjorn", level: 1, weapon: "axe"}

Мы можем использовать новые методы, которые мы установили в прототипе Warrior.

hero1.attack();
Console
"Bjorn attacks with the axe."

Но что произойдет, если мы попытаемся использовать методы дальше по цепочке прототипов?

hero1.greet();
Output
Uncaught 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]], которое используется всеми объектами. Мы также узнали, как создавать собственные функции-конструкторы и как работает наследование прототипов для передачи значений свойств и методов.