Прототипическое наследование в JavaScript
Наследование в классическом ООП означает передачу свойств и методов от родителя к дочернему элементу, чтобы он мог повторно использовать методы и свойства, определенные в родительском элементе. JavaScript реализует наследование через объекты. Каждый объект в JavaScript имеет внутреннюю ссылку ([[Prototype]]
) на объект, называемый прототипом, который, в свою очередь, может иметь собственный прототип и так далее, пока прототип не укажет на ноль. null
не имеет прототипа и действует как конец цепочки прототипов.
Так как же получить доступ к этому прототипу?
-
AnyObject.getPrototypeOf(anyObject)
anyObject.__proto__
Первый является стандартом согласно стандарту ECMAScript, а второй нестандартен, но широко реализован большинством браузеров. Однако оба указывают на одно и то же: прототип ([[Prototype]]
).
Наследование свойств
Когда мы пытаемся получить доступ к свойству из объекта JavaScript, сначала он ищется в самом объекте. Если он там не найден, он ищется по прототипу объекта. Этот процесс продолжается вверх по цепочке прототипов, пока не достигнет нуля. Если свойство все еще не найдено, оно вернет неопределенное значение.
const country = {
name : "India",
id: "1",
__proto__: {
planet: "Earth"
}
}
console.log(country.name);
// returns "India" because the
// property is found on the object itself
console.log(country.id);
// returns "1" because the
// property is found on the object itself
console.log(country.planet);
// returns "Earth" because the property was not found on the object itself,
// so it is checked on its [[Prototype]], which is the object containing the desired property
Наследование методов
В JavaScript нет таких методов, как в других объектно-ориентированных языках программирования. Вместо этого все является собственностью. Следовательно, и методы, и свойства наследуются как свойства.
Когда выполняется унаследованная функция, значение this
указывает на наследующий объект.
const countryPrototype = {
singNationalAnthem() {
console.log("singing " + this.nationalAnthem)
}
}
country.singNationalAnthem() //singing ""
// This is because the this binding here is the object nation and has own
// property singNationalAnthem and nationalAnthem
const country = {
name: "India",
id: "1",
nationalAnthem: "Jana Gana Mana....",
__proto__: countryPrototype
}
country.singNationalAnthem() //singing Jana Gana Mana....
// Here the method singNationalAnthem is not found on the object country
// so it is looked up on its [[Prototype]] nation.
// it prints Jana Gana Mana.... becuase the this binding refers to the
// object country and its has an own property nationalAnthem
Функции конструктора
Сила прототипов в том, что мы можем повторно использовать набор свойств, если они должны присутствовать в каждом экземпляре. Например, если нам нужно создать массив стран и метод singNationalAnthem
должен присутствовать на всех объектах, у нас может возникнуть соблазн создать объекты, как показано ниже.
const germany = {
name: "Germany",
id: "2",
nationalAnthem: "Einigkeit und Recht und Freiheit....",
__proto__: nation
}
const netherlands = {
name: "Netherlands",
id: "3",
nationalAnthem: "Wilhelmus van Nassouwe"
__proto__: nation
}
Поэтому каждый раз, когда мы хотели создать страну, мы вручную создавали объект и устанавливали свойство [[Prototype]] (__proto__)
, что не является эффективным способом. Именно здесь на помощь приходят функции-конструкторы, позволяющие совместно использовать свойства всем экземплярам типа Country
.
function Country(name, id, nationalAnthem) {
this.name = name;
this.id = id;
this.nationalAnthem = nationalAnthem;
}
Country.prototype.singNationalAnthem = function () {
console.log("singing " + this.nationalAnthem)
}
// Now all the instances of Country created using the constructor function
// will inherit the method singNationalAnthem
const india = new Country("India", "1", "Jana Gana Mana....")
india.singNationalAnthem() // singing Jana Gana Mana....
const germany = new Country("Germany", "2", "Einigkeit und Recht und Freiheit....")
germany.singNationalAnthem() //singing Einigkeit und Recht und Freiheit....
Здесь важно то, что Country.prototype
не является [[Prototype]]
функции Country
, вместо этого его прототип на самом деле является Function.prototype
, но все экземпляры/объекты, созданные из функции-конструктора Country
(Индия и Германия, как в пример) для параметра [[Prototype]]
будет установлено значение Country.prototype
;
теперь цепочка прототипов будет выглядеть примерно так
india -> Country.prototype -> Object prototype -> null
Country.prototype -> Function prototype -> Object prototype -> null
Мы можем добиться того же результата, используя классы. В JavaScript классы на самом деле являются синтаксическим сахаром над функциями-конструкторами.
class Country {
constructor(name, id, nationalAnthem) {
this.name = name;
this.id = id;
this.nationalAnthem = nationalAnthem;
}
singNationalAnthem() {
console.log("singing " + this.nationalAnthem)
}
}
const india = new Country("India", "1", "Jana Gana Mana....");
const germany = new Country("Germany", "2", "Einigkeit und Recht und Freiheit....")
india.singNationalAnthem() // singing Jana Gana Mana....
germany.singNationalAnthem() // singing Einigkeit und Recht und Freiheit....
Использование Object.setPrototypeOf
для установки прототипа
В приведенных выше примерах установлены прототипы для экземпляров, созданных из функции-конструктора, но при классическом наследовании ООП у вас есть базовый класс, который расширяют ваши дочерние классы. Этого поведения также можно добиться с помощью функций конструктора.
function Nation() {
this.name = ""
}
Nation.prototype.singNationalAnthem = function() {
console.log("singing " + this.nationalAnthem);
}
function Country(name, id, nationalAnthem) {
this.name = name;
this.id = id;
this.nationalAnthem = nationalAnthem;
}
Object.setPrototypeOf(Country.prototype, Nation.prototype);
const indiaObj = new Country("India", "1", "Jana Gana Mana...");
indiaObj.singNationalAnthem(); //singing Jana Gana Mana...
Теперь цепочка прототипов выглядит примерно так
indiaObj -> Country.prototype -> Nation.prototype -> Object prototype -> null
То же самое можно написать с использованием классов, как показано ниже.
class Nation {
constructor(name, id, nationalAnthem) {
this.name = name;
this.id = id;
this.nationalAnthem = nationalAnthem;
}
singNationalAnthem() {
console.log("singing " + this.nationalAnthem);
}
}
class Country extends Nation {
constructor(name, id, nationalAnthem, states) {
super(name, id, nationalAnthem);
this.states = states;
}
}
const india = new Country("India", "1", "Jana Gana Mana...", 28);
india.singNationalAnthem()
Ссылки на прототип по умолчанию
В JavaScript для каждого созданного объекта будет неявно установлен [[Prototype]]
по умолчанию.
- для объектов это прототип объекта
- для функций это прототип функции
- для массивов это прототип массивов….
Методы массива, такие как карта, фильтр, forEach…
на самом деле являются свойствами/методами, определенными в прототипе массива, и поэтому они доступны в каждом экземпляре массива.
Заключение
Модель наследования в JavaScript отличается от других языков, таких как Java/C++, и поначалу может показаться немного запутанной. Все является либо объектом (экземпляром), либо функцией (конструктором), и даже сами функции являются экземплярами конструктора Function. Классы — это просто синтаксический сахар в этой модели.
Конструктор функции имеет специальное свойство, называемое прототипом, которое при использовании с ключевым словом new помогает установить прототип создаваемых объектов, а из-за динамической природы языка даже этот прототип можно изменить во время выполнения.