Механізм роботи прототипів в JS дозволяє об'єктам успадковувати функціонал (властивості і методи) один одного, на відміну від класичних ООП мов програмування, в яких подібний результат досягається завдяки класам і успадкуванню класів. Проте, не дивлячись на не класичний спосіб реалізації, ми маємо повноцінне "успадкування", описане в парадигмі ООП.
Кожний об'єкт має прототип, лянцюг прототипів може продовжуватися, тобто прототип об'єкта так само може мати свій прототип. Якщо прототип дорівнює null - це означає, що прототипу не існує і те, що ланцюг закінчився.
Коли використовується властивість або метод об'єкта, йде перевірка чи вказана властивість/метод є у самого цільового об'єкта. Якщо немає - йде перевірка у прототипа цього об'єкту. і так далі по ланцюгу. Як тільки властивість/метод знайдено - пошук закінчено. Таким чином, якщо і у цільового об'єкта і у його прототипа є властивість з однаковою назвою - цільовий об'єкт буде мати пріоритет, через те, що він у ланцюгу перший.
Отримати прототип об'єкта можна використовуючи метод Object.getPrototypeOf
. Зараз це вважається стандартною і рекомендованою практикою.
const obj = {}; // Власноруч створений пустий об'єкт
console.log(obj.toString()); // '[object Object]'
// Виклик toString не буде помилкою, бо такий метод буде знайдено в прототипі
console.log(Object.getPrototypeOf(obj).toString()); // Той самий результат
Існує ще старий, наразі не рекомендований спосіб отримати прототип - через властивість __proto__
у цільового об'єкта.
Створення та зміна прототипів
Object.create
Object.create(object)
- створює новий об’єкт, встановлюючи як прототип об’єкт, що був переданий першим аргументом.
const ConsoleLogger = {
log(msg) {
console.log("[ConsolleLogger]: " + msg);
}
};
const consoleLogger = Object.create(ConsoleLogger);
consoleLogger.log("Hello world!");
Функції конструктори
Функції конструктори мають спеціальну властивість - prototype
. Об’єкти, що створені такими функціями, а саме через оператор new (в тому числі і класи звичайно), будуть мати як прототип той самий prototype
функції конструктора.
function ConsoleLogger() {}
ConsoleLogger.prototype.log = function(msg) {
console.log("[ConsolleLogger]: " + msg);
}
const logger = new ConsoleLogger();
logger.log("Hello world!");
console.log(Object.getPrototypeOf(logger) === ConsoleLogger.prototype);
Object.setPrototypeOf
Частково схоже на Object.create
, але замість створення нового об'єкта - змінює прототип існуючого. Використання цього метода не рекомендується з-за можливих проблем з оптимізацією внутрішньої імплементації Object.setPrototypeOf
. Також зміна прототипів існуючих об'єктів може призвести до неочікуваної поведінки.
const ConsoleLogger = {
log(msg) {
console.log("[ConsolleLogger]: " + msg);
}
};
const logger = Object.setPrototypeOf({}, ConsoleLogger);
Приклад проблеми з прототипами
Бувають ситуації коли необхідно зберігати пари ключ-значення (далі dictionary). Спеціально для такої задачі існує Map
, проте доволі часто замість нього використовують звичайні об’єкти. Крім того, що Map
є більш оптимізованим інструментом під цей конкретний сценарій, зі звичайними об’єктами є додаткові нюанси.
Звичайний об’єкт - { }
не є пустим dictionary. Якраз з-за механізму прототипів можна стикнутися з неочікуваною поведінкою у разі випадкового збігу або використання властивостей, які вже є у ланцюгу прототипів.
const dictionary = {} // Очікується що пар ключ-значення немає
const key = prompt("Enter the key"); // Вводиться 'toString' або '__proto__'
console.log(Boolean(dictionary[key])); // Мало би бути false, бо dictionary щойно створений, але буде true - тому що 'toString'/'__proto__' було знайдено у прототипа
Саме з-за подібних нюансів все ж таки варто використовувати Map для вирішення таких сценаріїв. Проте звичайні об’єкти також можна "виправити", якщо позбавити їх прототипу.
const dictionary = Object.create(null);
Таким чином буде створений дійсно пустий об’єкт. Варто пам’ятати що в такому випадку не буде доступу до жодних методів - toString
, hasOwnProperty
і т.д. І це також доволі легко пропустити і забути в моменті, що так само може призвести до різних неочікуваних помилок.
Історія розвитку роботи з прототипами доволі цікава
- Властивість
prototype
у функцій конструкторів була від самого початку, це найстаріший спосіб створити об'єкт з кастомним прототипом. - Далі десь в проміжку між 2009 і 2015 роками з'явився метод
Object.create
. Він дозволяє створити об'єкт з вказаним прототипом. Однак не надає можливості його отримати або змінити. Отже, в браузерах імплементували властивість__proto__
для більш зручної роботи з прототипами, проте ця поведінка не була стандартизованою. - З виходом ES6 у 2015 році були додані методи
Object.getPrototypeOf
таObject.setPrototypeOf
, що виконували однакову функцію як з__proto__
. З цього часу це актуальні способи взаємодії з прототипами. В той час як__proto__
досі можна використовувати, він не є стандартом і його підтримка не гарантується, з-за цих причин і варто його уникати.
- Телеграм
- Ресурси:
[07.03.2025 - 23:15 Kyiv time]
Top comments (0)