Прототипне спадкування. __proto__ & prototype для чайників
У цій статті поговоримо про тему, яка може здаватися дуже складною для початківців та колись була непростою і для мене. Але я спробую розповісти про це максимально доступною мовою, майже на пальцях.
Увага. Деякі приклади та розʼяснення в цій статті більш поверхові та написані винятково для загального розуміння принципу роботи прототипів і спадкування. Насправді існує величезна кількість нюансів та уточнень, але я вважаю, що вони наразі будуть зайві.
Будемо рухатись повільно, послідовно, щоб в кінці не залишилось жодних запитань.
Створення сутностей в 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
. І саме з нього вже успадковуються усі методи та властивості.
Насправді ця стаття не розкриває цю тему повністю, бо вона дуже складна і велика, але сподіваюся, що мені вдалося зробити так, щоб у вас зʼявилося хоча б її базове розуміння.
6 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів