[UA] Прототипи (JS)
Механізм роботи прототипів в 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__. З цього часу це актуальні способи взаємодії з прототипами. В той час як __pro
![[UA] Прототипи (JS)](https://media2.dev.to/dynamic/image/width%3D1000,height%3D500,fit%3Dcover,gravity%3Dauto,format%3Dauto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5vxxx106bljfvui93jy.png)
Механізм роботи прототипів в 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]