16 липня 2023 р.

Посилальний Тип

Поглиблений функціонал мови

Ця стаття охоплює складну тему, щоб краще зрозуміти певні крайні випадки.

Це не є важливим. Багато досвідчених розробників живуть добре, не знаючи цього. Прочитайте, якщо ви хочете знати, як певні речі працюють під капотом.

Динамічно оцінений виклик методу може втратити this.

Наприклад:

let user = {
  name: "Іван",
  hi() { alert(this.name); },
  bye() { alert("До побачення"); }
};

user.hi(); // працює

// тепер викличмо user.hi або user.bye залежно від назви
(user.name == "John" ? user.hi : user.bye)(); // Помилка!

На останньому рядку є умовний оператор, який вибирає або user.hi або user.bye. У цьому випадку результат – user.hi.

Потім метод негайно викликається за допомогою дужок (). Але це працює неправильно!

Як ви бачите, виклик призводить до помилки, тому що значення "this" всередині виклику стає undefined.

Це працює (метод об’єкта через крапку):

user.hi();

Це ні (динамічно оцінений метод):

(user.name == "John" ? user.hi : user.bye)(); // Помилка!

Чому? Якщо ми хочемо зрозуміти, чому це трапляється, загляньмо під капот, як працює виклик obj.method().

Пояснення Посилального Типу

Дивлячись уважно, ми можемо помітити дві операції в інструкції obj.method():

  1. По-перше, крапка '.' витягує властивість obj.method.
  2. Потім дужки () виконуйте її.

Отже, як інформація про this передається з першої частини до другої?

Якщо ми поставимо ці операції на окремі рядки, то this напевно буде втрачено:

let user = {
  name: "John",
  hi() { alert(this.name); }
};

// розділимо отримання та виклик методу на два рядки
let hi = user.hi;
hi(); // Помилка, тому що this -- це undefined

Тут hi = user.hi поміщає функцію в змінну, а потім на останньому рядку, ця змінна повністю автономна, і тому не має this.

Щоб зробити виклик user.hi() робочим, JavaScript використовує трюк – крапка '.' повертає не функцію, а значення спеціального посилальний типу.

Посилальний тип – це “тип специфікації”. Ми не можемо явно використовувати його, але він використовується всередині мови.

Значення посилального типу – це комбінація трьох значення (base, name, strict), де:

  • base – це об’єкт.
  • name – це назва властивості.
  • strict – це true якщо діє use strict.

Результат доступу до властивості user.hi є не функцією, а значенням посилального типу. Для user.hi у суворому режимі це:

// Значення посилального типу
(user, "hi", true)

Коли дужки () викликаються з посилальним типом, вони отримують повну інформацію про об’єкт та його метод, і можуть встановити правильний this (user у даному випадку).

Посилальний тип – це особливий “посередницький” внутрішній тип, який використовується з метою передачі інформації від крапки . до дужок виклику ().

Будь-яка інша операція, наприклад присвоєння hi = user.hi в цілому відкидає посилальний тип та приймає значення user.hi (функції) і передає його. Отже, будь-яка подальша операція “втрачає” this.

Отже, як результат, значення this передається правильно тільки тоді, якщо функція викликається безпосередньо за допомогою крапки obj.method() або синтаксису квадратних дужок obj['method']() (вони роблять одне й те ж саме). Існують різні способи розв’язання цієї проблеми, як func.bind().

Підсумки

Посилальний тип – це внутрішній тип мови.

Читання властивості, наприклад, крапкою . в obj.method() повертає не саме значення властивості, але спеціальне значення “посилального типу”, яке зберігає як значення властивості, так і об’єкт, з якою він був взятий.

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

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

Вся ця механіка прихована від наших очей. Це лише важливо в тонких випадках, наприклад, коли метод отримується динамічно з об’єкта, використовуючи вираз.

Завдання

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

Який результат цього коду?

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

(user.go)()

П.С. Тут є пастка :)

Помилка!

Спробуйте:

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

(user.go)() // помилка!

Повідомлення про помилку в більшості браузерів не дає нам велику кількість підказок про те, що пішло не так.

Помилка з’являється, оскільки крапка з комою відсутня після user = {...}.

JavaScript не вставляє автоматично крапку з комою перед дужками (user.go)() тому, що він читає код, як:

let user = { go:... }(user.go)()

Тоді ми також можемо побачити, що такий спільний вираз синтаксично є викликом об’єкта { go: ... } як функція з аргументом (user.go). І це також відбувається на тій же лінії, з let user, тому, оскільки об’єкт user ще не визначений, виникає помилка.

Якщо ми вставляємо крапку з комою, все добре:

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

(user.go)() // Іван

Зверніть увагу, що дужки навколо (user.go) нічого не роблять тут. Зазвичай вони встановлюють порядок операцій, але тут крапка спрацьовує спочатку в будь-якому випадку, тому немає ефекту. Тільки грає роль лише крапка з комою вкінці.

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

У коді нижче ми маємо намір викликати метод obj.go() 4 рази поспіль.

Але виклики (1) та (2) працюють по-різному ніж (3) і (4). Чому?

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

Ось пояснення.

  1. Це звичайний виклик методу об’єкта.

  2. Те ж саме, дужки не змінюють порядок операцій тут, крапка спрацьовує спочатку в будь-якому випадку.

  3. Тут у нас є більш складний виклик (вираз)(). Виклик працює так, ніби він був розділений на два рядки:

    f = obj.go; // обчислити вираз
    f();        // викликати те, що ми маємо

    Тут f() виконується як функція, без this.

  4. Ми маємо вираз подібну річ, як в (3), ліворуч від дужок ().

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

Будь-яка операція на цьому, крім виклику методу (наприклад, присвоєння = або ||) перетворює його в звичайне значення, яке не носить інформацію, яка дозволяє встановити this.

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

Коментарі

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