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

Автор: admin

Дата: 13.09.2020 02:05

JavaScript - это язык, основанный на прототипах, что означает, что свойства и методы объекта могут совместно использоваться посредством обобщенных объектов, которые можно клонировать и расширять. Это называется прототипным наследованием и отличается от наследования классов. Что такое прототипы объектов и как использовать функцию конструктора для расширения прототипов в новые объекты?

Прототипы JavaScript

Каждый объект в JavaScript имеет внутреннее свойство, называемое [[Prototype]], где [[]] - означает, что это внутреннее свойство, и к нему нельзя получить доступ непосредственно в коде. Есть два способа создать объект в JS : первый - obj = {}, второй использовать конструктор объекта: let obj= new Object(). Чтобы найти [[Prototype]] этого вновь созданного объекта, мы будем использовать метод getPrototypeOf().

let obj = new Object(); 
console.log(Object.getPrototypeOf(obj));// выведет {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

Другой способ найти [[Prototype]] - использовать свойство __proto__, устаревший и не присутствует во всех современных браузерах.

let obj = new Object(); 
console.log(obj .__proto__));// выведет {конструктор: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ,…}

Каждый объект в JavaScript имеет [[Prototype]], поскольку он создает способ для связывания любых двух или более объектов.

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

Когда вы пытаетесь получить доступ к свойству или методу объекта, JavaScript сначала выполняет поиск по самому объекту, а если он не найден, он будет искать [[Prototype]] объекта. Если в самом объекте и его [[Prototype]] совпадение не найдено, JavaScript проверит прототип связанного объекта и продолжит поиск, пока не будет достигнут конец цепочки прототипов. В конце цепочки прототипов находится Object.prototype. Все объекты наследуют свойства и методы Object. Любая попытка поиска за пределами конца цепочки приводит к нулю. Объект let obj= new Object() - пустой объект, наследуемый от Object. obj может использовать любое свойство или метод, имеющийся у Object, например toString(). obj.toString(); выведет [object Object]- цепочка прототипа состоит из одного звена. obj->Object. Если мы попытаемся связать два свойства [[Prototype]] вместе, оно будет нулевым - obj.__proto__.__ proto__; выведет null.

Если рассмотреть массивы в JavaScript, у них есть много встроенных методов, таких как pop() и push(). Причина, по которой у вас есть доступ к этим методам при создании нового массива, заключается в том, что любой созданный вами массив имеет доступ к свойствам и методам в Array.prototype. Мы можем проверить это, создав новый массив arr = [] другой способ arr = new Array(). [[Prototype]] нового массива arr, имеет больше свойств и методов, чем объект obj, т.к. унаследовал все от Array.prototype.

let arr = new Array(); 
console.log(arr .__proto__);// выведет [constructor: ƒ, concat: ƒ, pop: ƒ, push: ƒ, …]

Теперь можно связать два прототипа вместе, поскольку наша цепочка прототипов в этом случае длиннее. Похоже, arr->Array->Object.

let arr = new Array(); 
console.log(arr .__proto__.__ proto__);// выведет {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

которая относится Object.prototype. Если рассмотреть [[Prototype]] против свойства prototype функции-конструктора, чтобы увидеть, что они ссылаются на одно и то же.

arr.__proto__ === Array.prototype;// true
arr.__proto__.__ proto__ === Object.prototype; // true

Мы также можем использовать для этого метод isPrototypeOf().

Array.prototype.isPrototypeOf(arr); // true
Object.prototype.isPrototypeOf(arr); // true

Можно использовать оператор instanceof, чтобы проверить, появляется ли свойство прототипа конструктора где-нибудь в цепочке прототипов объекта arr instanceof Array; // true. Все объекты JavaScript имеют скрытое внутреннее свойство [[Prototype]]. Объекты могут быть расширены и будут наследовать свойства и методы [[Prototype]] своего конструктора. Эти прототипы можно объединить в цепочку, и каждый дополнительный объект будет наследовать все по всей цепочке. Цепочка заканчивается Object.prototype.

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

Функции-конструкторы - это функции, которые используются для создания новых объектов. Оператор new используется для создания новых экземпляров. Кроме встроенных конструкторы, для создания таких объектов как new Array() и new Date() мы также можем создавать собственные, из которых можно создавать новые объекты.

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

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

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

//Инициализируем функцию-конструктор для нового героя
function Hero(name, level) {
  this.name = name;
  this.level = level;
}

Мы создали функцию-конструктор под названием Hero с двумя параметрами: именем и уровнем. Поскольку у каждого персонажа будет имя и уровень, для каждого нового персонажа имеет смысл иметь эти свойства. Ключевое слово this будет относиться к новому созданному экземпляру, поэтому установка this.name для параметра name гарантирует, что для нового объекта будет установлено свойство name.Теперь мы можем создать новый экземпляр с помощью new.

let hero1 = new Hero('Герой 1', 1);//{name: "Герой 1", level: 1}
console.log(Object.getPrototypeOf(hero1));//Hero {name: "Bjorn", level: 1}

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

// Добавляем метод приветствия в прототип Hero
Hero.prototype.greet = function() {
  return `${this.name} говорит привет.`;
}
let hero1 = new Hero('Герой 1', 1);//{name: "Герой 1", level: 1}
hero1.greet(); //Герой 1 говорит привет.

Рассмотрим пример когда необходимо создать новые функции-конструкторы, чтобы они были связаны с исходным Hero. Мы можем использовать метод call() для копирования свойств из одного конструктора в другой конструктор. Давайте создадим конструктор Warrior и Healer.

//Инициализируем конструктор Warrior
function Warrior(name, level, weapon) {
  // Цепной конструктор с вызовом
  Hero.call(this, name, level);

  // Добавляем новое свойство
  this.weapon = weapon;
}

// Инициализируем конструктор Healer
function Healer(name, level, spell) {
  Hero.call(this, name, level);

  this.spell = spell;
}

Оба новых конструктора теперь имеют свойства Героя и несколько уникальных. Мы добавим метод attack() к Warrior и метод heal() к Healer.

Warrior.prototype.attack = function() {
  return `${this.name} атаки с помощью ${this.weapon} .`;
}

Healer.prototype.heal = function() {
  return `${this.name} приводит к ${this.spell} .`;
}

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

const hero1 = new Warrior('Warrior', 1, 'axe');
const hero2 = new Healer('Healer', 1, 'cure');

//Warrior {name: "Warrior", level: 1, attack: "топор"}

Мы можем использовать новые методы, которые мы установили для прототипа Warrior. hero1.attack();- Warrior 1 атакует топором, но что произойдет, если мы попытаемся использовать методы дальше по цепочке прототипов? `hero1.greet();' - hero1.greet не является функцией

Свойства и методы прототипа не связываются автоматически, когда вы используете call() для цепочки конструкторов. Мы будем использовать Object.create() для связывания прототипов, убедившись, что поместили его до того, как какие-либо дополнительные методы будут созданы и добавлены к прототипу.

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

//Свяжите прототипы и добавьте методы прототипа
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} атаки с ${this.weapon}.`;
}

Healer.prototype.heal = function() {
  return `${this.name} бросает ${this.spell}.`;
}

// Инициализировать отдельные экземпляры символов
const hero1 = new Warrior('Bjorn', 1, 'axe');
const hero2 = new Healer('Kanin', 1, 'cure');

С помощью этого кода мы создали наш класс Hero с базовыми свойствами, создали два класса персонажей с именами Warrior и Healer из исходного конструктора, добавили методы к прототипам и создали отдельные экземпляры персонажей.

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