![](https://habrastorage.org/getpro/habr/upload_files/b7e/405/e52/b7e405e528e719b477568a592c821328.png)
Часто на собеседовании опытный разработчик может спросить у начинающего: «Что такое __proto__ и prototype, и чем они отличаются?». Обычно этот вопрос либо ставит в тупик, либо на него отвечают заученной мантрой из видео «50 вопросов на собеседовании»: « __proto__ — это ссылка на prototype, а prototype — это собственно свойство». И этот ответ правильный, только большинство недавно пришедших в профессию разработчиков не понимают, что это значит на самом деле. Причина проста — они не встречают в разработке ни __proto__, ни prototype, потому что современные стандарты JS прячут от него работу с этими свойствами за синтаксический сахар. Эта статья для таких, как я — разработчиков, которые столкнулись с JS в то время, когда никаких __proto__ и prototype на поверхности уже нет, а желание понять, как это устроено "под капотом" остается.
Синтаксический сахар
Историю нужно начать с причины, по которой сейчас разработчик почти не сталкивается с __proto__ и вовсе не сталкивается с prototype. И это вовсе не ключевое слово class и имитация классов, появившиеся в ECMAScript 2015. Первопричина — это синтаксический сахар, механизм, с помощью которого происходит развитие JS. Я решил уделить немного внимания этому понятию, потому что часто о нем говорят, как о само собой разумеющемся знании, но для начинающих разработчиков — это не всегда так. Синтаксический сахар — это механизм, с помощью которого происходит упрощение кода «визуально», но не происходит его изменение «под капотом». Простой пример, в JS есть цикл for — это не синтаксический сахар, это цикл с уникальным поведением, которого нельзя добиться другими инструментами JS. А вот цикл for…of или for…in — это синтаксический сахар, т.е. цикл, который «под капотом» разворачиваются в обычный for. Сделаны эти циклы для визуального упрощения. Итак, главное, что нужно понять — синтаксический сахар не меняет процедур, которые происходят «под капотом», он меняет только визуальное восприятие кода.
//один и тот же код с использованием обычного for и for … of
for (let i = 0; i < arr.length; i += 1) {
console.log(arr[ i ]);
}
for (const element of arr) {
console.log(element);
}
Псевдо-классы и функции-конструкторы в JS
А вот теперь поговорим про классы в JS. Говоря класс, мы должны понимать, что это не тот же самый класс, что в других языках. Классы в JS — это синтаксический сахар поверх прототипного наследования. Сейчас разберемся, о чем идет речь подробнее. Если довольно легко понять суть синтаксического сахара на примере с циклами for и for…of, то вот с классами все не так просто. Разработчик, который начал осваивать JS недавно, просто не сталкивается с той конструкцией, на основе которой сделан класс, потому что классы полностью ее вытеснили.
Прототипное наследование
Сделаем небольшое погружение в прототипное наследование, это поможет нам понять причины, по которым появился этот механизм. Представьте, что вы в 1995 году, у вас компьютер с 8 мегабайтами оперативной памяти и процессором на 66 МГц, и вам нужно написать очень экономный к памяти язык программирования. Вот тогда-то и было внедрено прототипное наследование, которое позволяло даже с вашими 8Мб оперативной памяти спокойно разрабатывать на JS. Суть примерно такова — давайте создадим корневой объект — прародителя, и дадим ему какое-то поведение, после чего, сделаем дочерний объект, у которого будет свое поведение и поведение родителя. А на основе этого дочернего объекта можно будет создать еще одного потомка, который наследует поведение обоих предков. Но вся суть этого в том, что наследуемое поведение для дочернего объекта не создается заново, а просто ссылается на место в памяти, где хранится предок. Сейчас разберемся подробно. Прародитель всех объектов в JS — это функция конструктор Object. У этого объекта есть поведение - мы для примера возьмем метод valueOf(). Потомки этого первого объекта все остальные функции-конструкторы, заранее созданные в JS — например String, Number, Array и т.д. У функции-конструктора Array есть свое поведение, например, методы массивов, а также поведение родителя, например метод valueOf(). Когда вы создаете конкретный массив в коде, вы делаете экземпляр, созданный функцией-конструктором Array. В итоге, при вызове метода valueOf() у этого экземпляра, с точки зрения памяти это выглядит так:
![](https://habrastorage.org/getpro/habr/upload_files/f0e/d95/479/f0ed9547923f5c54f44105b3228a7e99.jpg)
Родитель по отношению к потомку называется прототип, именно отсюда название «прототипное наследование». Таким образом происходит очень экономный расход памяти, и именно из этого механизма появляются понятия «ссылочный тип данных», «this», «цепочка прототипов» и т.д. Ну а называется эта ссылка, которая обращается к предку __proto__. То есть «под капотом» общение происходит через эту ссылку, и вызов метода valueOf() выглядит вот так:
myArr.__proto__.__proto__.valueOf.apply(myArr);
Конечно стоит отметить — что в современном языке так никто не пишет, этот фрагмент кода тут только в качестве наглядного примера.
Небольшой фан-факт: именно из этого механизма появилось знаменитое выражение «В JS все является объектами».
Функции-конструкторы
Следующая задача, которая стоит перед создателями языка в 95-м — дать разработчикам возможность обобщать сущности, наделять их поведением, но при этом точно также быть экономными в отношении памяти. По факту этот механизм уже есть - на его основе происходит создание функции-конструктора Array, которая наследуется от Object. Вы как разработчик хотите создать сущность, на основе которой хотите штамповать экземпляры с одинаковым интерфейсом и поведением, и наверное вы сейчас думаете: «ну это же описание класса в JS», но не забывайте, что мы с вами в 1995 году, а классы появились только в ECMAScript 2015. Так что разработчики получили в свои руки функции-конструкторы. Функция-конструктор — это обычная функция, которая определяет интерфейс вашей новой сущности:
function User(login, email) {
this.login = login;
this.email = email;
}
Вы скажете: “а где же поведение? Где методы этой новой сущности?”. Первое интуитивное решение - это вписать поведение прямо внутрь функции-конструктора:
function User(login, email) {
this.login = login;
this.email = email;
this.changeEmail = function (newEmail) {
this.email = newEmail
}
}
Но такой подход в корне неверный, потому что при таком описании сущности вы будете получать в каждом экземпляре не ссылку на метод changeEmail(), который хранится в памяти функции-конструктора, а будете копировать эту функцию в каждый новый экземпляр, что неэффективно с точки зрения памяти.
И тут спустя 5 тысяч знаков у нас наконец появляется слово prototype. Для того, чтобы хранить методы только в функции-конструкторе, а в экземпляре обращаться к ним по ссылке, есть специальное свойство функции-конструктора, которое называется prototype — это по факту специализированное хранилище для поведения всех экземпляров, созданных функцией-конструктором. В результате наш код преобразуется в следующий:
function User(login, email) {
this.login = login;
this.email = email;
}
User.prototype.changeEmail = function (newEmail) {
this.email = newEmail
}
Теперь, надеюсь, вам понятно, что prototype — это свойство функции конструктора, которое хранит в себе интерфейс предка, к которому через ссылку __proto__ будет обращаться потомок.
Код разработчика до появления классов в ECMAScript 2015 выглядел так:
function User(login, email) {
this.login = login;
this.email = email;
}
User.prototype.changeEmail = function (newEmail) {
this.email = newEmail;
};
function Admin(login, email, team) {
User.call(this, login, email);
this.team = team;
}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.constructor = Admin;
Admin.prototype.changeTeam = function (newTeam) {
this.team = newTeam;
};
В этом куске кода была создана функция-конструктор User, интерфейс которого включает login, email и метод изменения почты changeEmail, а потом сделана функция-конструктор Admin, которая наследует функцию-конструктор User, но расширяет и интерфейс и поведение. Если вы это впервые видите — это выглядит как магия, и страшно непонятно. А все потому, что ровно этот код, но с использованием классов выглядит так:
class User {
constructor(login, email) {
this.login = login;
this.email = email;
}
changeEmail(newEmail) {
this.email = newEmail;
}
}
class Admin extends User {
constructor(login, email, team) {
super(login, email);
this.team = team;
}
changeTeam(newTeam) {
this.team = newTeam;
}
}
Согласитесь, теперь это визуально читается понятнее, но от появления ключевого слова class — в JS не появляются классы, все происходит, как указано в первом фрагменте кода — просто разработчик больше этого не видит. Этот синтаксический сахар, по факту, навеки прячет от нас свойство prototype, поэтому разработчики, не писавшие до выхода ES2015 испытывают такие трудности с пониманием, что же это за штуки такие __proto__ и prototype.
В данной статье есть ряд упрощений, например, я не затронул синтаксический сахар вокруг самой функции-конструктора, а также я целиком выпустил свойство constructor у prototype, это мне кажется излишним усложнением для концептуального объяснения сути __proto__ и prototype. Надеюсь, что эта статья сподвигнет кого-то из вас разобраться с этими вопросами глубже. Мне понравилась статья-размышление о наследовании в JS, и я вам рекомендую начать с нее.
Итоги
Давайте подведем краткие итоги, которые, надеюсь, теперь будут звучать для вас осмысленно.
__proto__ — это свойство любого объекта в JS, которое является ссылкой на свойство prototype функции-конструктора:
prototype — это свойство функции-конструктора, которое хранит поведение наследуемое потомками:
у каждой функции в JS есть свойство prototype, но только у функций! Класс в JS — это синтаксический сахар вокруг функции-конструктора, следовательно, у классов тоже есть свойство prototype.
Потомок связан с родителем свойством __proto__, которое указывает на свойство prototype родителя, в котором в свою очередь хранится своя ссылка __proto__, указывающая на его родителя. Такая связь называется цепочка прототипов, а сам механизм такого наследования называется прототипное наследование.
Попробуйте самостоятельно написать несколько функций-конструкторов используя старый синтаксис, это расставит все точки над i.
Полезные ссылки
Бесплатный вебинар от OTUS: "Обзор технологий для построения API"
Статья-размышление о наследовании в JS, о которой я упоминал выше
Статья затрагивающая еще и оператор new
Классное видео, где вы можете потренировать полученную информацию