22 лютого 2024 р.

F.prototype

Як відомо, ми можемо створювати нові об’єкти за допомогою функції-конструктора, ось так new F().

Якщо розглядати F.prototype як властивість об’єкта, то оператор new автоматично створює приховану властивість [[Prototype]] для новоствореного об’єкта.

Будь ласка, зверніть увагу:

JavaScript мала можливість успадкування властивостей через прототипи ще від самого початку її створення. Це було одним з головних особливостей мови.

Але в ті часи, не було прямого доступу до прототипів. Тільки одна річ, яка працювала надійно, була властивість функції-конструктора "prototype" , яка і описана в цій главі. Ось чому ще існує багато коду де цю властивість використовують.

Зауважте, що F.prototype тут означає звичайну властивість, яку назвали "prototype" в об’єкті F. Це звучить дуже співзвучно з терміном “prototype”, але тут це означає всього лиш звичайне ім’я властивості.

Ось приклад:

let animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

Вираз Rabbit.prototype = animal дослівно означає наступне: “коли new Rabbit створено, його властивість [[Prototype]] посилається на об’єкт animal”.

Ось кінцева картинка:

На картинці, "prototype" що показана горизонтальною стрілкою, це звичайна властивість, а [[Prototype]]показана вертикальною, що означає rabbit успадковує властивості від свого прототипа animal.

F.prototype використовується тільки у разі використання функції-конструктора new F

Властивість F.prototype використовується коли буде викликано new F, і створює властивість [[Prototype]] для нового об’єкта.

Якщо властивість F.prototype після створення змінюється (F.prototype = <another object>), тоді і новий об’єкт який створюється функцією-конструктором new F буде мати посилання [[Prototype]] на інший об’єкт, а в раніше створених об’єктах є свої прототипи які були визначені ще при їх створенні.

Типове значення F.prototype, властивості конструктора

Кожна функція має властивість "prototype" навіть якщо ми цю властивість самі не прописуємо. Тобто вона існує за замовчуванням, або ця властивість є типовою.

В свою чергу, така типова властивість "prototype" представляє собою об’єкт, який має єдину властивість з назвою constructor, що зворотньо посилається на назву самої функції-конструктора.

Ось як тут:

function Rabbit() {}

/* властивість створена за замовчуванням
Rabbit.prototype = { constructor: Rabbit };
*/

Можемо перевірити це:

function Rabbit() {}
// за замовчуванням:
// Rabbit.prototype = { constructor: Rabbit }

alert( Rabbit.prototype.constructor == Rabbit ); // true

Отже, якщо ми нічого не робимо з властивістю constructor то вона є доступна для всіх об’єктів rabbits через [[Prototype]]:

function Rabbit() {}
// за замовчуванням:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // успадковує від {constructor: Rabbit}

alert(rabbit.constructor == Rabbit); // true (від прототипу)

Ми можемо використовувати властивість constructor для створення нових об’єктів використовуючи той самий конструктор, який вже існує.

Як тут:

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");

let rabbit2 = new rabbit.constructor("Black Rabbit");

Це дуже практично у випадку наявності об’єкта, але не знаємо за допомогою якого саме конструктора той об’єкт був створений (для прикладу який був імпортований з якоїсь бібліотеки), а нам потрібно створити новий об’єкт по типу того, що вже існує.

Але самим важливим моментом щодо "constructor" є те, що…

…Сама мова JavaScript не забезпечує правильного значення "constructor".

Так, воно існує за замовчуванням у властивостях "prototype" для функцій, але це все, що є. Те, що стається з "constructor" пізніше, цілковито залежить від нас самих.

А саме, якщо ми замінимо типове значення на якесь інше, тоді в ньому не буде ніякого "constructor".

Наприклад:

function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};

let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false

Отже, щоб мати правильний "constructor" ми можемо чи додавати, чи видаляти властивості у дефолтному "prototype" замість того, щоб її цілковито заміняти:

function Rabbit() {}

// Тут ми не заміняємо цілковито властивість Rabbit.prototype
// а просто додаємо до неї
Rabbit.prototype.jumps = true
// а тому дефолтне Rabbit.prototype.constructor зберігається

чи по іншому, відновлюємо constructor вручну:

Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

// і тепер, constructor також правильний, тому що ми додали його вручну

Підсумок

В цьому розділі було коротко описано шлях для встановлення прихованої властивості [[Prototype]] об’єктів, які були створені за допомогою функції-конструктора. Пізніше, буде надано більше прикладів коду, які покладаються на ці властивості.

Все досить просто, тільки треба додати кілька деталей щоб усе було зрозуміло:

  • Властивість об’єкта F.prototype (ні в якому разі не [[Prototype]]) встановлює приховану властивість [[Prototype]] нового об’єкта, тільки тоді, коли буде викликана через new F().
  • Значення властивості F.prototype може бути, або посиланням на об’єкт, або null: інші значення не працюють.
  • Тільки властивість "prototype" має такий спеціальний ефект: може встановлюватись в конструкторі та може викликатись через оператор new.

У звичайних об’єктах властивість prototype не є чимось спеціальним:

let user = {
  name: "John",
  prototype: "Bla-bla" // немає ніякої магії
};

За замовчуванням, усі функції мають F.prototype = { constructor: F }, і ми можемо отримати конструктор об’єкта через його властивість "constructor".

Завдання

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

В коді, що показаний нижче, ми створюємо об’єкт new Rabbit і потім міняємо його прототип.

На початку, маємо цей код:

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // true
  1. Ми додали ще рядок коду (виділений). Що покаже тепер alert?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = {};
    
    alert( rabbit.eats ); // ?
  2. …А якщо маємо такий код (з видаленим рядком коду)?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false;
    
    alert( rabbit.eats ); // ?
  3. А якщо так (також з видаленим рядком коду)?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete rabbit.eats;
    
    alert( rabbit.eats ); // ?
  4. І останній варіант:

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete Rabbit.prototype.eats;
    
    alert( rabbit.eats ); // ?

Відповідь:

  1. true.

    Визначення Rabbit.prototype встановлює властивість [[Prototype]] для новоствореного об’єкта, але це жодним чином не впливає на вже існуючий об’єкт.

  2. false.

    Об’єкти призначаються шляхом посилання на них. Об’єкт з властивістю Rabbit.prototype не дублюється, це є той самий об’єкт на який посилаються як через Rabbit.prototype так і через властивість [[Prototype]] об’єкта rabbit.

    А отже, коли ми змінюємо контент такого об’єкта через посилання, такі зміни стають видимі і через інші посилання.

  3. true.

    Усі delete операції застосовуються безпосередньо на самому об’єкті. Тут delete rabbit.eats намагається видалити властивість eats з об’єкта rabbit, але такої властивості немає. А тому така операція не має ніякого ефекту.

  4. undefined.

    Властивість eats видалена з прототипу, вона більше не існує.

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

Уявіть собі, у нас є довільний об’єкт obj, який створений функцією-конструктором – ми не знаємо яким саме конструктором, але потрібно створити новий об’єкт використовуючи той самий конструктор.

Чи можна створити новий об’єкт ось так?

let obj2 = new obj.constructor();

Покажіть приклад функції-конструктора для обєкта obj, який забезпечить правильну роботу такого коду, а також приклад, який, при такому коді, працює неправельно.

Так, ми можемо використовувати такий підхід якщо ми впевнені, що властивість "constructor" має правильне значення.

Для прикладу, якщо ми не чіпаємо властивість за замовчуванням "prototype", тоді цей код буде працювати правильно:

function User(name) {
  this.name = name;
}

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // Pete (працює!)

Код працює, тому що User.prototype.constructor == User.

…Але якщо хтось, якщо можна так виразитись, перезапише User.prototype і забуде додати властивість constructor в посиланні властивості об’єкта User, тоді цей код не буде працювати правильно.

Наприклад:

function User(name) {
  this.name = name;
}
User.prototype = {}; // (*)

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // undefined

Чому user2.name є undefined?

Ось тут пояснення як new user.constructor('Pete') працює:

  1. Спочатку, здійснюється пошук у властивості constructor об’єкта user. Нічого не знаходять.
  2. Потім переключаються на ланцюжок прототипу. Прототипом для об’єкта user є User.prototype, і він також не має властивості constructor (тому що ми “забули” визначити його правильним чином!).
  3. Йдучи далі по ланцюжку прототипу, визначаємо, що User.prototype є простий об’єкт, його прототипом є вбудований глобальний Object.prototype.
  4. Врешті, для вбудованого Object.prototype, є вбудований конструктор глобального об’єкта Object.prototype.constructor == Object от він і використовується.

Таким чином, в кінці, ми отримуємо let user2 = new Object('Pete').

Ймовірно, це не те, що нам потрібно. Ми би хотіли стоврити new User, а не new Object. Це і є наслідки пропуску властивості constructor.

(на випадок, якщо вас зацікавить, виклик new Object(...) перетворює його аргументи на об’єкт. Це в теорії, але на практиці ніхто не викликає new Object з аргументами; і загалом, не використовується узагалі new Object для створення нових об’єктів).

Навчальна карта