Skip to content

Files

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Latest commit

2cfc33f · Feb 16, 2025

History

History
280 lines (185 loc) · 20.5 KB

File metadata and controls

280 lines (185 loc) · 20.5 KB

Перетворення об’єктів в примітиви

Що відбувається, коли об’єкти додаються obj1 + obj2, віднімаються obj1 - obj2 або друкуються за допомогою alert(obj)?

JavaScript не дозволяє налаштувати те, як працюють оператори з об’єктами. На відміну від деяких інших мов програмування, таких як Ruby або C++, ми не можемо реалізувати спеціальний об’єктний метод для обробки додавання (або інших операторів).

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

Це важливе обмеження, оскільки результат obj1 + obj2 (або інша математична операція) не може бути іншим об’єктом!

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

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

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

У нас є дві цілі:

  1. Це дозволить нам зрозуміти, що відбувається у випадку помилок в коді, коли така операція відбулася випадково.
  2. Є винятки, де такі операції можливі та доцільні. Наприклад, віднімання або порівняння дати (Date об’єкти). Ми будемо зустрічатися з ними пізніше.

Правила перетворення

У розділі info:type-conversions ми розглянули правила числових, рядкових та логічних перетворень примітивів. Але ми пропустили перетворення об'єктів. Тепер, коли ми знаємо про методи і символи, стає можливим це зробити.

  1. Немає ніякого перетворення в тип Boolean. Всі об'єкти є істинними (true) в булевому контексті, ось так просто. Існують лише числові та рядкові перетворення.
  2. Числове перетворення відбувається коли ми віднімаємо об’єкти або застосовуємо математичні функції. Наприклад, Date об’єкти (розглянуті в розділі info:date) можуть відніматися, і результат date1 - date2 -- це різниця у часі між двома датами.
  3. Що стосується перетворення рядків -- це зазвичай відбувається коли ми виводимо об’єкт (наприклад alert(obj) та в подібних ситуаціях).

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

Тепер давайте перейдемо до технічних деталей, тому що це єдиний спосіб глибоко розкрити тему.

Підказки

Як JavaScript вирішує, яке перетворення застосувати?

Є три варіанти перетворення типів, що відбуваються в різних ситуаціях. Вони називаються "підказками" (англ. hints), і описані в специфікації:

"string" : Для перетворення об’єкта в рядок. Тоді, коли ми виконуємо над об'єктом операцію, яка очікує рядок. Наприклад, alert:

```js
// вивід
alert(obj);

// використання об’єкта як ключа властивості об’єкта
anotherObj[obj] = 123;
```

"number" : Для перетворення об’єкта в число. Тоді, коли ми робимо математичні операції:

```js
// явне перетворення
let num = Number(obj);

// математичні операції (крім бінарного додавання)
let n = +obj; // унарне додавання
let delta = date1 - date2;

// порівняння менше/більше
let greater = user1 > user2;
```

Більшість вбудованих математичних функцій також включають і такі перетворення.

"default" : Трапляється в рідкісних випадках, коли оператор "не впевнений", який тип очікувати.

Наприклад, бінарний плюс `+` може працювати як з рядками (об’єднує їх), так і з цифрами (додає їх), тому обидва випадки -- і рядки, і цифри -- будуть працювати. Отже, якщо бінарний плюс отримує об’єкт як аргумент, він використовує підказку `"default"` для його перетворення.

Також, якщо об’єкт порівнюється за допомогою (`==`) з рядком, числом або символом, то також незрозуміло, яке перетворення слід виконати, тому використовується підказка `"default"`.

```js
// бінарний плюс використовує підказку "default"
let total = obj1 + obj2;

// порівняння obj == число використовує підказку "default"
if (user == 1) { ... };
```

Оператори порівняння більше та менше, такі як `<` `>`, також можуть працювати як з рядками, так і з числами. Проте, вони з історичних причин використовують підказку `"number"` , а не підказку `"default"`.

Але на практиці все трохи простіше.

Всі вбудовані об’єкти, за винятком лише об’єкту Date (про який ми дізнаємося пізніше), реалізовують перетворення з підказкою "default" так само як з підказкою "number". І нам, мабуть, слід робити так само.

Проте важливо знати про всі три підказки і незабаром ми побачимо чому.

Щоб зробити перетворення, JavaScript намагається знайти та викликати три методи об’єкта:

  1. Намагається викликати метод obj[Symbol.toPrimitive](hint) (це такий метод з системним символом Symbol.toPrimitive в якості символьного ключа), якщо в цього об'єкта такий метод існує,
  2. Інакше, якщо підказка має значення "string"
    • намагається викликати obj.toString() або obj.valueOf() -- залежно від того, який з цих методів існує в конкретного об'єкта.
  3. Або, якщо підказка має значення "number" або "default"
    • спробує викликати obj.valueOf() або obj.toString() -- залежно від того, який з цих методів існує в конкретного об'єкта.

Symbol.toPrimitive

Почнемо з першого методу. Є вбудований символ під назвою Symbol.toPrimitive, який слід використовувати в якості ключа методу перетворення, як, наприклад:

obj[Symbol.toPrimitive] = function(hint) {
  // тут йде код, щоб перетворити цей об’єкт в примітив
  // він повинен повернути примітивне значення
  // в hint надходить або "string", або "number", або "default"
};

Якщо метод symbol.toPrimitive в конкретного об'єкта існує, він буде використовуватись для всіх підказок, і ніякі інші методи не потрібні.

Наприклад, тут об’єкт user імплементує цей метод:

let user = {
  name: "Іван",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// демонстрація перетворення:
alert(user); // виведе 'hint: string' і поверне '{name: "Іван"}'
alert(+user); // виведе 'hint: number' і поверне 1000
alert(user + 500); // виведе 'hint: default' і поверне 1500

Як ми бачимо з коду, user стає самоописуючим рядком або грошовою сумою в залежності від способу перетворення. Один єдиний метод [Symbol.toPrimitive] об’єкта user обробляє всі випадки перетворення.

toString/valueOf

Якщо немає Symbol.toPrimitive тоді JavaScript намагається знайти методи toString і valueOf:

  • Якщо підказка має значення "string": JavaScript спершу спробує викликати метод toString, і якщо в цього об'єкта цей метод відсутній або якщо якщо цей метод повертає об'єкт замість примітивного значення, тоді JavaScript викличе valueOf (таким чином, при перетворенні об'єкта в рядок пріоритет має toString).
  • Якщо підказка має значення "number"/"default", JavaScript спершу спробує викликати valueOf, і якщо цей метод відсутній або якщо якщо цей метод повертає об'єкт замість примітивного значення, тоді JavaScript викличе toString (таким чином, для математичних дій пріоритет має valueOf).

Методи toString і valueOf походять з стародавніх часів. Вони не є символами (багато років назад символів в JavaScript не існувало), а скоріше є "звичними" методами, що названі за допомогою рядків. Вони надають альтернативний «старомодний» спосіб реалізації перетворення.

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

В звичайного об'єкта методи toString та valueOf присутні за замовчуванням. І за замовчуванням вони вони працюють наступним чином:

  • Метод toString повертає рядок "[object Object]".
  • Метод valueOf повертає сам об’єкт.

Ось демо:

let user = {name: "Іван"};

alert(user); // [object Object]
alert(user.valueOf() === user); // true

Отже, якщо ми спробуємо використовувати об’єкт як рядок, наприклад в alert та ін., то за замовчуванням ми побачимо [object Object].

Вбудований метод valueOf згадується тут лише заради повноти картини, щоб уникнути непорозумінь. Бо JavaScript цей метод ігнорує. Ігнорує, бо як бачите, цей метод повертає сам об’єкт. Не питайте мене чому так. Просто так склалось з історичних причин. Власне ми можемо уявити ніби об'єкта valueOf не існує.

Давайте самостійно реалізуємо ці методи, щоб налаштувати перетворення.

Наприклад, тут user при перетворенні стає тим самим, що й в прикладі вище, але цього разу використовуючи комбінацію toString і valueOf замість Symbol.toPrimitive:

let user = {
  name: "Іван",
  money: 1000,

  // для hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // для hint="number" чи "default"
  valueOf() {
    return this.money;
  }

};

alert(user); // toString -> {name: "Іван"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

Як бачимо, поведінка така ж, як і в попередньому прикладі з Symbol.toPrimitive.

Часто нам потрібне одне єдине «універсальне» місце для обробки всіх перетворень примітивів. У цьому випадку ми можемо реалізувати тільки toString, ось так:

let user = {
  name: "Іван",

  toString() {
    return this.name;
  }
};

alert(user); // toString -> Іван
alert(user + 500); // toString -> Іван500

Якщо відсутній Symbol.toPrimitive і valueOf, то всі перетворення примітивів буде обробляти toString.

Перетворення може повернути будь-який примітивний тип

Важливо знати, що методи перетворення примітивів не обов’язково повертають примітив "підказаного" типу.

JavaScript не вимагає від toString повернути саме рядок якщо підказка має значення "string". І не вимагає від Symbol.toPrimitive повернути число якщо підказка має значення "number'.

Єдина вимога до методів перетворення примітивів: ці методи повинні повертати саме примітивний тип, а не об’єкт.

З історичних причин, якщо `toString` чи `valueOf` поверне об’єкт, це не призведе до помилки. Таке значення буде просто проігнороване (так само, якби цей метод не існував). Воно не призведе до помилки лише тому, що в давнину JavaScript не вмів нормально видавати помилки.

Але, вимоги до `Symbol.toPrimitive` суворіші. Він *мусить* повернути примітив, інакше JavaScript видасть помилку.

Подальші перетворення

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

Якщо ми передамо об’єкт як аргумент, то відбувається два етапи обчислень:

  1. Об’єкт буде перетворено на примітив (використовуючи правила, описані вище).
  2. Якщо це необхідно для подальших обчислень, то й отриманий примітив також буде перетворено.

Наприклад:

let obj = {
  // За відсутності інших методів, toString обробляє всі перетворення
  toString() {
    return "2";
  }
};

alert(obj * 2); // 4, бо спершу об’єкт перетворено на примітив "2", а тоді множенням отримано число
  1. Множення obj * 2 спочатку перетворює об’єкт в примітив (вийде рядок "2").
  2. Тоді "2" * 2 стане 2 * 2 (рядок перетворюється на число).

Бінарний плюс натомість, в точно такій же ситуації буде з радістю приймати рядки, оскільки він вміє працювати не лише з числами, а й з рядками:

let obj = {
  toString() {
    return "2";
  }
};

alert(obj + 2); // 22 ("2" + 2), перетворення до примітиву повернуло рядок => Конкатенація

Підсумки

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

Існує 3 типи (підказки) для цього перетворення:

  • "string" (для alert та інших операцій, які потребують рядка)
  • "number" (для математичних операцій)
  • "default" (для дуже небагатьох операторів; зазвичай об’єкти реалізують це так само як і "number".)

Специфікація явно описує який оператор використовує яку підказку.

Алгоритм перетворення наступний:

  1. Викликати obj[Symbol.toPrimitive](hint) (якщо цей метод в цього об'єкта існує),
  2. Інакше, якщо підказка має значення "string"
    • спробувати викликати obj.toString() або obj.valueOf(), залежно від того, який з цих методів в цього об'єкта існує.
  3. Інакше, якщо підказка має значення "number" чи "default"
    • спробувати викликати obj.valueOf() або obj.toString(), залежно від того, який з цих методів в цього об'єкта існує.

Виклик усіх цих методів повинен повертати примітив (якщо ці методи визначені).

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