Пошук уроків, статей та іншого контенту
Зрозумієте ланцюжок прототипів і навчитеся створювати об’єкти з потрібним прототипом через Object.create.
У JavaScript кожен об’єкт має внутрішній зв’язок із іншим об’єктом — його прототипом. Специфікація позначає цей зв’язок як [[Prototype]].
Прототип не є класом у традиційному розумінні. Це звичайний об’єкт, у якому JavaScript може шукати властивості та методи, якщо їх немає безпосередньо в поточному об’єкті.
const animal = {
eats: true,
};
const dog = Object.create(animal);
dog.name = "Rex";
console.log(dog.name); // Rex — власна властивість dog
console.log(dog.eats); // true — властивість знайдена у прототипі animalУ цьому прикладі:
dog має власну властивість name;
animal є прототипом dog;
властивість eats фізично не належить dog;
під час доступу до dog.eats JavaScript знаходить її в animal.
Перевірити прототип можна за допомогою Object.getPrototypeOf:
console.log(Object.getPrototypeOf(dog) === animal); // trueЯкщо властивість не знайдена в самому об’єкті, JavaScript переходить до його прототипу. Якщо властивості немає і там, пошук продовжується далі.
const grandparent = {
familyName: "Smith",
};
const parent = Object.create(grandparent);
parent.parentProperty = true;
const child = Object.create(parent);
child.childProperty = true;
console.log(child.childProperty); // true
console.log(child.parentProperty); // true
console.log(child.familyName); // SmithЛанцюжок має такий вигляд:
child → parent → grandparent → Object.prototype → nullПошук властивості виконується приблизно так:
Перевірити, чи є властивість у child.
Якщо немає — перевірити parent.
Якщо немає — перевірити grandparent.
Потім перевірити Object.prototype.
Якщо властивість не знайдена — дійти до null.
Повернути undefined.
console.log(Object.getPrototypeOf(child) === parent); // true
console.log(Object.getPrototypeOf(parent) === grandparent); // true
console.log(Object.getPrototypeOf(grandparent) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // nullObject.prototype — верхній стандартний прототип для звичайних об’єктів. У ньому визначені, зокрема:
toString;
valueOf;
hasOwnProperty;
isPrototypeOf;
propertyIsEnumerable.
Важливо розрізняти властивості, що належать самому об’єкту, і властивості, успадковані від прототипу.
const defaults = {
theme: "light",
language: "uk",
};
const settings = Object.create(defaults);
settings.language = "en";
console.log(settings.theme); // light
console.log(settings.language); // ensettings.language є власною властивістю і перекриває властивість із прототипу. Це називається затіненням властивості.
Перевірити, чи є властивість власною, можна за допомогою Object.hasOwn:
console.log(Object.hasOwn(settings, "language")); // true
console.log(Object.hasOwn(settings, "theme")); // falseТакож можна використати метод прототипу:
console.log(settings.hasOwnProperty("language")); // trueПроте Object.hasOwn є безпечнішим варіантом. Метод hasOwnProperty може бути:
затінений властивістю об’єкта;
відсутнім у об’єктів із null-прототипом;
перезаписаним даними з ненадійного джерела.
const data = {
hasOwnProperty: "це не метод",
};
console.log(Object.hasOwn(data, "hasOwnProperty")); // trueObject.createМетод Object.create(proto) створює новий об’єкт і встановлює proto як його прототип.
const vehicle = {
move() {
return `${this.brand} рухається`;
},
};
const car = Object.create(vehicle);
car.brand = "Toyota";
console.log(car.move()); // Toyota рухаєтьсяМетод move не копіюється в car. Він залишається в vehicle, а car отримує доступ до нього через ланцюжок прототипів.
Це дозволяє спільно використовувати методи між багатьма об’єктами:
const userMethods = {
describe() {
return `${this.name}, ${this.role}`;
},
};
const admin = Object.create(userMethods);
admin.name = "Олена";
admin.role = "admin";
const editor = Object.create(userMethods);
editor.name = "Андрій";
editor.role = "editor";
console.log(admin.describe()); // Олена, admin
console.log(editor.describe()); // Андрій, editor
console.log(admin.describe === editor.describe); // trueОбидва об’єкти використовують один і той самий метод із прототипу, але this під час виклику посилається на конкретний об’єкт.
Object.create з дескрипторами властивостейДругий аргумент Object.create дозволяє одразу визначити власні властивості через дескриптори.
const accountPrototype = {
getSummary() {
return `${this.owner}: ${this.balance} грн`;
},
};
const account = Object.create(accountPrototype, {
owner: {
value: "Марія",
writable: false,
enumerable: true,
configurable: false,
},
balance: {
value: 1500,
writable: true,
enumerable: true,
configurable: false,
},
});
console.log(account.getSummary()); // Марія: 1500 грн
account.balance = 2000;
console.log(account.balance); // 2000
account.owner = "Олег";
console.log(account.owner); // МаріяДля властивості можна налаштувати:
value — значення;
writable — чи можна змінювати значення;
enumerable — чи буде властивість видима під час переліку;
configurable — чи можна видалити або переналаштувати властивість.
Якщо властивість створюється звичайним присвоєнням:
const object = {};
object.name = "Тест";то вона зазвичай має такі характеристики:
writable: true;
enumerable: true;
configurable: true.
Дескриптори дозволяють створювати об’єкти з точнішим контролем над поведінкою властивостей.
Object.create не створює копію прототипу. Він лише встановлює зв’язок із ним.
const prototype = {
settings: {
enabled: true,
},
};
const first = Object.create(prototype);
const second = Object.create(prototype);
first.settings.enabled = false;
console.log(second.settings.enabled); // falseОбидва об’єкти звертаються до одного вкладеного об’єкта settings, що лежить у прототипі.
Якщо потрібен незалежний стан для кожного об’єкта, його слід створювати як власну властивість:
const settingsPrototype = {
isEnabled() {
return this.settings.enabled;
},
};
const firstSettings = Object.create(settingsPrototype);
firstSettings.settings = { enabled: true };
const secondSettings = Object.create(settingsPrototype);
secondSettings.settings = { enabled: true };
firstSettings.settings.enabled = false;
console.log(firstSettings.isEnabled()); // false
console.log(secondSettings.isEnabled()); // trueМетоди доцільно зберігати в прототипі, а змінний стан — у самому об’єкті.
thisПрототип визначає, де буде знайдений метод, але не фіксує значення this.
const personPrototype = {
introduce() {
return `Мене звати ${this.name}`;
},
};
const firstPerson = Object.create(personPrototype);
firstPerson.name = "Ірина";
const secondPerson = Object.create(personPrototype);
secondPerson.name = "Тарас";
console.log(firstPerson.introduce()); // Мене звати Ірина
console.log(secondPerson.introduce()); // Мене звати ТарасПід час виклику:
firstPerson.introduce();значення this — це firstPerson, навіть якщо функція фізично зберігається в personPrototype.
Однак якщо метод передати окремо, контекст може втратитися:
const introduce = firstPerson.introduce;
console.log(introduce()); // Може виникнути помилка або this буде undefinedДля суворого режиму та модулів this у такому виклику буде undefined.
Якщо одна й та сама властивість є на кількох рівнях ланцюжка, використовується найближче значення.
const base = {
value: "base",
};
const middle = Object.create(base);
middle.value = "middle";
const object = Object.create(middle);
object.value = "object";
console.log(object.value); // object
delete object.value;
console.log(object.value); // middle
delete middle.value;
console.log(object.value); // baseВидалення власної властивості не видаляє властивість із прототипу. Воно лише дозволяє пошуку перейти на наступний рівень.
Читання властивості та запис властивості мають різну поведінку.
const prototype = {
count: 0,
};
const object = Object.create(prototype);
object.count = 10;
console.log(object.count); // 10
console.log(prototype.count); // 0
console.log(Object.hasOwn(object, "count")); // trueЯкщо властивість, знайдена в прототипі, є звичайною записуваною властивістю, присвоєння зазвичай створює нову власну властивість об’єкта.
Тому object.count = 10 не змінює prototype.count.
Інша ситуація виникає, якщо в прототипі є setter або незаписувана властивість.
const prototype = {
set value(newValue) {
console.log(`Отримано: ${newValue}`);
},
};
const object = Object.create(prototype);
object.value = 42; // Отримано: 42
console.log(Object.hasOwn(object, "value")); // falseПрисвоєння викликає setter із прототипу і не створює власну властивість.
Функції-конструктори мають властивість prototype. Вона не є прототипом самої функції, а використовується як прототип для об’єктів, створених через new.
function User(name) {
this.name = name;
}
User.prototype.describe = function () {
return `Користувач: ${this.name}`;
};
const user = new User("Назар");
console.log(user.describe()); // Користувач: Назар
console.log(Object.getPrototypeOf(user) === User.prototype); // true
console.log(User.prototype.constructor === User); // trueОператор new User("Назар") приблизно виконує такі кроки:
Створює новий об’єкт.
Встановлює його прототипом User.prototype.
Викликає User із this, що посилається на новий об’єкт.
Повертає створений об’єкт, якщо конструктор не повернув інший об’єкт.
Ту саму структуру можна створити явно через Object.create:
function User(name) {
this.name = name;
}
User.prototype.describe = function () {
return `Користувач: ${this.name}`;
};
const user = Object.create(User.prototype);
User.call(user, "Назар");
console.log(user.describe()); // Користувач: НазарУ реальному коді для конструкторів зазвичай використовують new, а Object.create — коли потрібно безпосередньо керувати прототипом.
Синтаксис class також використовує прототипи.
class User {
constructor(name) {
this.name = name;
}
describe() {
return `Користувач: ${this.name}`;
}
}
const user = new User("Софія");
console.log(Object.getPrototypeOf(user) === User.prototype); // true
console.log(user.describe === User.prototype.describe); // trueМетоди класу зберігаються в User.prototype, а не копіюються в кожен екземпляр.
Для наслідування класів JavaScript встановлює ланцюжок прототипів між прототипами:
class User {
describe() {
return "Користувач";
}
}
class Admin extends User {
manage() {
return "Керування системою";
}
}
const admin = new Admin();
console.log(admin.manage()); // Керування системою
console.log(admin.describe()); // Користувач
console.log(Object.getPrototypeOf(admin) === Admin.prototype); // true
console.log(Object.getPrototypeOf(Admin.prototype) === User.prototype); // trueЛанцюжок має вигляд:
admin → Admin.prototype → User.prototype → Object.prototype → nullObject.create(null)Передавши null до Object.create, можна створити об’єкт без прототипу:
const dictionary = Object.create(null);
dictionary.apple = "яблуко";
dictionary.orange = "апельсин";
console.log(dictionary.apple); // яблуко
console.log(Object.getPrototypeOf(dictionary)); // nullТакий об’єкт не успадковує властивості з Object.prototype:
console.log(dictionary.toString); // undefined
console.log(dictionary.hasOwnProperty); // undefinedОб’єкти без прототипу корисні для словників і мап властивостей, коли ключі можуть мати будь-які рядкові значення:
const counts = Object.create(null);
for (const word of ["js", "css", "js"]) {
counts[word] = (counts[word] ?? 0) + 1;
}
console.log(counts.js); // 2
console.log(counts.css); // 1Водночас для складних словників часто зручніше використовувати Map, оскільки він спеціально призначений для зберігання пар ключ-значення.
Object.getPrototypeOfПовертає фактичний прототип об’єкта:
const prototype = {};
const object = Object.create(prototype);
console.log(Object.getPrototypeOf(object) === prototype); // trueЦе рекомендований спосіб читання прототипу.
Object.setPrototypeOfДозволяє змінити прототип уже створеного об’єкта:
const firstPrototype = {
getType() {
return "перший прототип";
},
};
const secondPrototype = {
getType() {
return "другий прототип";
},
};
const object = Object.create(firstPrototype);
console.log(object.getType()); // перший прототип
Object.setPrototypeOf(object, secondPrototype);
console.log(object.getType()); // другий прототипОднак часта зміна прототипів після створення об’єктів може погіршувати оптимізацію JavaScript-рушія. Тому зазвичай краще одразу створювати об’єкт із потрібним прототипом через Object.create.
instanceofОператор instanceof перевіряє, чи присутній prototype правої частини в ланцюжку прототипів лівої частини.
function Animal() {}
const animal = Object.create(Animal.prototype);
console.log(animal instanceof Animal); // trueinstanceof не перевіряє назву типу і не визначає, чи об’єкт був створений безпосередньо конкретним конструктором. Він перевіряє саме ланцюжок прототипів.
function Parent() {}
function Child() {}
Child.prototype = Object.create(Parent.prototype);
const child = new Child();
console.log(child instanceof Child); // true
console.log(child instanceof Parent); // true__proto__ та стандартні APIВластивість __proto__ часто використовують для доступу до прототипу:
const prototype = {};
const object = Object.create(prototype);
console.log(object.__proto__ === prototype); // trueПроте __proto__ — це історичний accessor, який не варто використовувати в новому коді для загального керування прототипами.
Надавайте перевагу:
Object.getPrototypeOf(object) замість object.__proto__;
Object.setPrototypeOf(object, prototype) замість object.__proto__ = prototype;
Object.create(prototype) для створення нового об’єкта.
Окремо слід пам’ятати, що в об’єктному літералі ключ __proto__ має спеціальну поведінку:
const prototype = {
enabled: true,
};
const object = {
__proto__: prototype,
};
console.log(Object.getPrototypeOf(object) === prototype); // trueЦе відрізняється від створення властивості з таким текстовим ім’ям у деяких інших контекстах, тому для явного керування прототипом краще використовувати Object.create.
Розглянемо фабрику, яка створює документи з одним спільним прототипом методів.
const documentPrototype = {
publish() {
if (this.status === "published") {
return "Документ уже опублікований";
}
this.status = "published";
return `Документ "${this.title}" опубліковано`;
},
getInfo() {
return {
title: this.title,
author: this.author,
status: this.status,
};
},
};
function createDocument(title, author) {
const document = Object.create(documentPrototype);
document.title = title;
document.author = author;
document.status = "draft";
return document;
}
const firstDocument = createDocument("Прототипи", "Олена");
const secondDocument = createDocument("Замикання", "Тарас");
console.log(firstDocument.publish());
// Документ "Прототипи" опубліковано
console.log(secondDocument.getInfo());
// { title: 'Замикання', author: 'Тарас', status: 'draft' }
console.log(firstDocument.publish());
// Документ уже опублікований
console.log(firstDocument.publish === secondDocument.publish); // true
console.log(Object.hasOwn(firstDocument, "publish")); // false
console.log(Object.getPrototypeOf(firstDocument) === documentPrototype); // trueУ цьому дизайні:
методи створюються один раз;
кожен документ має власні title, author і status;
зміна стану одного документа не впливає на інший;
прототип явно визначений через Object.create.
Можна побудувати кілька рівнів абстракції без класів.
const entityPrototype = {
describe() {
return `Сутність ${this.id}`;
},
};
const userPrototype = Object.create(entityPrototype);
userPrototype.describe = function () {
return `Користувач ${this.name} (#${this.id})`;
};
userPrototype.login = function () {
this.isOnline = true;
return `${this.name} увійшов у систему`;
};
const adminPrototype = Object.create(userPrototype);
adminPrototype.deleteUser = function (user) {
return `${this.name} видалив користувача ${user.name}`;
};
const admin = Object.create(adminPrototype);
admin.id = 1;
admin.name = "Адміністратор";
admin.isOnline = false;
const user = Object.create(userPrototype);
user.id = 2;
user.name = "Користувач";
user.isOnline = false;
console.log(admin.describe()); // Користувач Адміністратор (#1)
console.log(admin.login()); // Адміністратор увійшов у систему
console.log(admin.deleteUser(user)); // Адміністратор видалив користувача Користувач
console.log(user.describe()); // Користувач Користувач (#2)
console.log(typeof user.deleteUser); // undefinedЛанцюжок для admin:
admin → adminPrototype → userPrototype → entityPrototype → Object.prototype → nullТакий підхід може бути корисним для реалізації об’єктних моделей, але для більшості прикладних задач синтаксис class часто є зрозумілішим. Водночас розуміння прототипів необхідне, щоб правильно пояснювати поведінку класів, instanceof і методів екземплярів.
Оператор in перевіряє всю ланцюжок прототипів:
const prototype = {
inherited: true,
};
const object = Object.create(prototype);
object.own = true;
console.log("own" in object); // true
console.log("inherited" in object); // true
console.log("missing" in object); // falseObject.hasOwn перевіряє лише власні властивості:
console.log(Object.hasOwn(object, "own")); // true
console.log(Object.hasOwn(object, "inherited")); // falseЦя різниця важлива під час перебору:
for (const key in object) {
if (Object.hasOwn(object, key)) {
console.log(`Власна властивість: ${key}`);
}
}Цикл for...in може перебирати перелічувані властивості з прототипу, тому перевірка Object.hasOwn захищає від обробки успадкованих ключів.
const prototype = {
value: 1,
};
const object = Object.create(prototype);
console.log(Object.hasOwn(object, "value")); // falseobject не містить копії value. Пошук лише переходить до прототипу.
const prototype = {
items: [],
};
const first = Object.create(prototype);
const second = Object.create(prototype);
first.items.push("значення");
console.log(second.items); // ["значення"]Масив є спільним для обох об’єктів. Стан потрібно створювати окремо для кожного екземпляра.
prototype функції з прототипом функціїfunction User() {}
console.log(User.prototype); // прототип об'єктів, створених через new User()
console.log(Object.getPrototypeOf(User)); // прототип самої функції UserUser.prototype і Object.getPrototypeOf(User) — різні поняття.
hasOwnProperty без перевіркиconst object = Object.create(null);
object.name = "Тест";
// object.hasOwnProperty("name") спричинить помилку
console.log(Object.hasOwn(object, "name")); // trueДля універсального коду використовуйте Object.hasOwn.
Object.setPrototypeOf(object, anotherPrototype);Це працює, але може ускладнити оптимізацію об’єктів рушієм JavaScript. Якщо прототип відомий заздалегідь, використовуйте:
const object = Object.create(anotherPrototype);const prototype = {
count: 0,
};
const object = Object.create(prototype);
object.count = 5;
console.log(prototype.count); // 0
console.log(object.count); // 5Присвоєння створило власну властивість count в object.
Кожен об’єкт має внутрішній прототип [[Prototype]].
JavaScript шукає властивість спочатку в об’єкті, а потім послідовно в його прототипах.
Послідовність прототипів утворює ланцюжок, який завершується null.
Object.create(proto) створює об’єкт із заданим прототипом.
Object.getPrototypeOf дозволяє прочитати прототип, а Object.setPrototypeOf — змінити його.
Власні властивості можна перевіряти через Object.hasOwn.
Властивості з прототипу не копіюються, тому спільний змінний стан може випадково використовуватися кількома об’єктами.
Методи зручно зберігати в прототипі, а дані конкретного екземпляра — у самому об’єкті.
class, конструктори та instanceof працюють поверх механізму прототипів.
Object.create(null) створює об’єкт без Object.prototype.
Для створення об’єкта з відомим прототипом краще одразу використати Object.create, а не змінювати прототип пізніше.