Пошук уроків, статей та іншого контенту
Використаєте Object.getPrototypeOf і Object.setPrototypeOf для читання та зміни прототипів і оціните пов'язані ризики.
У JavaScript кожен об’єкт може мати внутрішній зв’язок з іншим об’єктом — прототипом. Якщо властивість або метод не знайдено безпосередньо в об’єкті, JavaScript шукає їх у його прототипі, потім у прототипі прототипу і так далі.
Цей ланцюжок називають ланцюжком прототипів.
const animal = {
eat() {
return "Тварина їсть";
}
};
const dog = Object.create(animal);
console.log(dog.eat()); // "Тварина їсть"
console.log(dog.hasOwnProperty("eat")); // falseОб’єкт dog не має власної властивості eat, але може скористатися методом із прототипу animal.
Власні властивості об’єкта та властивості прототипу — це різні речі:
console.log(Object.hasOwn(dog, "eat")); // false
console.log("eat" in dog); // trueObject.hasOwn(object, property) перевіряє лише власні властивості.
Оператор in перевіряє весь ланцюжок прототипів.
Object.getPrototypeOfМетод Object.getPrototypeOf() повертає безпосередній прототип об’єкта.
const vehicle = {
move() {
return "Рух";
}
};
const car = Object.create(vehicle);
console.log(Object.getPrototypeOf(car) === vehicle); // true
console.log(Object.getPrototypeOf(vehicle) === Object.prototype); // trueУ цьому прикладі ланцюжок має такий вигляд:
car → vehicle → Object.prototype → nullДля об’єкта, створеного через об’єктний літерал, прототипом зазвичай є Object.prototype:
const user = {
name: "Олена"
};
console.log(Object.getPrototypeOf(user) === Object.prototype); // trueЗавершенням стандартного ланцюжка є null:
console.log(Object.getPrototypeOf(Object.prototype)); // nullObject.getPrototypeOf() корисний, коли потрібно:
перевірити походження об’єкта;
визначити, чи належить об’єкт певному прототипу;
дослідити ланцюжок наслідування;
працювати з об’єктами, створеними через Object.create().
const personPrototype = {
greet() {
return `Привіт, я ${this.name}`;
}
};
const person = Object.create(personPrototype);
person.name = "Марко";
if (Object.getPrototypeOf(person) === personPrototype) {
console.log(person.greet()); // "Привіт, я Марко"
}Не слід плутати прототип об’єкта з його конструктором. Прототип — це конкретний об’єкт, з якого виконується пошук властивостей.
Object.setPrototypeOfМетод Object.setPrototypeOf(object, prototype) змінює безпосередній прототип уже створеного об’єкта.
const firstPrototype = {
describe() {
return "Перший прототип";
}
};
const secondPrototype = {
describe() {
return "Другий прототип";
}
};
const item = Object.create(firstPrototype);
console.log(item.describe()); // "Перший прототип"
Object.setPrototypeOf(item, secondPrototype);
console.log(item.describe()); // "Другий прототип"Після виклику Object.setPrototypeOf() пошук методу describe відбувається вже в secondPrototype.
Новий прототип може бути:
об’єктом;
null.
const record = {
id: 10
};
console.log(record.toString); // функція з Object.prototype
Object.setPrototypeOf(record, null);
console.log(Object.getPrototypeOf(record)); // null
console.log(record.toString); // undefinedТак можна створити об’єкт без успадкованих властивостей Object.prototype. Проте такий об’єкт поводиться незвично: у нього немає методів toString, hasOwnProperty та інших стандартних методів прототипу.
Розглянемо об’єкт, який спочатку використовує прототип для звичайних повідомлень, а потім отримує іншу поведінку.
const regularLogger = {
log(message) {
console.log(`[INFO] ${message}`);
}
};
const silentLogger = {
log() {
// Навмисно нічого не виводимо
}
};
const service = Object.create(regularLogger);
service.log("Запуск сервісу"); // [INFO] Запуск сервісу
Object.setPrototypeOf(service, silentLogger);
service.log("Це повідомлення не буде показано");
console.log(Object.getPrototypeOf(service) === silentLogger); // trueТакий підхід працює, але в більшості прикладних ситуацій краще передати потрібну поведінку під час створення об’єкта або використати композицію.
Наприклад, замість зміни прототипу можна створити об’єкт одразу з потрібним прототипом:
const service = Object.create(regularLogger);
service.log("Сервіс запущено");Object.create() та Object.setPrototypeOf()Ці методи вирішують різні задачі:
Object.create(prototype) створює новий об’єкт із заданим прототипом.
Object.setPrototypeOf(object, prototype) змінює прототип уже наявного об’єкта.
Object.getPrototypeOf(object) читає поточний прототип.
Коли прототип відомий під час створення об’єкта, перевагу зазвичай надають Object.create():
const accountPrototype = {
getBalance() {
return this.balance;
}
};
const account = Object.create(accountPrototype);
account.balance = 500;
console.log(account.getBalance()); // 500Зміна прототипу може знадобитися під час низькорівневої роботи з об’єктами, міграції даних або реалізації спеціальних механізмів сумісності. Але це не типовий спосіб організації звичайної бізнес-логіки.
Після зміни прототипу об’єкт може отримати інші методи або втратити доступ до попередніх.
const readable = {
format() {
return "Читабельний формат";
}
};
const json = {
format() {
return JSON.stringify(this);
}
};
const data = Object.create(readable);
data.value = 42;
console.log(data.format()); // "Читабельний формат"
Object.setPrototypeOf(data, json);
console.log(data.format()); // {"value":42}Якщо код очікує, що об’єкт завжди має певний метод, динамічна зміна прототипу може спричинити помилки.
JavaScript-рушії оптимізують об’єкти, припускаючи, що їхня структура та прототипи не змінюються часто. Виклик Object.setPrototypeOf() може змусити рушій скасувати частину оптимізацій.
Тому не варто змінювати прототипи об’єктів у циклах або для великої кількості об’єктів без вагомої причини:
// Небажаний підхід для великої кількості об'єктів
for (const item of items) {
Object.setPrototypeOf(item, somePrototype);
}Краще створювати об’єкти з потрібним прототипом відразу:
const items = values.map((value) => {
const item = Object.create(somePrototype);
item.value = value;
return item;
});Якщо прототип змінюється на об’єкт із властивостями, яких не очікує код, це може вплинути на перевірки через in, for...in і звичайне читання властивостей.
const prototype = {
isAdmin: true
};
const user = {
name: "Олег"
};
Object.setPrototypeOf(user, prototype);
console.log(user.isAdmin); // true
console.log("isAdmin" in user); // true
console.log(Object.hasOwn(user, "isAdmin")); // falseВластивість isAdmin не належить самому user, але код може помилково сприйняти її як його власні дані.
Object.hasOwnЯкщо потрібно перевірити саме власну властивість, використовуйте Object.hasOwn():
const defaults = {
color: "blue"
};
const options = Object.create(defaults);
options.size = "large";
console.log("color" in options); // true
console.log(Object.hasOwn(options, "color")); // false
console.log(Object.hasOwn(options, "size")); // trueНе варто без потреби викликати object.hasOwnProperty(property), оскільки:
метод може бути перевизначений;
об’єкт може мати прототип null;
властивість hasOwnProperty може бути відсутньою.
const value = Object.create(null);
value.name = "Тест";
console.log(Object.hasOwn(value, "name")); // trueДані з HTTP-запитів, файлів або користувацьких форм можуть містити довільні ключі. Не слід без перевірки додавати такі ключі до прототипів або використовувати їх для зміни прототипу.
Особливо небезпечним є неконтрольоване поєднання зовнішніх даних із механізмами на кшталт:
Object.setPrototypeOf(target, externalValue);У прикладному коді прототип має бути визначений програмою, а не довільним значенням із зовнішнього джерела.
Для словників із довільними ключами іноді зручно використовувати об’єкт без прототипу:
const dictionary = Object.create(null);
dictionary["constructor"] = "звичайне значення";
dictionary["toString"] = "ще одне значення";
console.log(dictionary.constructor); // "звичайне значення"
console.log(Object.getPrototypeOf(dictionary)); // nullПроте такі об’єкти не мають стандартних методів Object.prototype, тому для них потрібно використовувати статичні методи Object, наприклад Object.keys() і Object.hasOwn().
Якщо потрібна одна з кількох поведінок, можна створювати об’єкт із потрібним прототипом одразу:
const formats = {
text: {
serialize(value) {
return String(value);
}
},
json: {
serialize(value) {
return JSON.stringify(value);
}
}
};
function createSerializer(type) {
const prototype = formats[type];
if (!prototype) {
throw new Error(`Невідомий формат: ${type}`);
}
const serializer = Object.create(prototype);
serializer.type = type;
return serializer;
}
const textSerializer = createSerializer("text");
const jsonSerializer = createSerializer("json");
console.log(textSerializer.serialize(123)); // "123"
console.log(jsonSerializer.serialize({ id: 1 })); // {"id":1}Тут прототип не змінюється після створення об’єкта.
Часто замість наслідування зручніше зберігати потрібну поведінку як властивість:
function createReport(formatter) {
return {
format(data) {
return formatter(data);
}
};
}
const jsonReport = createReport((data) => JSON.stringify(data));
const textReport = createReport((data) => String(data));
console.log(jsonReport.format({ total: 10 })); // {"total":10}
console.log(textReport.format(10)); // "10"Композиція не змінює ланцюжок прототипів і робить залежності явними.
Object.setPrototypeOf() та класиЕкземпляри класів також мають прототип. Зазвичай він визначається автоматично:
class User {
greet() {
return `Привіт, ${this.name}`;
}
}
const user = new User();
user.name = "Ірина";
console.log(Object.getPrototypeOf(user) === User.prototype); // trueТехнічно прототип екземпляра можна змінити, але це може порушити очікувану поведінку класу:
const otherPrototype = {
greet() {
return "Інше привітання";
}
};
Object.setPrototypeOf(user, otherPrototype);
console.log(user.greet()); // "Інше привітання"
console.log(user instanceof User); // falseПісля цього user більше не вважається екземпляром User за допомогою оператора instanceof, оскільки його прототип більше не дорівнює User.prototype.
const prototype = {
role: "user"
};
const account = Object.create(prototype);
console.log(account.role); // "user"
console.log(Object.hasOwn(account, "role")); // falseЗначення доступне через ланцюжок прототипів, але не збережене безпосередньо в account.
in, коли потрібна власна властивістьconst defaults = {
enabled: true
};
const settings = Object.create(defaults);
if ("enabled" in settings) {
// Умова істинна, хоча властивість не є власною
}
if (Object.hasOwn(settings, "enabled")) {
// Умова хибна
}Другий аргумент Object.setPrototypeOf() має бути об’єктом або null.
const object = {};
Object.setPrototypeOf(object, null); // коректно
Object.setPrototypeOf(object, {}); // коректноПередача примітивного значення спричиняє помилку:
const object = {};
try {
Object.setPrototypeOf(object, 42);
} catch (error) {
console.log(error instanceof TypeError); // true
}Якщо можна створити об’єкт через Object.create(), фабричну функцію або клас, не варто спочатку створювати об’єкт, а потім змінювати його прототип.
Object.setPrototypeOf() не копіює властивості з нового прототипу в об’єкт:
const prototype = {
color: "green"
};
const item = {};
Object.setPrototypeOf(item, prototype);
console.log(item.color); // "green"
console.log(Object.hasOwn(item, "color")); // falsecolor залишається властивістю prototype, а не item.
Прототип — це об’єкт, у якому JavaScript шукає властивості, не знайдені безпосередньо в поточному об’єкті.
Object.getPrototypeOf(object) повертає безпосередній прототип об’єкта.
Object.setPrototypeOf(object, prototype) змінює прототип уже створеного об’єкта.
Прототипом можна зробити об’єкт або null.
Object.create(prototype) зазвичай кращий за зміну прототипу після створення.
Object.hasOwn() перевіряє лише власні властивості, а in — власні та успадковані.
Часті зміни прототипів можуть погіршити продуктивність і ускладнити код.
Не можна дозволяти зовнішнім даним неконтрольовано визначати прототипи об’єктів.
Для динамічної поведінки часто безпечнішими є фабричні функції та композиція.