10 жовтня 2024 р.

Вбудовані прототипи

Властивість "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 and Boolean.prototype. Тільки undefined та null не мають об’єктів-обгорток.
  • Вбудовані прототипи можуть бути змінені або доповнені новими методами. Але їх не рекомендується змінювати. Єдиний допустимий випадок, мабуть, коли ми додаємо якийсь новий стандарт, котрий ще не підтримується рушієм JavaScript

Завдання

важливість: 5

Додайте до прототипу всіх функцій метод defer(ms), що запускає функцію після ms мілісекунд.

Після цього цей код повинен працювати:

function f() {
  alert("Привіт!");
}

f.defer(1000); // показує "Привіт!" через 1 секунду
Function.prototype.defer = function(ms) {
  setTimeout(this, ms);
};

function f() {
  alert("Привіт!");
}

f.defer(1000); // показує "Привіт!" через 1 сек
важливість: 4

Додати до прототипу всіх функцій метод defer(ms), що повертає обгортку, що затримує виклик на ms мілісекунд.

Ось приклад того, як метод повинен працювати:

function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // показує 3 після 1 секунди

Будь ласка, зверніть увагу, що аргументи повинні бути передані до вихідної функції.

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

// перевіримо це
function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // показує 3 після 1 сек

Будь ласка, зверніть увагу: ми використовуємо this в f.apply, щоб наше декорування працювало для методів об’єкта.

Отже, якщо функція-обгортка викликається як метод об’єкта, то this передається до оригінального методу f.

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

let user = {
  name: "Іван",
  sayHi() {
    alert(this.name);
  }
}

user.sayHi = user.sayHi.defer(1000);

user.sayHi();
Навчальна карта