29 березня 2025 р.

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("Білий кролик"); //  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 = <інший об'єкт>), то нові об’єкти, створені з допомогою new F, будуть мати в якості [[Prototype]] інший об’єкт, а вже існуючі об’єкти будуть посилатись на старий.

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

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

За замовчуванням “prototype” є об’єктом з однією єдиною властивістю constructor, яка посилається на саму ж функцію.

Ось як тут:

function Rabbit() {}

/* значення prototype, яке призначається за замовченням
Rabbit.prototype = { constructor: Rabbit };
*/

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

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

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

Ну й логічно, якщо ми самі нічого з властивістю constructor не робимо, то вона є доступна для всіх об’єктів rabbit через [[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("Білий кролик");

let rabbit2 = new rabbit.constructor("Чорний кролик");

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

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

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

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

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

Наприклад:

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

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

Отже, щоб мати правильний "constructor", замість цілком заміняти дефолтний prototype якимось нашим іншим об’єктом, ми можемо просто додавати або видаляти властивості до уже наявного дефолтного "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 і потім змінюємо її prototype.

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

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('Богдан');
let user2 = new user.constructor('Данило');

alert( user2.name ); // Данило (працює!)

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

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

Наприклад:

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

let user = new User('Богдан');
let user2 = new user.constructor('Данило');

alert( user2.name ); // undefined

Чому user2.name є undefined?

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

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

Таким чином, в кінці кінців, ми отримуємо те ж саме, якби написали let user2 = new Object('Данило').

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

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

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

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)