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

Вступление

JavaScript - это * язык на основе прототипов *, означающий, что свойства и методы объекта могут совместно использоваться обобщенными объектами, которые могут быть клонированы и расширены. Это известно как прототип наследования и отличается от наследования классов. Среди популярных объектно-ориентированных языков программирования JavaScript относительно уникален, так как другие известные языки, такие как PHP, Python и Java, являются языками на основе классов, которые вместо этого определяют классы в качестве чертежей для объектов.

В этом уроке мы узнаем, что такое прототипы объектов и как использовать функцию конструктора для расширения прототипов в новые объекты. Мы также узнаем о наследовании и цепочке прототипов.

Прототипы JavaScript

В Understanding Objects in JavaScript мы рассмотрели тип данных объекта, как создать объект и как получить доступ и изменить объект свойства. Теперь мы узнаем, как прототипы можно использовать для расширения объектов.

Каждый объект в JavaScript имеет внутреннее свойство, называемое + +. Мы можем продемонстрировать это, создав новый пустой объект.

let x = {};

Таким способом мы обычно создаем объект, но учтите, что другой способ сделать это - с помощью конструктора объекта: + let x = new Object () +.

Чтобы найти + + этого вновь созданного объекта, мы будем использовать метод + getPrototypeOf () +.

Object.getPrototypeOf(x);

Вывод будет состоять из нескольких встроенных свойств и методов.

Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Другой способ найти + + - через свойство + proto +. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto [+ proto +] - это свойство, которое предоставляет внутреннее + + объекта.

x.__proto__;

Вывод будет таким же, как если бы вы использовали + getPrototypeOf () +.

Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Важно, чтобы каждый объект в JavaScript имел + +, поскольку он создает способ для любых двух или более объектов, которые будут связаны.

У созданных вами объектов есть + +, как и у встроенных объектов, таких как + Date _ и` + Array I`. Ссылка на это внутреннее свойство может быть сделана из одного объекта в другой через свойство + prototype +, как мы увидим позже в этом руководстве.

Наследование прототипа

Когда вы пытаетесь получить доступ к свойству или методу объекта, JavaScript сначала выполняет поиск по самому объекту, а если он не найден, он ищет объект + +. Если после консультации с объектом и его + + совпадение все еще не найдено, JavaScript проверит прототип связанного объекта и продолжит поиск, пока не будет достигнут конец цепочки прототипов.

В конце цепочки прототипов находится https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/prototype [+ Object.prototype +]. Все объекты наследуют свойства и методы https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object [+ Object +]. Любая попытка поиска за концом цепочки приводит к + null +.

В нашем примере + x + - это пустой объект, который наследуется от + Object +. + x + может использовать любое свойство или метод, которые имеет + Object +, например + toString () +.

x.toString();
Output[object Object]

Эта цепь прототипа имеет длину только одно звено. + x ++ Object +. Мы знаем это, потому что если мы попытаемся связать два свойства + + вместе, это будет + null +.

x.__proto__.__proto__;
Outputnull

Давайте посмотрим на другой тип объекта. Если у вас есть опыт Working с массивами в JavaScript, вы знаете, что у них есть много встроенных методов, таких как + pop () + `и + push () + . Причина, по которой у вас есть доступ к этим методам при создании нового массива, заключается в том, что любой создаваемый вами массив имеет доступ к свойствам и методам `+ Array.prototype +.

Мы можем проверить это, создав новый массив.

let y = [];

Имейте в виду, что мы могли бы также написать его как конструктор массива + let y = new Array () +.

Если мы посмотрим на + + нового массива + y +, мы увидим, что он имеет больше свойств и методов, чем объект + x +. Он унаследовал все от + Array.prototype +.

y.__proto__;
[constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]

Вы заметите свойство + constructor + в прототипе, которое установлено в + Array () +. Свойство + constructor + возвращает функцию-конструктор объекта, который представляет собой механизм, используемый для конструирования объектов из функций.

Теперь мы можем связать два прототипа вместе, так как в этом случае наша цепочка прототипов длиннее. Это выглядит как + y ++ Array →` + Object N`.

y.__proto__.__proto__;
Output{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Эта цепочка теперь ссылается на + Object.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 имеют скрытое внутреннее свойство + + (которое может быть открыто через + proto + в некоторых браузерах). Объекты могут быть расширены и будут наследовать свойства и методы в + + своего конструктора.

Эти прототипы могут быть объединены в цепочку, и каждый дополнительный объект будет наследовать все в цепочке. Цепочка заканчивается + Object.prototype +.

Функции конструктора

  • Функции конструктора * - это функции, которые используются для создания новых объектов. Https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new [+ new + operator] используется для создания новых экземпляров на основе функции конструктора. Мы видели некоторые встроенные конструкторы JavaScript, такие как + new Array () + и + new Date () +, но мы также можем создавать собственные шаблоны для создания новых объектов.

В качестве примера, допустим, мы создаем очень простую текстовую ролевую игру. Пользователь может выбрать персонажа, а затем выбрать класс персонажа, например, воина, целителя, вора и так далее.

Поскольку у каждого персонажа будет много общих характеристик, таких как имя, уровень и точки попадания, имеет смысл создать конструктор в качестве шаблона. Однако, поскольку у каждого класса персонажа могут быть совершенно разные способности, мы хотим убедиться, что у каждого персонажа есть доступ только к его собственным способностям. Давайте посмотрим, как мы можем достичь этого с помощью наследования прототипов и конструкторов.

Начнем с того, что функция конструктора - это обычная функция. Он становится конструктором, когда он вызывается экземпляром с ключевым словом + new +. В JavaScript мы прописываем заглавную первую букву функции конструктора по соглашению.

characterSelect.js

// 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}

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

Object.getPrototypeOf(hero1);
Outputconstructor: ƒ Hero(name, level)

Вы можете заметить, что в конструкторе у нас есть только определенные свойства, а не методы. В JavaScript принято определять методы в прототипе для повышения эффективности и читабельности кода.

Мы можем добавить метод к + Hero + используя + prototype +. Мы создадим метод + greet () +.

characterSelect.js

...
// Add greet method to the Hero prototype
Hero.prototype.greet = function () {
 return `${this.name} says hello.`;
}

Поскольку + greet () + находится в + prototype + + Hero +, а + hero1 + является экземпляром + Hero +, метод доступен для + hero1 +.

hero1.greet();
Output"Bjorn says hello."

Если вы осмотрите + + Hero, вы увидите + greet () + в качестве доступной опции.

Это хорошо, но теперь мы хотим создать классы персонажей для героев. Не имеет смысла помещать все способности для каждого класса в конструктор + Hero + ', потому что разные классы будут иметь разные способности. Мы хотим создать новые функции конструктора, но мы также хотим, чтобы они были подключены к исходному `+ Hero +.

Мы можем использовать метод https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call [+ call () +] для копирования свойств из одного конструктора в другой конструктор. Давайте создадим Воина и Целителя.

characterSelect.js

...
// 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 +.

characterSelect.js

...
Warrior.prototype.attack = function () {
 return `${this.name} attacks with the ${this.weapon}.`;
}

Healer.prototype.heal = function () {
 return `${this.name} casts ${this.spell}.`;
}

На этом этапе мы создадим наших персонажей с помощью двух новых доступных классов персонажей.

characterSelect.js

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.create () +, чтобы связать прототипы, убедившись, что поставили его перед созданием и добавлением каких-либо дополнительных методов в прототип.

characterSelect.js

...
Warrior.prototype = Object.create(Hero.prototype);
Healer.prototype = Object.create(Hero.prototype);

// All other prototype methods added below
...

Теперь мы можем успешно использовать методы-прототипы из + Hero + на экземпляре + Warrior And + Healer`.

hero1.greet();
Output"Bjorn says hello."

Вот полный код нашей страницы создания персонажа.

characterSelect.js

// 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, и как связать свойства и методы объекта через скрытое свойство + +, которое разделяют все объекты. Мы также узнали, как создавать пользовательские функции конструктора и как работает наследование прототипов для передачи значений свойств и методов.