Прототипы и наследования в 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 - это язык, основанный на прототипах, и он работает иначе, чем традиционная парадигма на основе классов, которую используют многие другие объектно-ориентированные языки.