Я хорошо помню, как впервые столкнулся с прототипами в JavaScript. Это было в самом начале моего пути к программированию, и когда я начал углубляться в эту концепцию, я просто пришел к выводу: Ну… классы делают то же самое, поэтому я не собираюсь беспокоить…. Мое каким-то поверхностное понимание ООП позволило мне создавать простые программы, но это не зашло меня слишком далеко, так как я начал сталкиваться с ошибками, которые я не мог понять и исправить. В Launch School изучение прототипов стало неизбежным, поэтому пришлось попробовать.

Эта концепция была, безусловно, самой сложной из тех, с которыми мне приходилось сталкиваться (контекст выполнения тоже был довольно забавным). Я вспоминаю необъяснимое желание выбросить ноутбук в окно вместо того, чтобы тратить больше секунды на «попытки» понять, как работает JavaScript. Чем больше я читал, тем больше сбивался с толку, когда меня знакомили с прототипами, __proto__, [[Prototype]], prototype свойствами, прототипами функций и прототипами объектов (которые, кстати, не одно и то же). Я должен признать, что человек, который представил все эти термины в JavaScript, мог бы проявить немного больше творчества при выборе имен.

Однако кем бы вы ни были, кто бы ни читал эту статью, я хочу сообщить вам, что в конце туннеля есть свет! Понимание прототипов JavaScript на самом деле не такая уж далекая и недостижимая цель, как может показаться на первый взгляд. На самом деле все очень просто! Ждать! Прежде чем ты начнешь кидать меня тухлыми помидорами за такие слова, дай мне шанс! Обещаю, мы вместе сразимся с этим чудовищем!

Почему прототипы такие запутанные?

Давайте начнем с понимания того, почему так много людей находят прототипы такими запутанными. На то есть две причины. Во-первых, если кто-то уже знал о ООП на других объектно-ориентированных языках, это очевидно. Это сбивает с толку, потому что это другое. В JavaScript не используется типичное наследование классов, как в других языках, поэтому по этой причине, если вы узнали о ООП на других языках, лучше забыть об этом и подходить к прототипам JS с чистого листа и непредвзято. Вторая причина заключается в том, что слово «прототип» в JavaScript на самом деле означает несколько вещей. В этой статье я попытаюсь объяснить всю эту запутанную терминологию и продемонстрирую, как это связано.

[[Прототип]]

Начнем с этого «странного», которое вы, наверное, видели раньше. Что это за загадочный [[Prototype]] и в чем его функция?

Каждый раз, когда мы создаем объект, он автоматически получает этот необычный «подарок» - свойство [[Prototype]]. Если вы его поищете, то не найдете, так как он спрятан. Вы не можете его увидеть, вы не можете прикоснуться к нему, вы не можете почувствовать его запах, но он присутствует на каждом объекте, и он очень мощный, что мы увидим через мгновение. Я приглашаю вас писать код вместе со мной.

Допустим, мы создали объект babyYoda:

let babyYoda = {};
babyYoda['age'] = 'no one knows';
babyYoda.hasOwnProperty('age') // => true
babyYoda.hasOwnProperty('[[Prototype]]') // => false

Как видите, наш «малыш Йода» на самом деле не многого и не умеет делать. У него просто есть «возраст» и скрытое свойство «[[Prototype]]», которое мы не можем увидеть. Скажем так, мы бы хотели, чтобы он умел телекинезом. Многие джедаи также используют эту силу, поэтому имеет смысл использовать ее повторно, а не создавать это свойство непосредственно на «babyYoda». Для этого мы должны как-то предоставить ему доступ ко всем этим знаниям. Куда бы он пошел, чтобы приобрести такую ​​мудрость? Конечно джедаям! Давайте создадим объект, обладающий всеми этими великими силами:

let jediForces = {
  telekinesis() {
    console.log('moving objects with thoughts')
  },
  instinctiveAstrogation() {
    console.log('find a route through hyperspace');
  },
  flowWalking() {
    console.log('change the course of the future')
},
//all the other Jedi forces...
};

В настоящий момент «малыш Йода» не умеет телекинез:

babyYoda.telekinesis(); //TypeError: babyYoda.telekinesis is not a function

Как мы можем заставить «малышку Йоду» делать все эти удивительные вещи? Мы могли бы просто создать метод «телекинеза» для объекта «babyYoda». Однако это было бы излишним, если у нас уже есть объект с собственным «телекинезом», который делает именно то, что нам нужно. Представьте, что у нас есть несколько джедаев, которые тоже любят телекинез. Было бы бессмысленно определять этот метод для каждого из них. Вместо этого мы делегируем доступ к этому методу. Давайте посмотрим на этот фрагмент кода:

Object.setPrototypeOf(babyYoda, jediForces);
babyYoda.telekinesis(); //’moving object with thoughts’
babyYoda.hasOwnProperty('telekinesis'); //false
jediForces.hasOwnProperty('telekinesis') //true

Это просто волшебство, но как это случилось? В этом сила «[[Прототип]]»! Что мы сделали с «Object.setPrototype ()»: мы использовали его для установки скрытого свойства «[[Prototype]]», указывающего на «jediForces», и с этим мы создали связь между этими двумя объектами. Это соединение, которое дает одному объекту возможность доступа к методам другого объекта, мы называем цепочкой прототипов. Теперь «малыш Йода» может использовать все эти фантастические способности, не создавая их специально для себя, благодаря цепочке прототипов, которая связывает его со всеми «силами джедаев»! Вы можете представить «[[Prototype]]» как указатель, который сообщает одному объекту, где искать силы / функции / методы, к которым этот объект хочет иметь доступ.

Как это возможно и как это работает? Каждый раз, когда мы вызываем метод объекта, JavaScript сначала ищет этот метод в самом объекте. Если не найден, он будет смотреть на «[[Prototype]]» и на то, на что он указывает, и искать в следующем объекте в цепочке прототипов. Если он не найден, JS будет продолжаться до конца цепочки прототипов. Какой объект находится в конце цепочки прототипов? Если не указано иное, все объекты имеют «Object.prototype» в конце цепочки. Скрытый [[Prototype]] из Object указывает на null. Мы скоро вернемся к тому, что представляет собой таинственная prototype собственность на Object. Вы можете представить цепочку прототипов babyYoda так:

Есть несколько способов связать объект через цепочку прототипов. Мы уже видели Object.setPrototypeOf() в действии, но мы также можем использовать Object.create(), который создает и возвращает совершенно новый объект, внутреннее свойство [[Prototype]] которого настроено так, чтобы указывать на другой объект, который мы передаем в качестве аргумента. Мы также могли бы использовать newkeyword, который вызывает функцию как конструктор. (мы не собираемся здесь останавливаться). Также есть __proto__property. Поскольку он объявлен устаревшим, мы не будем о нем много говорить, но для ясной картины полезно понять, что это такое и как работает.

__proto__

__proto__ также называется dunder proto (поскольку он имеет двойное подчеркивание) - это особый тип свойства, который позволяет нам просматривать и устанавливать объект, на который [[Prototype]] указывает.

console.log(babyYoda.__proto__ === jediForces) // => true

Давайте попробуем изменить то, что babyYoda [[Prototype]] указывает на использование __proto__:

let darkSideOfTheForce = {
  killEveryone() {
    console.log('kill everyone!!!')
  }
}
babyYoda.__proto__ = darkSideOfTheForce;
babyYoda.killEveryone(); //=> kill everyone!!!
babyYoda.telekinesis(); //TypeError: babyYoda.telekinesis is not a function

Ну, это не то, чего мы хотим babyYoda. Давайте вернем ему доступ к jediForces.

babyYoda.__proto__ = jediForces;
babyYoda.telekinesis(); //=> ‘moving objects with thoughts’

Хотя dunder proto довольно удобен, он устарел, поэтому не рекомендуется устанавливать и получать доступ к тому, на что указывает [[Prototype]]. Вместо этого используйте Object.getPrototypeOf() и Object.setPrototypeOf().

Прототипы и прототипное наследование

В большинстве случаев, когда мы говорим о прототипах, мы имеем в виду объект, от которого наследуются другие объекты. В ООП наследование - это механизм, основывающий класс на другом, создавая отношения, подобные родительско-дочернему. Из-за прототипной природы в JS наследование работает больше как делегирование, поскольку наследование позволяет «дочернему» объекту иметь доступ к методам и свойствам его «родительского» объекта (и всех объектов, которые включены в его цепочку прототипов) без явного определения их в «дочерний» объект. Если вы столкнулись с наследованием в других языках, например в Java, это может быть сюрпризом. В JavaScript мы не копируем никаких методов. Скорее JavaScript делегирует эти свойства через цепочку прототипов. Этот особый тип наследования называется прототипным наследованием, но на самом деле это не что иное, как делегирование. Важно помнить, что всякий раз, когда мы вносим изменения в метод, все объекты, имеющие доступ к этому методу, также будут видеть это изменение. В нашем примере «Звездных войн» объект jediForces является прототипом _29 _ (_ 30_ наследуется от jediForces), поскольку babyYoda имеет доступ к методам из jediForces. Такие объекты, как jediForces, мы также называем прототипами объектов, поскольку на них указывает свойство объекта [[Prototype]].

jediForces.telekinesis = function() {
  console.log('moving objects with power of the mind');
}
babyYoda.telekinesis(); // => moving objects with power of the mind

Мы можем узнать, какой объект является прототипом другого объекта, используя Object.getPrototypeOf ():

console.log(Object.getPrototypeOf(babyYoda)); //=>
// {
//   telekinesis: [Function],
//   instinctiveAstrogation: [Function: instinctiveAstrogation],
//   flowWalking: [Function: flowWalking]
// }

Мы можем создать бесконечную цепочку прототипов, и объекты, которые находятся в ее начале, будут иметь доступ ко всем свойствам всех объектов в цепочке прототипов. Обычно каждая цепочка прототипов заканчивается Object.prototype, а затем null. Давайте подробнее рассмотрим, что такое свойство prototype.

Встроенные объекты

Вы могли встретить в документации, что некоторые имена методов выглядят как Array.prototype.slice(), а другие - как Object.create(). В чем разница между одним и другим?

В JavaScript все функции на самом деле являются объектами особого типа. Эта функция позволяет нам обрабатывать функции как любое другое значение: передавать его, возвращать или принимать в качестве аргумента. Поскольку функции являются объектами, у них также есть свойства. Одно свойство, которое имеют функциональные объекты, - это свойство prototype. Это очень важное свойство, поскольку оно используется для обозначения объекта, в котором хранятся различные функции. Это то, что делает функции особенными, поскольку свойство prototype существует только для этого типа объекта. Объект, на который указывает свойство prototype, также вызывается прототипом функции. Поскольку это прототип (мы используем его для совместного использования функций) и свойство функции.

Когда вы посмотрите документацию MDN, вы найдете пару встроенных объектов, таких как Array, Object или String. Все они являются функциями-конструкторами и, следовательно, имеют свойство прототипа. Мы не будем углубляться в то, что такое конструктор, но вкратце это функция, которую мы используем для создания экземпляров многих объектов одного типа. Давайте посмотрим, как выглядит Array. Мы можем использовать Object.getOwnPropertyNames(Array) для просмотра имен всех собственных, а не скрытых свойств встроенного объекта Array:

Object.getOwnPropertyNames(Array); // => [ 'length', 'name', 'prototype', 'isArray', 'from', 'of' ]

Здесь мы видим все «собственные» свойства объекта Array. Возможно, вы уже узнали некоторых из них. Это свойства, которые мы вызываем непосредственно в самом объекте, например: Array.isArray(). Вы заметили собственность prototype? Давайте посмотрим, на что указывает это загадочное свойство prototype:

Object.getOwnPropertyNames(Array.prototype);
//=>['length',      'constructor',    'concat',
'copyWithin',  'fill',           'find',
'findIndex',   'lastIndexOf',    'pop',
'push',        'reverse',        'shift',
'unshift',     'slice',          'sort',
'splice',      'includes',       'indexOf',
'join',        'keys',           'entries',
'values',      'forEach',        'filter',
'flat',        'flatMap',        'map',
'every',       'some',           'reduce',
'reduceRight', 'toLocaleString', 'toString'
]

Вам это кажется знакомым? Как насчет того, чтобы сделать то же самое с Object.prototype? Вы заметили __proto__? Вы также можете попробовать String.prototype или Number.prototype. Здесь у нас есть все те методы, которые мы используем ежедневно! Разве это не волшебно?

Давайте немного поэкспериментируем. Как вы думаете, что здесь произойдет?

Object.prototype.find = function () {
   console.log('finding a life purpose')
};
let lifeMeaning = {};
lifeMeaning.find(); // => ???

Подождите ... Мы вызвали метод find на lifeMeaning, разве это не должно возвращать TypeError, поскольку lifeMeaning - это просто пустой объект? Что здесь происходит?

Вы помните, что является последним объектом в конце цепочки прототипов lifeMeaning? Да! Object.prototype! Объект, в котором хранятся все методы, к которым у нас есть доступ! Мы просто добавили новый метод к объекту, на который ссылается Object.prototype. Массив также имеет свойство prototype, а объект, на который он указывает, хранит все методы, такие как split(), slice(), forEach(), и, следовательно, любой массив будет иметь доступ к этим методам, даже если мы никогда явно не определяли эти функции! Разве это не невероятно?

Продолжая изучать объектно-ориентированную сторону JavaScript, важно помнить, что прототипная природа JavaScript будет заметна на каждом этапе. Полиморфизм, наследование и инкапсуляция не будут работать, как в других объектно-ориентированных языках. Точно так же синтаксис «класса», представленный в ES6, - это просто синтаксический сахар, скрывающий то, что на самом деле происходит под капотом. Хотя некоторым разработчикам понимание прототипов кажется ненужным, и многие из них отращивают много седых волос, пытаясь убить этого зверя, рано или поздно они понимают, что это неизбежно. Прототипы здесь, чтобы остаться, поэтому не игнорируйте их, молясь, чтобы они тихо ушли из-под вашей кровати.