Как стать автором
Обновить
829.84
OTUS
Цифровые навыки от ведущих экспертов

__proto__ и prototype

Время на прочтение6 мин
Количество просмотров33K

Часто на собеседовании опытный разработчик может спросить у начинающего: «Что такое __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() у этого экземпляра, с точки зрения памяти это выглядит так:

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

Полезные ссылки

Теги:
Хабы:
Всего голосов 21: ↑17 и ↓4+15
Комментарии10

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS