Пошук уроків, статей та іншого контенту
Дослідите внутрішній прототип об'єкта, властивість prototype і роль ланцюжка прототипів у пошуку властивостей.
У JavaScript кожен звичайний об’єкт має внутрішній зв’язок з іншим об’єктом — своїм прототипом.
Прототип використовується для пошуку властивостей і методів, яких немає безпосередньо в самому об’єкті.
const user = {
name: "Олена"
};
console.log(user.toString());В об’єкті user ми явно не створювали метод toString. Проте JavaScript знаходить його в прототипі об’єкта, а потім викликає.
Якщо властивість не знайдена в самому об’єкті, JavaScript перевіряє його прототип. Якщо властивості немає і там, перевіряється прототип прототипу. Так триває доти, доки пошук не завершиться або не буде досягнуто null.
Стандартний спосіб отримати прототип — метод Object.getPrototypeOf():
const user = {
name: "Олена"
};
const prototype = Object.getPrototypeOf(user);
console.log(prototype === Object.prototype); // trueЗвичайний об’єкт, створений за допомогою літерала {}, зазвичай має прототип Object.prototype.
Об’єкт Object.prototype містить базові методи, доступні більшості об’єктів:
const user = {
name: "Олена"
};
console.log(user.toString());
console.log(user.hasOwnProperty("name"));__proto__У старому коді можна зустріти властивість __proto__:
const user = {
name: "Олена"
};
console.log(user.__proto__ === Object.prototype); // true__proto__ може використовуватися для читання або зміни прототипу, але для цього краще застосовувати стандартні методи:
Object.getPrototypeOf(object) — отримати прототип;
Object.setPrototypeOf(object, prototype) — змінити прототип;
Object.create(prototype) — створити об’єкт із певним прототипом.
Розглянемо пошук властивості крок за кроком:
const user = {
name: "Олена"
};
console.log(user.toString());JavaScript діє приблизно так:
Перевіряє, чи має user власну властивість toString.
Якщо не має, переходить до Object.getPrototypeOf(user).
Перевіряє Object.prototype.
Знаходить там toString.
Повертає знайдене значення.
Такий послідовний пошук називається ланцюжком прототипів.
Ланцюжок можна уявити так:
user → Object.prototype → nullnull означає завершення ланцюжка. У null немає прототипу і властивостей, які можна перевірити.
Для перевірки, чи існує властивість у самому об’єкті або в його прототипах, використовується оператор in:
const user = {
name: "Олена"
};
console.log("name" in user); // true
console.log("toString" in user); // true
console.log("email" in user); // false"toString" in user повертає true, хоча toString не є власною властивістю user. Вона знайдена в прототипі.
Власна властивість — це властивість, яка безпосередньо належить об’єкту, а не успадкована від прототипу.
Для перевірки власної властивості використовується Object.hasOwn():
const user = {
name: "Олена"
};
console.log(Object.hasOwn(user, "name")); // true
console.log(Object.hasOwn(user, "toString")); // falseObject.hasOwn() повертає true лише тоді, коли властивість належить саме об’єкту.
У старішому коді часто використовують:
console.log(user.hasOwnProperty("name")); // trueПроте Object.hasOwn(object, property) є надійнішим варіантом. Наприклад, об’єкт може мати власну властивість з назвою hasOwnProperty, яка перекриє однойменний метод прототипу.
prototypeНе слід плутати внутрішній прототип об’єкта з властивістю prototype.
Кожен звичайний об’єкт має внутрішній прототип. Його можна отримати так:
const user = {};
console.log(Object.getPrototypeOf(user));prototype функціїВластивість prototype зазвичай є у функцій, які можуть використовуватися як конструктори.
function User(name) {
this.name = name;
}
console.log(User.prototype);User.prototype — це звичайний об’єкт. Якщо створити об’єкт за допомогою new User(), цей об’єкт отримає User.prototype як свій внутрішній прототип:
function User(name) {
this.name = name;
}
const user = new User("Олена");
console.log(Object.getPrototypeOf(user) === User.prototype); // trueЛанцюжок прототипів у цьому прикладі виглядає так:
user → User.prototype → Object.prototype → nullМетоди можна додавати до прототипу конструктора:
function User(name) {
this.name = name;
}
User.prototype.sayHello = function () {
return `Привіт, я ${this.name}`;
};
const firstUser = new User("Олена");
const secondUser = new User("Андрій");
console.log(firstUser.sayHello());
console.log(secondUser.sayHello());
console.log(firstUser.sayHello === secondUser.sayHello); // trueМетод створений один раз у User.prototype і використовується всіма об’єктами User.
Це економить пам’ять порівняно зі створенням нової функції для кожного об’єкта:
function User(name) {
this.name = name;
// Функція створюється окремо для кожного екземпляра
this.sayHello = function () {
return `Привіт, я ${this.name}`;
};
}Зазвичай спільні методи конструктора розміщують у його прототипі.
newОператор new виконує кілька дій:
Створює новий порожній об’єкт.
Встановлює його прототипом Constructor.prototype.
Виконує функцію-конструктор із this, що посилається на новий об’єкт.
Повертає створений об’єкт.
Приклад:
function Product(name, price) {
this.name = name;
this.price = price;
}
Product.prototype.getInfo = function () {
return `${this.name}: ${this.price} грн`;
};
const product = new Product("Книга", 300);
console.log(product.getInfo());
console.log(Object.getPrototypeOf(product) === Product.prototype); // trueМетод getInfo не є власною властивістю product, але доступний через ланцюжок прототипів.
console.log(Object.hasOwn(product, "name")); // true
console.log(Object.hasOwn(product, "getInfo")); // false
console.log("getInfo" in product); // trueСинтаксис класів є зручнішим способом роботи з прототипами. Методи класу також зберігаються в прототипі його екземплярів.
class User {
constructor(name) {
this.name = name;
}
sayHello() {
return `Привіт, я ${this.name}`;
}
}
const user = new User("Олена");
console.log(user.sayHello());
console.log(Object.hasOwn(user, "name")); // true
console.log(Object.hasOwn(user, "sayHello")); // false
console.log(Object.getPrototypeOf(user) === User.prototype); // trueМетод sayHello розташований у User.prototype, а не безпосередньо в user.
Схема залишається такою самою:
user → User.prototype → Object.prototype → nullЯкщо властивість є і в об’єкті, і в його прототипі, JavaScript використовує властивість самого об’єкта.
const user = {
name: "Олена"
};
console.log(user.toString()); // метод із Object.prototype
user.toString = function () {
return "Власний метод toString";
};
console.log(user.toString()); // власний метод userВласна властивість перекриває властивість із прототипу. Прототип при цьому не змінюється.
Це називається затіненням властивості.
const parent = {
role: "користувач"
};
const child = Object.create(parent);
console.log(child.role); // "користувач"
child.role = "адміністратор";
console.log(child.role); // "адміністратор"
console.log(parent.role); // "користувач"Спочатку child.role знаходить значення в parent. Після присвоєння в child створюється власна властивість role.
Object.create()Метод Object.create() створює новий об’єкт і встановлює для нього вказаний прототип:
const userMethods = {
sayHello() {
return `Привіт, я ${this.name}`;
}
};
const user = Object.create(userMethods);
user.name = "Олена";
console.log(user.sayHello());
console.log(Object.getPrototypeOf(user) === userMethods); // trueЛанцюжок прототипів:
user → userMethods → Object.prototype → nullМожна створити об’єкт без прототипу, передавши null:
const dictionary = Object.create(null);
dictionary.apple = "яблуко";
console.log(dictionary.apple); // "яблуко"
console.log(Object.getPrototypeOf(dictionary)); // nullТакий об’єкт не має методів із Object.prototype, наприклад toString або hasOwnProperty.
Прототип можна змінити за допомогою Object.setPrototypeOf():
const animal = {
move() {
return "Тварина рухається";
}
};
const dog = {
bark() {
return "Гав-гав";
}
};
Object.setPrototypeOf(dog, animal);
console.log(dog.bark());
console.log(dog.move());Після зміни прототипу ланцюжок має вигляд:
dog → animal → Object.prototype → nullObject.setPrototypeOf() варто використовувати обережно. Часті зміни прототипів можуть погіршити продуктивність JavaScript-двигуна. Якщо прототип відомий під час створення об’єкта, краще використати Object.create() або клас.
const personMethods = {
introduce() {
return `Мене звати ${this.name}`;
}
};
const student = Object.create(personMethods);
student.name = "Марія";
student.course = 2;
console.log(student.introduce());
console.log(Object.hasOwn(student, "name")); // true
console.log(Object.hasOwn(student, "introduce")); // false
console.log("introduce" in student); // true
console.log(Object.getPrototypeOf(student) === personMethods); // true
console.log(Object.getPrototypeOf(personMethods) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // nullПошук student.introduce відбувається так:
У student методу introduce немає.
JavaScript переходить до personMethods.
У personMethods метод знайдений.
Метод викликається з this, що посилається на student.
prototype і внутрішній прототипprototype — це властивість функції або класу, яка використовується для створення прототипу екземплярів.
Внутрішній прототип конкретного об’єкта можна отримати через:
Object.getPrototypeOf(object);const user = {};
console.log("toString" in user); // true
console.log(Object.hasOwn(user, "toString")); // falseОператор in перевіряє весь ланцюжок, а Object.hasOwn() — лише сам об’єкт.
Object.prototypeНе варто без потреби додавати власні властивості до Object.prototype:
// Не рекомендується
Object.prototype.isAdmin = true;Такі властивості стануть доступними майже всім об’єктам і можуть спричинити неочікувану поведінку або конфлікти з кодом.
thisМетод прототипу може використовувати властивості об’єкта, для якого його викликали:
const methods = {
getName() {
return this.name;
}
};
const user = Object.create(methods);
user.name = "Олена";
console.log(user.getName()); // "Олена"this залежить від способу виклику. У виразі user.getName() він посилається на user.
Динамічна зміна прототипів у вже створених об’єктів ускладнює код. Краще заздалегідь обрати підхід:
літерали об’єктів — для простих незалежних об’єктів;
Object.create() — для явного створення прототипного зв’язку;
класи — для створення однотипних об’єктів із методами;
фабричні функції — коли не потрібен клас, але потрібна зручна функція створення.
Об’єкт має внутрішній прототип, через який JavaScript шукає відсутні властивості.
Object.getPrototypeOf() повертає прототип об’єкта.
Ланцюжок прототипів завершується значенням null.
in перевіряє властивість у всьому ланцюжку прототипів.
Object.hasOwn() перевіряє лише власні властивості об’єкта.
prototype — це властивість функції або класу, призначена для прототипу екземплярів.
Методи класів і конструкторів спільно зберігаються в прототипі.
Власна властивість об’єкта перекриває властивість із прототипу.
Object.create() дає змогу створити об’єкт із потрібним прототипом.
Object.setPrototypeOf() змінює прототип, але використовувати його слід обережно.