Всем привет! В этой небольшой статье мы с вами поговорим о прототипном наследовании в JavaScript и о некоторых нюансах, связанных с ним.
Введение
Вы никогда не задавались вопросом, откуда строки, массивы и объекты «знают» свои методы? Откуда, например, строка знает, что она может использовать метод .toUpperCase()? Или откуда массив знает, что он может воспользоваться методом .sort()? Мы ведь никогда не определяли эти методы вручную, не так ли?
А ответ следующий: все эти методы встроены в каждый тип структуры данных через так называемое прототипное наследование.
В JavaScript объект способен наследовать свойства другого объекта. Объект, от которого наследуются свойства, называется прототипом. Проще говоря, объекты могут наследовать свойства других объектов - прототипов.
Возможно, у вас возник вопрос: а зачем вообще нужно наследование? Что ж, наследование решает проблему дублирования данных и логики. За счет наследования объекты могут использовать свойства и методы совместно, что избавляет нас от необходимости вручную устанавливать все эти свойства и методы для каждого объекта отдельно.
Как получить доступ к свойствам и методам прототипа в JavaScript
Когда мы пытаемся получить доступ к свойству объекта, поиск этого свойства производится не только в этом объекте, но и в прототипе объекта, в прототипе прототипа и т.д. до тех, пока не будет найдено свойство, которое будет соответствовать имени, или не будет достигнут конец цепочки прототипов.
JavaScript вернет undefined только в том случае, если свойство или метод не будет найден во всей цепочке прототипов.
Все объекты в JavaScript имеют внутреннее свойство под названием [[Prototype]].
Если мы с вами создадим массив и выведем его в консоль вот таким образом:
const arr = [1,2,3]
console.log(arr)
То увидим следующее:
Двойные квадратные скобки, в которых заключено свойство [[Prototype]], говорит о том, что это внутреннее свойство, и к нему нельзя получить прямой доступ в коде.
Для того, чтобы найти свойство [[Prototype]] объекта, нам понадобиться метод Object.getPrototypeOf().
const arr = [1,2,3]
console.log(Object.getPrototypeOf(arr))
Вывод будет содержать несколько встроенных методов и свойств:
Также обратите внимание на то, что прототипы можно изменять различными способами.
Цепочка прототипов
В конце цепочки прототипов располагается Object.prototype. Все объекты наследуют свойства и методы Object. Любая попытка найти что-либо за пределами конца цепочки, приведет к выводу null.
Если вы попробуете найти прототип прототипа массива, функции или строки, то увидите, что это объект. А все потому, что в JavaScript все объекты являются потомками или экземплярами Object.prototype, который является объектом, устанавливающим свойствам и методы для всех типов данных JavaScript.
const arr = [1,2,3]
const arrProto = Object.getPrototypeOf(arr)
console.log(Object.getPrototypeOf(arrProto))
Все типы прототипов (например, прототип массива) определяет свои собственные методы и свойства, а в некоторых случаях даже переопределяет методы и свойства Object.prototype (вот почему у массивов есть методы, которых, например, нет у объектов).
Все, что касается внутреннего устройства и самой цепочки прототипов, все в JavaScript построено на Object.prototype.
Если мы попытаемся обратиться к Object.prototype, то получим null.
const arr = [1,2,3]
const arrProto = Object.getPrototypeOf(arr)
const objectProto = Object.getPrototypeOf(arrProto)
console.log(Object.getPrototypeOf(objectProto))
Прототипно-ориентированный язык программирования
JavaScript – это прототипно-ориентированный язык. Это значит, что свойства и методы объектов могут передаваться через обобщенные объекты, которые, в свою очередь, можно клонировать и расширять.
Если мы говорим о наследовании, JavaScript имеет лишь одну структуру: объекты.
Все объекты имеют закрытое свойство (которое называется [[Prototype]]), которое содержит ссылку на другой объект – его прототип. У этого объекта-прототипа есть свой собственный прототип, и так далее, пока цепочка не дойдет до объекта, равного null.
По определению null не имеет прототипа и является последним звеном в этой цепочке прототипов.
Это так называемое прототипное наследование, и оно отличается от наследования классов. Среди популярных языков программирования JavaScript является относительно уникальным, так как другие довольно известные языки, например, PHP, Python и Java, основаны на классах, и определяют классы как шаблоны объектов.
Здесь вы, вероятно, скажете: «Но мы же МОЖЕМ реализовать классы в JavaScript!». Да, можем, но это уже синтаксический сахар.
Классы JavaScript
Классы – это способ настроить шаблон для создания объектов с заранее определенными свойствами и методами. Создав класс с определенными свойствами и методами, в дальнейшем вы можете создать экземпляры объектов этого класса, которые унаследуют все свойства и методы этого класса.
В JavaScript мы можем создать класс следующим образом:
class Alien {
constructor (name, phrase) {
this.name = name
this.phrase = phrase
this.species = "alien"
}
fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
sayPhrase = () => console.log(this.phrase)
}
А затем мы можем создать экземпляр объекта этого класса:
const alien1 = new Alien("Ali", "I'm Ali the alien!")
console.log(alien1.name) // output: "Ali"
Классы – это способ создать более модульный, организованный и понятный код. Их довольно часто используют в ООП.
Однако учитывайте, что JavaScript на самом деле не поддерживает классы, как другие языки. Ключевое слово было введено в ES6 в качестве синтаксического сахара, упрощающего организацию кода.
Чтобы лучше представить это, посмотрите сюда. То же самое, что мы делали, когда определяли класс, мы можем сделать, определив функцию и отредактировав прототип:
function Alien(name, phrase) {
this.name = name
this.phrase = phrase
this.species = "alien"
}
Alien.prototype.fly = () => console.log("Zzzzzziiiiiinnnnnggggg!!")
Alien.prototype.sayPhrase = () => console.log(this.phrase)
const alien1 = new Alien("Ali", "I'm Ali the alien!")
console.log(alien1.name) // output "Ali"
console.log(alien1.phrase) // output "I'm Ali the alien!"
alien1.fly() // output "Zzzzzziiiiiinnnnnggggg"
Любую функцию можно вызвать в качестве конструктора с помощью ключевого слова new, а свойство прототипа этой функции можно использовать для объекта, от которого наследуются методы. В JavaScript понятие «класс» используется только на концептуальном уровне для описания метода, представленного выше. Технически это просто функция.
По сути это не имеет большого значения, ведь мы все равно можем спокойно реализовывать ООП и использовать классы, как и в большинстве других языков программирования. Но важно помнить, что в основе JavaScript лежит наследование прототипов.
Заключение
Вот и все! Как всегда, я надеюсь, что вам понравилась эта статья, и вы смогли узнать что-то новое.