DEV Community

Michail_Novos
Michail_Novos

Posted on

[UA] Прототипи (JS)

Механізм роботи прототипів в JS дозволяє об'єктам успадковувати функціонал (властивості і методи) один одного, на відміну від класичних ООП мов програмування, в яких подібний результат досягається завдяки класам і успадкуванню класів. Проте, не дивлячись на не класичний спосіб реалізації, ми маємо повноцінне "успадкування", описане в парадигмі ООП.

Кожний об'єкт має прототип, лянцюг прототипів може продовжуватися, тобто прототип об'єкта так само може мати свій прототип. Якщо прототип дорівнює null - це означає, що прототипу не існує і те, що ланцюг закінчився.

Коли використовується властивість або метод об'єкта, йде перевірка чи вказана властивість/метод є у самого цільового об'єкта. Якщо немає - йде перевірка у прототипа цього об'єкту. і так далі по ланцюгу. Як тільки властивість/метод знайдено - пошук закінчено. Таким чином, якщо і у цільового об'єкта і у його прототипа є властивість з однаковою назвою - цільовий об'єкт буде мати пріоритет, через те, що він у ланцюгу перший.

Отримати прототип об'єкта можна використовуючи метод Object.getPrototypeOf. Зараз це вважається стандартною і рекомендованою практикою.

const obj = {}; // Власноруч створений пустий об'єкт
console.log(obj.toString()); // '[object Object]'
// Виклик toString не буде помилкою, бо такий метод буде знайдено в прототипі
console.log(Object.getPrototypeOf(obj).toString()); // Той самий результат
Enter fullscreen mode Exit fullscreen mode

Існує ще старий, наразі не рекомендований спосіб отримати прототип - через властивість __proto__ у цільового об'єкта.

Створення та зміна прототипів

Object.create

Object.create(object) - створює новий об’єкт, встановлюючи як прототип об’єкт, що був переданий першим аргументом.

const ConsoleLogger = {
    log(msg) {
        console.log("[ConsolleLogger]: " + msg);
    }
};
const consoleLogger = Object.create(ConsoleLogger);
consoleLogger.log("Hello world!");
Enter fullscreen mode Exit fullscreen mode

Функції конструктори

Функції конструктори мають спеціальну властивість - 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);
Enter fullscreen mode Exit fullscreen mode

Object.setPrototypeOf

Частково схоже на Object.create, але замість створення нового об'єкта - змінює прототип існуючого. Використання цього метода не рекомендується з-за можливих проблем з оптимізацією внутрішньої імплементації Object.setPrototypeOf. Також зміна прототипів існуючих об'єктів може призвести до неочікуваної поведінки.

const ConsoleLogger = {
    log(msg) {
        console.log("[ConsolleLogger]: " + msg);
    }
};
const logger = Object.setPrototypeOf({}, ConsoleLogger);
Enter fullscreen mode Exit fullscreen mode

Приклад проблеми з прототипами

Бувають ситуації коли необхідно зберігати пари ключ-значення (далі dictionary). Спеціально для такої задачі існує Map, проте доволі часто замість нього використовують звичайні об’єкти. Крім того, що Map є більш оптимізованим інструментом під цей конкретний сценарій, зі звичайними об’єктами є додаткові нюанси.

Звичайний об’єкт - { } не є пустим dictionary. Якраз з-за механізму прототипів можна стикнутися з неочікуваною поведінкою у разі випадкового збігу або використання властивостей, які вже є у ланцюгу прототипів.

const dictionary = {} // Очікується що пар ключ-значення немає
const key = prompt("Enter the key"); // Вводиться 'toString' або '__proto__'
console.log(Boolean(dictionary[key])); // Мало би бути false, бо dictionary щойно створений, але буде true - тому що 'toString'/'__proto__' було знайдено у прототипа
Enter fullscreen mode Exit fullscreen mode

Саме з-за подібних нюансів все ж таки варто використовувати Map для вирішення таких сценаріїв. Проте звичайні об’єкти також можна "виправити", якщо позбавити їх прототипу.

const dictionary = Object.create(null);
Enter fullscreen mode Exit fullscreen mode

Таким чином буде створений дійсно пустий об’єкт. Варто пам’ятати що в такому випадку не буде доступу до жодних методів - 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)