Властивість "prototype"
широко використовується ядром самого JavaScript. Її використовують всі вбудовані функції конструктора.
Спочатку ми розглянемо деталі, а потім розберемося як додати нових можливостей вбудованим об’єктам.
Object.prototype
Скажімо, ми виводимо порожній об’єкт:
let obj = {};
alert( obj ); // "[object Object]" ?
Де код, який генерує рядок "[object Object]"
? Це вбудований метод toString
, але де це? obj
порожній!
…але коротке позначення obj = {}
– це те саме, що obj = new Object()
, де Object
є вбудованою функцією-конструктором об’єкта, з власним prototype
, що посилається на величезний об’єкт з toString
та іншими методами.
Ось що відбувається:
Коли викликається new Object()
(або створюється літеральний об’єкт {...}
), у властивість [[Prototype]]
встановлюється Object.prototype
згідно з правилом, яке ми обговорювали у попередньому розділі:
Отже, коли викликається obj.toString()
, цей метод береться з Object.prototype
.
Ми можемо перевірити це так:
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
Будь ласка, зверніть увагу, що більше немає [[Prototype]]
у ланцюгу викликів над Object.prototype
:
alert(Object.prototype.__proto__); // null
Інші вбудовані прототипи
Інші вбудовані об’єкти, такі як Array
, Date
, Function
та інші також зберігають методи у прототипах.
Наприклад, коли ми створюємо масив [1, 2, 3]
, внутрішньо використовується конструктор new Array()
. Таким чином Array.prototype
стає його прототипом і надає свої методи. Це дуже ефективно.
За специфікацією, на вершині ієрархії всі вбудовані прототипи мають Object.prototype
. Ось чому деякі люди кажуть, що “все успадковується від об’єктів”.
Ось загальна картина (для 3 вбудованих об’єктів):
Перевірмо прототипи вручну:
let arr = [1, 2, 3];
// arr успадковується від Array.prototype?
alert( arr.__proto__ === Array.prototype ); // true
// потім від Object.prototype?
alert( arr.__proto__.__proto__ === Object.prototype ); // true
// і null на вершині.
alert( arr.__proto__.__proto__.__proto__ ); // null
Деякі методи в прототипах можуть перекриватися, наприклад, Array.prototype
має свій власний toString
, який перелічує елементи розділені комами:
let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- результат Array.prototype.toString
Як ми бачили раніше, Object.prototype
також має toString
, але Array.prototype
ближче по ланцюгу прототипів, тому використовується варіант масиву.
Інструменти браузера, такі як консоль розробника Chrome, також показують наслідування (можливо, доведеться використовувати console.dir
для вбудованих об’єктів):
Інші вбудовані об’єкти також працюють так само. Навіть функції – вони є об’єктами вбудованого конструктора Function
, а їхні методи (call
/apply
та інші) беруться з Function.prototype
. Функції мають власні toString
.
function f() {}
alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true, успадковується від об’єктів
Примітиви
Найскладніша річ відбувається з рядками, числами та бульовими значеннями.
Як ми пам’ятаємо, вони не є об’єктами. Але якщо ми спробуємо отримати доступ до їх властивостей, створюються тимчасові об’єкти-обгортки, використовуючи вбудовані конструктори String
, Number
та Boolean
. Вони забезпечують методи, а після цього зникають.
Ці об’єкти створюються приховано від нас, і більшість рушіїв оптимізують їх, але специфікація описує це саме таким чином. Методи цих об’єктів також знаходяться у прототипах, доступних як String.prototype
, Number.prototype
та Boolean.prototype
.
null
та undefined
не мають жодних об’єктів-обгортокСпеціальні значення null
та undefined
стоять окремо. Вони не мають об’єктів-обгорток, тому для них недоступні методи та властивості. І вони також не мають відповідних прототипів.
Зміна вбудованих прототипів
Вбудовані прототипи можуть бути змінені. Наприклад, якщо додати метод до String.prototype
, він стає доступним для всіх рядків:
String.prototype.show = function() {
alert(this);
};
"БУМ!".show(); // БУМ!
Під час розробки ми можемо мати ідеї для нових вбудованих методів, які ми хотіли б мати, і ми можемо мати спокусу додати їх до вбудованих прототипів. Але це, як правило, погана ідея.
Прототипи є глобальними, тому так можна легко отримати конфлікт. Якщо дві бібліотеки додають метод String.prototype.show
, то один з них буде перезаписаний іншим.
Отже, загалом, модифікація вбудованого прототипу вважається поганою ідеєю.
У сучасному програмуванні існує лише один випадок, коли затверджується модифікація рідних прототипів. Це створення поліфілів.
Поліфіл – це термін, що означає заміну методу, який існує в специфікації JavaScript, але ще не підтримується певним рушієм JavaScript.
Тоді ми можемо реалізувати його вручну та заповнити вбудований прототип ним.
Наприклад:
if (!String.prototype.repeat) { // якщо такого методу немає
// додайте його до прототипу
String.prototype.repeat = function(n) {
// повторіть рядок n разів
// насправді, код повинен бути трохи складнішим
// (повний алгоритм можна знайти в специфікації)
// але навіть недосконалий поліфіл часто вважається прийнятним для використання
return new Array(n + 1).join(this);
};
}
alert( "Ла".repeat(3) ); // ЛаЛаЛа
Запозичення з прототипів
У розділі Декоратори та переадресація виклику, call/apply ми говорили про запозиченння методів.
Це коли ми приймаємо метод від одного об’єкта і копіюємо його в інший.
Деякі методи вбудованих прототипів часто позичаються.
Наприклад, якщо ми створимо об’єкт, подібний до масиву, ми можемо скопіювати деякі методи Array
до нього.
Приклад:
let obj = {
0: "Привіт",
1: "світ!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Привіт,світ!
Це працює, оскільки для внутрішнього алгоритму вбудованого методу join
важливі тільки правильні індекси та властивість length
. Це метод не перевіряє, чи дійсно об’єкт є масивом. Багато вбудованих методів працюють подібним чином.
Ще одна можливість полягає в тому, щоб успадкуватися від масиву, встановлюючи obj.__ proto__
як Array.prototype
, таким чином всі методи Array
автоматично будуть доступні в obj
.
Але це неможливо, якщо obj
вже успадковується від іншого об’єкта. Пам’ятайте, що ми не можемо успадковуватись від декількох об’єктів одночасно.
Запозичення методів є гнучкими, воно дозволяє змішувати функціональність різних об’єктів, якщо це необхідно.
Підсумки
- Всі вбудовані об’єкти слідують однаковому шаблону:
- Методи зберігаються у прототипі (
Array.prototype
,Object.prototype
,Date.prototype
та ін.). - Сам об’єкт зберігає лише дані (елементи масиву, властивості об’єкта, дату).
- Методи зберігаються у прототипі (
- Примітиви також зберігають методи у прототипах об’єктів-обгорток:
Number.prototype
,String.prototype
andBoolean.prototype
. Тількиundefined
таnull
не мають об’єктів-обгорток. - Вбудовані прототипи можуть бути змінені або доповнені новими методами. Але їх не рекомендується змінювати. Єдиний допустимий випадок, мабуть, коли ми додаємо якийсь новий стандарт, котрий ще не підтримується рушієм JavaScript