Прототипне спадкування. __proto__ & prototype для чайників

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

У цій статті поговоримо про тему, яка може здаватися дуже складною для початківців та колись була непростою і для мене. Але я спробую розповісти про це максимально доступною мовою, майже на пальцях.

Увага. Деякі приклади та розʼяснення в цій статті більш поверхові та написані винятково для загального розуміння принципу роботи прототипів і спадкування. Насправді існує величезна кількість нюансів та уточнень, але я вважаю, що вони наразі будуть зайві.

Будемо рухатись повільно, послідовно, щоб в кінці не залишилось жодних запитань.

Створення сутностей в JS

Не будемо занурюватися в ООП, але сподіваюся, що той, хто читає цю статтю, має базове розуміння ООП, що таке class, його поведінку і принцип спадкування.

Для того, аби освіжити памʼять, усе ж таки напишу приклад класу та принцип його роботи.

Так ми згадали, що екземпляри класу успадковують його властивості та методи. Батьківський клас Dog у цьому разі є прототипом для обʼєктів goldenRetriever та collie.

Так ось. Будь-яка сутність, яка має посилальний тип даних, є обʼєктом, та успадковує властивості та методи від свого головного і єдиного батьківського класу.

Спадкування властивостей і методів

Зверніть увагу: const man = {} і const man = new Object() — це два різних способів створення обʼєктів, але сенс у тому, що в обох випадках нові обʼєкти успадковують властивості та методи в головного класу Object. Тобто десь під капотом в JavaScript існує щось на кшталт цього:

Це, звісно, лише приклад для розуміння. Внутрішня реалізація JavaScript набагато складніша.

Тепер ви розумієте, що коли ми створюємо новий масив і викликаємо у нього якісь методи, вони насправді успадковані від головного, першого і єдиного батьківського класу Array.

__proto__

Цей Array, по суті, є прототипом усіх масивів, які ми створюємо.

Внутрішня властивість кожного об’єкта в JavaScript, яка вказує на його прототип, від якого можна успадковувати властивості та методи називається __proto__.

Більш наочно:

Згадаймо наш клас Dog та його нові екземпляри. Так ось:

Так само працює і з іншими посилальними типами даних.

Приклад з іншої сторони:

Тобто змінні obj та arr мають різні прототипи, тому і фактичні посилання на них (__proto__) теж різні. Вони успадковані від різних батьківських класів.

prototype

А ось «місце», де зберігаються усі властивості та методи сутності, яка була успадкована від іншої сутності в JavaScript, має назву prototype.

А тепер правильними словами: prototype — це внутрішня властивість об’єкта (яка також є обʼєктом), що використовується для посилання на властивості та методи його прототипу (__proto__).

Спадкування може відбуватися не тільки у класів, а й у будь-яких обʼєктів, а також не тільки під час створення якогось об’єкта, але й пізніше:

Приклад встановлення прототипа вище приведений тільки для простішого розуміння принципу роботи прототипів і спадкування. Так робити не варто через те, що __proto__ є нестандартною властивістю, ця властивість не описана в стандарті ECMAScript. Він був введений як неформальне розширення в різних реалізаціях JavaScript (наприклад, у браузерах), але воно ніколи не було частиною офіційного стандарту.

Краще розгляньмо одну з правильних та очевидних реалізацій цього. Наприклад, за допомогою спеціальних властивостей Object.setPrototypeOf:

Іншими словами:

Метод Object.setPrototypeOf встановлює myPrototype як прототип для myObject.

Я згоден, що дуже легко заплутатись, тому ще раз:

prototype — це об’єкт, який містить методи та властивості, успадковані від прототипа, і він використовується для визначення цих методів і властивостей для нових об’єктів.

__proto__ — це посилання на конкретний прототип об’єкта, і він використовується для доступу до методів і властивостей, які належать цьому прототипу.

Також важливо розуміти, що сутності, які успадковуються від інших, насправді успадковують властивості та методи не тільки від своїх безпосередніх батьків (прототипів), але й батьків їхніх батьків. А також глобальні властивості й методи, які є доступними в кореневому об’єкті, від якого утворюються всі об’єкти в JavaScript.

А тепер довгий, але наочний приклад:

Як воно працює

Розгляньмо наш попередній приклад і тоді зрозуміємо, яким способом сам JavaScript добирається до одного чи того методу. У змінної goldenRetriever ми викликаємо метод hasOwnProperty. Спочатку інтерпретатор заходить у властивості goldenRetriever і шукає там метод hasOwnProperty

Оскільки він його не знаходить, тому що він відсутній в самому екземплярі, він перевіряє, чи є в сутності goldenRetriever властивість prototype.

Якщо властивість prototype існує, інтерпретатор шукає метод hasOwnProperty вже в прототипі.

І, звісно, він її теж там не знаходить, бо у класу Dog теж немає такого методу.

Він знову перевіряє, чи є у прототипа теж властивість prototype. Якщо є, то він починає шукати потрібний метод там.

І знову не знаходить, тому що і в нашому головному батьківському класі Animal теж ії немає. Знову інтерпретатор перевіряє наявність властивості prototype, і якщо вона є, шукає наш метод там.

І нарешті інтерпретатор знаходить потрібний нам метод, тому що він є у головного обʼєкта JavaScript, від якого успадковуються всі об’єкти.

Інтерпретатор JavaScript буде шукати метод в об’єкті, і якщо його не знайде там, він перевірить прототип об’єкта. У головного об’єкта JavaScript немає властивості prototype, оскільки він сам не успадковує жодних властивостей. Він є початковим об’єктом. Тому коли інтерпретатор дістається до цього місця і бачить, що властивості prototype не існує, він генерує помилку, оголошуючи, що не вдалося знайти метод, який ми намагаємося викликати в цій сутності.

Описане вище називається ланцюжком прототипів.

Спробуємо викликати метод, що не існує:

Метод cat() не знайдений в жодному об’єкті в ланцюжку прототипів, що відповідає тексту помилки Uncaught TypeError: goldenRetriever.cat is not a function. Важливо розуміти, що недостатньо лише наявності властивості prototype в об’єкті. Методи повинні бути визначені в цьому прототипі, щоб бути доступними через пошук в ланцюжку.

У вище описаному процесі фактично трапилось таке:

Перший __proto__ вказує на прототип об’єкта goldenRetriever, який є екземпляром класу Dog.

Другий __proto__ вказує на прототип об’єкта Dog, який є екземпляром класу Animal.

Третій __proto__ вказує на прототип об’єкта Animal, який уже є вбудованим об’єктом в JavaScript (Object).

Примітивні типи замість висновків

Я вважаю, що цей розділ вартий окремої статті. Але поверхово треба пробігтися основними моментами.

Тут уже цікаво. Адже примітивні типи не мають прототипів і не мають власних методів. А як же тоді ми викликаємо методи у строк або чисел, якщо їх немає?

Через те, що далі буде написано, існують навіть суперечки серед програмістів стосовно того, що насправді примітивних типів в JavaScript взагалі не існує. Але ми будемо дотримуватися офіційної специфікації мови.

Річ у тому, що самі собою такі примітивні типи як строка або число справді не мають жодних методів і властивостей. Але коли ми хочемо викликати методи, наприклад, toUpperCase() у строки, то інтерпретатор створює тимчасову обгортку над змінною у вигляді об’єкта, який має прототип — стандартний клас String. І саме з нього вже успадковуються усі методи та властивості.

Насправді ця стаття не розкриває цю тему повністю, бо вона дуже складна і велика, але сподіваюся, що мені вдалося зробити так, щоб у вас зʼявилося хоча б її базове розуміння.

👍ПодобаєтьсяСподобалось9
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

В першу чергу похвалю за написання статті — як на мене, це ознака, що автор дійсно намагається розібратися, як воно ото все працює всередині. Хороший фундамент для подальшого розвитку. Також гарно описаний алгоритм, як інтерпретатор шукає наявність у обʼєкта протерті або методу.

Але і мінуси є. Загалом, не рекомендував би статтю новачкам — це більше підсумки того, що знає автор, ніж навчальний матеріал. Тобто, автор щось вивчив, не впевнений, що повністю зрозумів, і спробував якось організувати нові знання, що вийшла ціла стаття. Проте, читаючи її, не виникає відчуття, що автор дійсно розуміє, про що пише.
В статті згадане проперті prototype, але жодного разу не показане в коді. Так, написано prototype, показано допоміжний __proto__ та «внутрішньо-консольний» [[Prototype]], і все це є посиланням на один і той самий обʼєкт. Але як воно взаємоповʼязане між собою, та чому на один обʼєкт аж стільки посилань — не вказано.
Як правильно зазначив нижче один з коментаторів — цю тему найкраще зрозуміти через стандарт ES5, а саме — як описувалися класи у js, коли ще не було синтаксису класів. Ще й стане зрозуміло, чому в MDN заголовки документації до одних методів мають властивість prototype, а інші ні :)

Хороша стаття для новачків, або для людей, які хочуть освіжити память.
По досвіду проведення інтервю, я б рекомендував все таки для початку добре розібратись в цілому — що таке прототип паттерн, для чого він потрібен, що порушує, а що дає взамін. Рекомендую глянути refactoring guru, там доволі лаконічно розписано простою мовою, що таке прототип, також можна глянути реалізацію прототипа з нуля в псевдо-коді, або ТС.

По останньому абзацу можна дійсно написати цілу статтю — boxing/unboxing типів, можна приклади з інших мов навести. Також дальше по суміжній темі можна уже і прочитати про ad hoc поліморфізм/приклад в JS

Часто забуваю різницю між прото і прототипом бо не використовую тому зайшов освіжити знання, та стаття загнала моє забуття ще глибше.

поговоримо про тему, яка може здаватися дуже складною для початківців
сподіваюся, що той, хто читає цю статтю, має базове розуміння ООП, що таке class, його поведінку і принцип спадкування.

Тому початківцям і складно — бо замість того, щоб розповідати саме про успадкування як механізм вирішення конкретних практичних проблем, та про його реалізацію в JS (яка є в реалі простішою за будь-яку іншу мейнстрімну мову), їм в голову долблять класами — які в джс ще й не «пасажири першого класу» (хоча вже й не хімічно чистий синтаксичний цукор, як спочатку). Ви самі ж робите початківцям боляче...

__proto__ - варто зазначити що вже давно депрікейт. Сам MDN каже не юзати його для чейнінгу та іншого.

Дякую за статтю, буквально нещодавно згадував як все влаштовано «під капотом», т.я. через відсутність прямої практики з прототипами, доволі швидко ця тема забувається.

Я думаю найкраще, як для мене колись було, prototype пояснюється на прикладі функцій-конструкторів. Зараз з класами ця частина сприймається трохи складніше, бо ти по суті не бачиш, де і як ця властивість фігурує при створені обʼєкту.

Підписатись на коментарі