Символи та їх властивості

Символи (symbols) є примітивним типом, який вводиться у ECMAScript 6 на додачу до існуючих: рядків, чисел, булевих значень, null та undefined. Символи є способом створення приватних членів об’єктів (private object members) — можливість, на яку JavaScript–розробники чекали протягом довгого часу. До символів будь–яка властивість з рядком у якості імені була легкодоступною, незалежно від безвісності (obscurity) імені, а можливість «приватних імен» має на увазі можливість створювати нерядкові імена властивостей. Тобто, тоді звичайні підходи для виявлення цих приватних імен не мали б працювати.

Пропозиція приватних імен, в кінцевому випадку, вилилась у символи в ECMAScript 6, а ця глава навчить вас як працювати з символами ефективно. Тоді як деталі реалізації залишились тими ж самими (вони додають нерядкові значення для імен властивостей), приватності, як мету, було відкинуто. Замість цього, символьні властивості зберігаються окремо від інших властивостей об’єкту.

Створення символів

Символи є унікальними серед всіх примітивів у JavaScript тим, що вони не мають літеральної форми, як от true для булевого типу, або 42 для числа. Ви можете створити символ з допомогою глобальної функції Symbol, як у цьому прикладі:

let firstName = Symbol();
let person = {};

person[firstName] = "Nicholas";
console.log(person[firstName]);     // "Nicholas"

Тут символ firstName створюється та використовується для присвоєння нової властивості об’єкту person. Цей символ повинен використовуватись щоразу, коли ви хочете отримати доступ до цієї властивості. Називати символьну змінну зрозумілими ім’ям — хороша ідея, адже ви з легкістю зможете зрозуміти, яку функцію виконує цей символ.

W> Оскільки символи є примітивними значеннями, виклик new Symbol() кине помилку. Ви також можете створити символ через new Object(yourSymbol), проте незрозуміло, коли така можливість може бути корисною.

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

let firstName = Symbol("first name");
let person = {};

person[firstName] = "Nicholas";

console.log("first name" in person);        // false
console.log(person[firstName]);             // "Nicholas"
console.log(firstName);                     // "Symbol(first name)"

Опис символу зберігається у внутрішній властивості [[Description]]. Ця властивість читається щоразу, коли метод toString() символу викликається явним або неявним способом. У цьому прикладі, метод toString() символу firstName викликається неявно через console.log(), тому опис виводиться у лог. Іншого способу отримати доступ до [[Description]] з коду немає. Я раджу завжди передавати опис символу, щоб полегшити роботу з символами.

A> Ідентифікація символів

A> Оскільки символи є примітивними значеннями, ви можете використовувати оператор typeof, щоб визначати чи змінна містить символ. ECMAScript 6 розширює typeof так, щоб він повертав "symbol" при використанні його з символом. Наприклад:

let symbol = Symbol("test symbol");
console.log(typeof symbol);         // "symbol"

A> Хоча й існують інші непрямі способи визначення, чи змінна є символом, проте оператор typeof є найбільш прийнятним та точним способом.

Використання символів

Ви можете використовувати символи всюди, де ви можете використовувати обчислювані імена властивостей. Ви вже бачили запис використання символів з квадратними дужками у цій главі, проте ви можете використовувати їх і в літералах об’єктів з обчислюваними іменами властивостей, а також з викликами Object.defineProperty() та Object.defineProperties(), як от:

let firstName = Symbol("first name");

// використання об’єктного літералу з обчислюваною властивістю
let person = {
    [firstName]: "Nicholas"
};

// робимо властивість доступною лише для читання
Object.defineProperty(person, firstName, { writable: false });

let lastName = Symbol("last name");

Object.defineProperties(person, {
    [lastName]: {
        value: "Zakas",
        writable: false
    }
});

console.log(person[firstName]);     // "Nicholas"
console.log(person[lastName]);      // "Zakas"

Цей приклад використовує об’єктний літерал з обчислюваною властивістю для створення символьної властивості firstName. На відміну від обчислюваних властивостей з несимвольними іменами, створена властивість є неперелічуваною (nonenumerable). Наступний рядок робить цю властивість доступною лише для читання. Потім, методом Object.defineProperties(), створюється доступна лише для читання символьна властивість lastName. Тут ще раз використовується об’єктний літерал з обчислюваною властивістю, проте цього разу як частина виклику Object.defineProperties().

Хоча символи й можуть використовуватись будь–де в якості імен для обчислюваних властивостей, вам потрібно мати систему поширення цих символів між різними частинами вашого коду, щоб використовувати їх ефективно.

Поширення символів

У певний момент ви можете зрозуміти, що різні частини вашого коду використовують одні і ті ж самі символи. Наприклад, припустимо, ви маєте два різних типи об’єктів у вашому додатку, що повинні використовувати символьні властивості для представлення унікальних ідентифікаторів. Слідкувати за символами у всіх файлах або у проектах з великою кодовою базою буде складно та призводитиме до помилок. Ось чому ECMAScript 6 вводить глобальний реєстр символів, до якого ви маєте доступ у будь–який момент.

Коли ви хочете створити символ, яким треба поділитись, використовуйте метод Symbol.for() замість виклику методу Symbol(). Метод Symbol.for() приймає єдиний параметр, який є рядковим ідентифікатором символу, який ви хочете створити. Цей параметр також використовується в якості опису символу. Наприклад:

let uid = Symbol.for("uid");
let object = {};

object[uid] = "12345";

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"

Спочатку метод Symbol.for() шукає символ з ключем "uid", та чи існує у глобальному реєстрі символів. Якщо він існує, тоді метод повертає існуючий символ. Якщо такого символу не існує, тоді створюється новий символ з заданим ключем, який реєструється у глобальному реєстрі символів. Після цього повертається цей новий символ. Це означає, що наступні виклики Symbol.for() з однаковим ключем повернуть один і той же символ, ось так:

let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};

console.log(object[uid]);       // "12345"
console.log(uid);               // "Symbol(uid)"

let uid2 = Symbol.for("uid");

console.log(uid === uid2);      // true
console.log(object[uid2]);      // "12345"
console.log(uid2);              // "Symbol(uid)"

У цьому прикладі, uid та uid2 містять один і той же символ, тому вони можуть використовуватись однаково. Перший виклик Symbol.for() створює символ, а другий виклик дістає символ з глобального реєстру символів.

Іншим унікальним аспектом поширених символів є можливість отримати ключ, що асоційований з символом у глобальному реєстрі символів з допомогою виклику методу Symbol.keyFor(). Наприклад:

let uid = Symbol.for("uid");
console.log(Symbol.keyFor(uid));    // "uid"

let uid2 = Symbol.for("uid");
console.log(Symbol.keyFor(uid2));   // "uid"

let uid3 = Symbol("uid");
console.log(Symbol.keyFor(uid3));   // undefined

Зауважте, що як uid, так і uid2 повертають ключ "uid". Символу uid3 немає у глобальному реєстрі, він не має жодного ключа асоційованого з собою, і тому Symbol.keyFor() повертає undefined.

W> Глобальний реєстр символів є спільним середовищем, так само як і глобальна область видимості. Це означає, що ви не можете зробити висновок про те, що є, а чого ще немає у цьому середовищі. Використовуйте простори імен для ключів символів, щоб уникнути можливих конфліктів імен при використанні сторонніх компонентів. Наприклад, код jQuery може використовувати префікс "jquery." для всіх ключів, наприклад "jquery.element" або щось схоже.

Приведення символів

Приведення типів є значною частиною JavaScript. Можливість мови приводити один тип даних до іншого є дуже гнучкою можливістю. Символи, однак, є недостатньо гнучкими при приведенні, оскільки інші типи не мають логічних еквівалентів символів. Зокрема, символи не можуть бути приведеними до рядків або чисел, тому що вони не можуть використовуватись в якості властивостей, що поводилися би як символи.

Приклади у цій главі використовували console.log() для виводу символів, і це працює тому, що console.log() викликає String() для цього символу, щоб створити інформативний вивід. Ви можете використовувати String() безпосередньо, щоб отримати такий самий результат, ось так:

let uid = Symbol.for("uid"),
    desc = String(uid);

console.log(desc);              // "Symbol(uid)"

Функція String() викликає uid.toString(), що повертає рядок опису цього символу. Однак, якщо ви спробуєте сконкатинувати з рядком, кинеться помилка:

let uid = Symbol.for("uid"),
    desc = uid + "";            // помилка!

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

Так само, ви не можете привести символ до числа. Всі математичні оператори спричинять помилку при застосуванні їх до символу. Наприклад:

let uid = Symbol.for("uid"),
    sum = uid / 1;            // помилка!

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

Отримання символьних властивостей

Методи Object.keys() та Object.getOwnPropertyNames() можуть отримувати імена всіх властивостей об'єкту. Перший метод повертає імена усіх перелічуваних властивостей, а другий повертає усі властивості незалежно від їх перелічуваності. Жоден з цих методів не повертає символьні властивості для збереження їхньої ECMAScript 5 функціональності. Замість цього, у ECMAScript 6 було додано метод Object.getOwnPropertySymbols(), щоб дозволити вам отримувати символьні властивості з об’єкту.

Значення, що повертається з Object.getOwnPropertySymbols() є масивом символьних властивостей. Наприклад:

let uid = Symbol.for("uid");
let object = {
    [uid]: "12345"
};

let symbols = Object.getOwnPropertySymbols(object);

console.log(symbols.length);        // 1
console.log(symbols[0]);            // "Symbol(uid)"
console.log(object[symbols[0]]);    // "12345"

У цьому коді, object має єдину символьну властивість uid. Масив, який поверне Object.getOwnPropertySymbols(), буде масивом, що містить лише цей символ.

Жоден об’єкт не має символьної властивості від початку, проте об’єкти можуть наслідувати символьні властивості від їхніх прототипів. ECMAScript 6 наперед задає кілька таких властивостей, імплементованих з використанням так званих добревідомих (well-known) символів.

Дослідження внутрішніх операцій з добревідомими символами

Центральною темою ECMAScript 5 було відкриття та визначення деяких “магічних” частин JavaScript, тих частин, які тоді не могли емулюватись розробниками. ECMAScript 6 продовжує цю традицію, відкриваючи навіть більше внутрішньої логіки мови, в першу чергу, через символьні властивості прототипів, що задають базову поведінку певних об’єктів.

ECMAScript 6 має наперед задані символи, які називаються добревідомими символами, що відображають загальну поведінку у JavaScript, що раніше розглядалась виключно внутрішніми операціями. Кожен добревідомий символ відображений властивістю об’єкту Symbol, як от Symbol.create.

Добревідомими символами є:

  • Symbol.hasInstance - метод, який використовується у instanceof для визначення успадкування об’єкта;
  • Symbol.isConcatSpreadable - булеве значення, що вказує, що Array.prototype.concat() слід “сплюснути” елементи колекції, якщо вона була передана в якості параметру до Array.prototype.concat();
  • Symbol.iterator - метод, що повертає ітератор (про ітератори піде мова у Главі 7);
  • Symbol.match - метод, що використовується у String.prototype.match() для порівняння рядків;
  • Symbol.replace - метод, що використовується у String.prototype.replace() для заміни підрядків;
  • Symbol.search - метод, що використовується у String.prototype.search() для знаходження підрядків;
  • Symbol.species - конструктор для створення похідних об’єктів (про похідні об’єкти буде йтись у Главі 8);
  • Symbol.split - метод, що використовується String.prototype.split() для розбиття рядків;
  • Symbol.toPrimitive - метод, що повертає примітивне представлення об’єкту;
  • Symbol.toStringTag - рядок, що використовується у Object.prototype.toString() для створення описів об’єктів;
  • Symbol.unscopables - об’єкт, властивості якого є іменами властивостей, що не повинні включатись в операторі with.

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

I> Перезапис методу, що визначається з допомогою добревідомого символу, перетворює звичайний об’єкт на незвичайний (exotic), тому що це змінює деяку внутрішню поведінку. В кінцевому результаті, ці зміни ніяк не повпливають на ваш код — вони просто змінюють те, як специфікація описує об’єкти.

Властивість Symbol.hasInstance

Кожна функція має метод Symbol.hasInstance, що визначає, чи є даний об’єкт екземпляром цієї функції, чи ні. Цей метод визначений в Function.prototype, тому всі функції наслідують поведінку за замовчуванням для властивості instanceof. Сама властивість Symbol.hasInstance визначається недоступною для запису та конфігурування і є неперелічуваною, щоб бути певними у тому, що вона не буде випадково змінена.

Метод Symbol.hasInstance приймає єдиний аргумент: значення для перевірки. Він повертає true, якщо передане значення є екземпляром функції. Щоб зрозуміти як працює Symbol.hasInstance, розгляньте такий код:

obj instanceof Array;

Цей код є еквівалентним до:

Array[Symbol.hasInstance](obj);

По суті, ECMAScript 6 перевизначає оператор instanceof як скорочення для виклику цього методу. І тепер, коли використовується виклик методу, ви можете змінити те, як працює instanceof.

Наприклад, припустимо ви хочете визначити функцію, що не матиме об’єктів–екземплярів. Ви можете зробити це, якщо жорстко встановите, що Symbol.hasInstance має повертати false, як ось тут:

function MyObject() {
    // ...
}

Object.defineProperty(MyObject, Symbol.hasInstance, {
    value: function(v) {
        return false;
    }
});

let obj = new MyObject();

console.log(obj instanceof MyObject);       // false

Ви повинні використовувати Object.defineProperty(), щоб перезаписати властивість, що недоступна для запису, тому у цьому прикладі використовується цей метод для перезапису методу Symbol.hasInstance на нову функцію. Нова функція завжди повертає false, тому навіть хоча й obj насправді є екземпляром класу MyObject, оператор instanceof поверне false після виклику Object.defineProperty().

Звісно, ви також можете перевіряти значення та вирішувати, чи має воно вважатись екземпляром. Для прикладу, числа зі значеннями від 1 до 100 мають розглядатись як екземпляри деякого спеціального чисельного типу. Щоб отримати таку поведінку, ви можете написати ось такий код:

function SpecialNumber() {
    // порожньо
}

Object.defineProperty(SpecialNumber, Symbol.hasInstance, {
    value: function(v) {
        return (v instanceof Number) && (v >=1 && v <= 100);
    }
});

let two = new Number(2),
    zero = new Number(0);

console.log(two instanceof SpecialNumber);    // true
console.log(zero instanceof SpecialNumber);   // false

Цей код задає метод Symbol.hasInstance, що повертає true, якщо значення є екземпляром Number і має значення від 1 до 100. Таким чином, two буде екземпляром SpecialNumber, не дивлячись на те, що немає жодних безпосередніх зв’язків між функцією SpecialNumber та змінною two. Зауважте, що лівий операнд у instanceof мусить бути об’єктом, для якого викличеться Symbol.hasInstance, оскільки для необ’єктів instanceof просто завжди повертає false.

W> Ви також можете перезаписати значення за замовчуванням властивості Symbol.hasInstance для всіх вбудованих функцій як от Date та Error. Це не рекомендовано, оскільки це може зробити ваш код складним для розуміння та непрогнозованим. Хорошою ідеєю є лише перезапис Symbol.hasInstance у ваших власних функцій і лише тоді, коли це необхідно.

Символ Symbol.isConcatSpreadable

JavaScript має метод concat() для конкатинації двох масивів. Ось як цей метод використовується:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat([ "blue", "black" ]);

console.log(colors2.length);    // 4
console.log(colors2);           // ["red","green","blue","black"]

Такий код конкатеную новий масив у кінець colors1 та створює colors2 — єдиний масив з усіма елементами обох масивів. Однак, метод concat() може також приймати необов’язкові аргументи і, у цьому випадку, ці аргументи будуть просто додаватись в кінець масиву. Наприклад:

let colors1 = [ "red", "green" ],
    colors2 = colors1.concat([ "blue", "black" ], "brown");

console.log(colors2.length);    // 5
console.log(colors2);           // ["red","green","blue","black","brown"]

Тут, додатковий аргумент "brown" переданий у concat() і він стає п’ятим елементом у масиві colors2. Чому аргументи–масиви трактуються не так як рядкові аргументи? Специфікація JavaScript каже, що масиви автоматично розбиваються на окремі елементи, а всі інші типи — ні. До ECMAScript 6 не було можливості змінити цю поведінку.

Властивість Symbol.isConcatSpreadable є булевим значенням, що вказує на те, чи цей об’єкт має властивість length і числові ключі, і ці числові властивості повинні додаватись окремо в результат виклику concat(). На відміну від інших добревідомих символів, ця властивість не з’являється у будь–яких об’єктів за замовчуванням. Замість цього, цей символ доступний в якості способу, щоб вказати як concat() працює з окремими типами об’єктів. Ви можете вказати як буде поводитись concat() при виклику для будь–якого типу, ось так:

let collection = {
    0: "Hello",
    1: "world",
    length: 2,
    [Symbol.isConcatSpreadable]: true
};

let messages = [ "Hi" ].concat(collection);

console.log(messages.length);    // 3
console.log(messages);           // ["Hi","Hello","world"]

Об’єкт collection у цьому прикладі встановлюється так, щоб виглядати наче масив: він має властивість length та два числові ключі. Властивість Symbol.isConcatSpreadable встановлюється в true, щоб вказувати на те, що значення властивостей мають бути додані у масив як окремі елементи. Коли у метод concat() передасться collection, результуючий масив матиме окремі елементи "Hello" та "world" після елементу "Hi".

I> Ви також можете встановити Symbol.isConcatSpreadable значення false для підкласів масивів, щоб попередити їх розбиття при виклику concat(). Про підкласи піде мова у Главі 8.

Символи Symbol.match, Symbol.replace, Symbol.search та Symbol.split

Рядки та регулярні вирази завжди мали тісні зв’язки у JavaScript. Рядковий тип, зокрема, має ряд методів, що приймають регулярний вирази в якості аргументів:

  • match(regex) - визначає чи даний рядок співпадає з регулярним виразом;
  • replace(regex, replacement) - замінює співпадіння з регулярним виразом на replacement;
  • search(regex) - знаходить відповідності регулярному виразу в рядку;
  • split(regex) - розбиває рядок на масив за регулярним виразом.

До ECMAScript 6 те, як ці методи взаємодіють з рядками, були приховані від розробників, не залишаючи жодної можливості, щоб зімітувати регулярні вирази через об’єкти, які створювались розробниками. ECMAScript 6 визначає чотири символи, що відповідають за ці чотири методи, відкриваючи нативну поведінку вбудованого об’єкту RegExp.

Символи Symbol.match, Symbol.replace, Symbol.search та Symbol.split відповідають методам, які мають мають викликатись з регулярними виразами в якості першого аргументу: метод match(), метод replace(), метод search() та метод split(), відповідно. Ці чотири символьні властивості задаються в RegExp.prototype в якості імплементації, яку рядкові методи мають використовувати за замовчуванням.

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

  • Symbol.match - функція, що приймає аргумент та повертає масив співпадінь, або null, якщо не було знайдено жодних співпадінь;
  • Symbol.replace - функція, що приймає рядок–аргумент та рядок–заміну та повертає новий рядок;
  • Symbol.search - функція, що приймає рядок–аргумент та повертає числовий індекс входження першого співпадіння, або -1, якщо не було знайдено жодних співпадінь;
  • Symbol.split - функція, що приймає рядок–аргумент та повертає масив, що містить фрагменти рядка, що були розділені за співпадінням.

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

// ефективна заміна для /^.{10}$/
let hasLengthOf10 = {
    [Symbol.match]: function(value) {
        return value.length === 10 ? [value.substring(0, 10)] : null;
    },
    [Symbol.replace]: function(value, replacement) {
        return value.length === 10 ?
            replacement + value.substring(10) : value;
    },
    [Symbol.search]: function(value) {
        return value.length === 10 ? 0 : -1;
    },
    [Symbol.split]: function(value) {
        return value.length === 10 ? ["", ""] : [value];
    }
};

let message1 = "Hello world",   // 11 characters
    message2 = "Hello John";    // 10 characters


let match1 = message1.match(hasLengthOf10),
    match2 = message2.match(hasLengthOf10);

console.log(match1);            // null
console.log(match2);            // ["Hello John"]

let replace1 = message1.replace(hasLengthOf10),
    replace2 = message2.replace(hasLengthOf10);

console.log(replace1);          // "Hello world"
console.log(replace2);          // "Hello John"

let search1 = message1.search(hasLengthOf10),
    search2 = message2.search(hasLengthOf10);

console.log(search1);           // -1
console.log(search2);           // 0

let split1 = message1.split(hasLengthOf10),
    split2 = message2.split(hasLengthOf10);

console.log(split1);            // ["Hello world"]
console.log(split2);            // ["", ""]

Об’єкт hasLengthOf10 націлений на те, щоб працювати так, як регулярний вираз, що знаходить будь–який рядок, що має довжину в 10 символів. Кожен з чотирьох методів hasLengthOf10 реалізований через відповідний символ, а тоді відповідні методи викликаються для двох рядків. Перший рядок, message1, має довжину 11 символів і тому не співпадає; другий рядок, message2, має довжину 10 символів і тому відповідає умові. Не дивлячись на те, що hasLengthOf10 не є регулярним виразом, від передається кожному рядковому методу і використовується правильно з додатковими методами.

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

Символ Symbol.toPrimitive

JavaScript часто намагається конвертувати об’єкти у примітивні значення при застосуванні певних операцій. Наприклад, коли ви порівнюєте рядок з об’єктом з допомогою оператора рівності (==), об’єкт конвертується у примітивне значення перед порівнянням. Саме це примітивне значення, яке має використовуватись, раніше було внутрішнім, проте ECMAScript 6 відкриває це значення (дає змогу змінити його) через метод Symbol.toPrimitive.

Метод Symbol.toPrimitive заданий в прототипі усіх стандартних типів та описує, що має статись при конвертації об’єкту у примітивний тип. Коли конвертація необхідна, викликається Symbol.toPrimitive з єдиним аргументом, який згадується у специфікації як hint. Аргумент hint — одним з трьох значень. Якщо hint є "number", тоді Symbol.toPrimitive має повернути число. Якщо hint є "string", тоді має повернутись рядок, а якщо він рівний "default", тоді операція не має бажаного типу (результатом може бути будь–який інший).

Для більшості стандартних об’єктів, числовий режим має таку поведінку, у порядку пріоритету:

  1. Викликати метод valueOf(), і якщо результат є примітивним значенням, повернути його;
  2. Інакше, викликати метод toString(), і якщо результат є примітивним значенням, повернути його;
  3. Інакше, кинути помилку.

Так само, для більшості стандартних типів, поведінка рядкового режиму має такий порядок:

  1. Викликати метод toString(), і якщо результат є примітивним значенням, повернути його;
  2. Інакше, викликати метод valueOf(), і якщо результатом є примітивне значення, повернути його;
  3. Інакше, кинути помилку.

У багатьох випадках, стандартні об’єкти трактують режим за замовчуванням (default mode) як еквівалентний до чисельного режиму (окрім Date, яка трактує режим за замовчуванням як еквівалент до рядкового режиму). Через задання методу Symbol.toPrimitive ви можете перевизначити поведінку приведення за замовчуванням.

I> Режим за замовчуванням використовується лише при використанні оператора ==, оператора + та, при передачі одного аргументу, конструктор Date. Більшість операцій використовують рядковий або чисельний режими.

Для перезапису поведінки приведення за замовчуванням використовуйте Symbol.toPrimitive та присвойте йому функцію в якості значення. Наприклад:

function Temperature(degrees) {
    this.degrees = degrees;
}

Temperature.prototype[Symbol.toPrimitive] = function(hint) {

    switch (hint) {
        case "string":
            return this.degrees + "\u00b0"; // символ градусів

        case "number":
            return this.degrees;

        case "default":
            return this.degrees + " degrees";
    }
};

let freezing = new Temperature(32);

console.log(freezing + "!");            // "32 degrees!"
console.log(freezing / 2);              // 16
console.log(String(freezing));          // "32°"

Такий скрипт оголошує конструктор Temperature та перезаписує метод за замовчуванням Symbol.toPrimitive на його прототипі. Різне значення повертається в залежності від того, чи аргумент hint вказує на рядок, число або режим за замовчуванням (аргумент hint встановлюється рушієм JavaScript). У рядковому режимі, функція Temperature() повертає температуру з Unicode–символом градусів. У числовому режимі, вона повертає просто числове значення, а у режимі за замовчуванням вона додає слово "degrees" після числа.

Кожен оператор console.log створює різне значення для аргументу hint. Оператор + призводить до того, що буде використовуватись режим за замочуванням, а hint — буде встановлено значення "default", оператор / призведе до того, що в hint буде значення "number" і буде використовуватись числовий режим, а функція String() призводить до того, що використовуватиметься рядковий режим і hint буде рівний "string". Також можливо повертати однакові значення для всіх трьох режимів — часто потрібно, що режим за замовчуванням був таким самим, як і рядковий або числовий.

Символ Symbol.toStringTag

Однією з найцікавіших проблем у JavaScript була можливість кількох глобальних середовищ виконання. Вона з’являється у web–браузерах, коли сторінка містить фрейм (iframe), оскільки як і сторінка, так і фрейм мають свої власні середовища виконання. У більшості випадків, це не проблема, оскільки дані можуть передаватись між середовищами. Проблема з’являється тоді, коли ви спробуєте ідентифікувати, з яким типом об’єкту ви працюєте після того, як того як цей об’єкт передавався між різними об’єктами.

Загальним прикладом цієї проблеми є передача масиву з фрейму у сторінку, що містить його або навпаки. В термінології ECMAScript 6 як фрейм, так і сторінка, що містить його представляють різні сфери, які є середовищами виконання для JavaScript. Кожна сфера має власну глобальну область видимості зі своїми власними копіями глобальних об’єктів. Незалежно від сфери, масив, що створюється, завжди буде масивом. Однак, при передачі його до іншої сфери, виклик instanceof Array поверне false, тому що цей масив, створений через конструктор з іншої сфери, а Array відображає конструктор з поточної.

Обхід проблеми з ідентифікацією

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

function isArray(value) {
    return Object.prototype.toString.call(value) === "[object Array]";
}

console.log(isArray([]));   // true

Це може виглядати як хак, проте це ідентифікує масиви однаково добре у всіх браузерах. Метод toString() для масивів не є дуже зручним для ідентифікації об’єктів, оскільки він повертає рядкове представлення елементів, що містяться у об’єктів. Проте метод toString() на Object.prototype має причуду: він включає внутрішньозадане ім'я [[Class]] у результат, що повертається. Розробники можуть використовувати цей метод для об’єктів, щоб дізнатись, з якого JavaScript–оточення прийшли дані об’єкту.

Розробники швидко зрозуміли, що, оскільки немає способу змінити цю поведінку, можна використовувати цей підхід, щоб розрізняти нативні об’єкти від об’єктів, створених розробниками. Найпоширенішим прикладом є об’єкт JSON в ECMAScript 5.

До ECMAScript 5 багато розробників використовували json2.js від Дугласа Крокфорда (Douglas Crockford), який створював глобальний об’єкт JSON. Оскільки браузери почали імплементувати глобальний об’єкт JSON, з’явилася потреба розрізняти глобальний об’єкт JSON з самого JavaScript–оточення та об’єкт JSON з інших бібліотек. Використовуючи той самий підхід, який я показав на прикладі функції isArray(), багато розробників написали ось такі функції:

function supportsNativeJSON() {
    return typeof JSON !== "undefined" &&
        Object.prototype.toString.call(JSON) === "[object JSON]";
}

Та властивість Object.prototype, що дозволяла розробникам ідентифікувати масиви між фреймами, також допомагала сказати чи JSON є нативним об’єктом JSON, чи ні. Ненативний об’єкт JSON повернув би [object Object], тоді як нативна версія поверне [object JSON]. Такий підхід став де-факто стандартом для ідентифікації нативних об’єктів.

ECMAScript 6 відповідь

ECMAScript 6 перевизначає цю поведінку через символ Symbol.toStringTag. Цей символ представляє властивість кожного об’єкта, що визначає яке значення слід повернути при виклику Object.prototype.toString.call() для цього об’єкта. Для масивів, функція поверне "Array", оскільки саме це значення міститься у властивості Symbol.toStringTag.

Також ви можете встановлювати значення Symbol.toStringTag для ваших власних об’єктів:

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

Person.prototype[Symbol.toStringTag] = "Person";

let me = new Person("Nicholas");

console.log(me.toString());                         // "[object Person]"
console.log(Object.prototype.toString.call(me));    // "[object Person]"

У цьому прикладі, властивість Symbol.toStringTag встановлена на Person.prototype для забезпечення поведінки за замовчуванням при створенні рядкового представлення. Оскільки Person.prototype наслідує метод Object.prototype.toString(), значення, яке повертається з Symbol.toStringTag, також використовується при виклику методу me.toString(). Однак, ви все ще можете задати власний метод toString(), що матиме іншу поведінку без впливу на те, як використовується метод Object.prototype.toString.call(). Ось приклад того, як це може виглядати:

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

Person.prototype[Symbol.toStringTag] = "Person";

Person.prototype.toString = function() {
    return this.name;
};

let me = new Person("Nicholas");

console.log(me.toString());                         // "Nicholas"
console.log(Object.prototype.toString.call(me));    // "[object Person]"

Цей код визначає, що Person.prototype.toString() повертає значення властивості name. Оскільки екземпляр Person більше не наслідує метод Object.prototype.toString(), виклик me.toString() демонструє іншу поведінку.

I> Всі об’єкти наслідують Symbol.toStringTag від Object.prototype, якщо не було вказано інакше. Рядок "Object" є значенням цієї властивості за замовчуванням.

Немає жодних обмежень стосовно того, яке значення має використовуватись у Symbol.toStringTag для об’єктів, що визначені розробниками. Для прикладу, ніщо не забороняє вам використовувати значення "Array" для властивості Symbol.toStringTag, як ось тут:

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

Person.prototype[Symbol.toStringTag] = "Array";

Person.prototype.toString = function() {
    return this.name;
};

let me = new Person("Nicholas");

console.log(me.toString());                         // "Nicholas"
console.log(Object.prototype.toString.call(me));    // "[object Array]"

У цьому прикладі, результатом виклику Object.prototype.toString() є "[object Array]", тобто результат є таким же, як і для звичайного масиву. Це підкреслює факт, що Object.prototype.toString() не є абсолютно надійним способом визначення типу об’єктів.

Також можна змінювати значення цієї властивості для нативних об’єктів. Просто присвойте щось властивості Symbol.toStringTag на прототипі об’єкта, ось так:

Array.prototype[Symbol.toStringTag] = "Magic";

let values = [];

console.log(Object.prototype.toString.call(values));    // "[object Magic]"

Властивість Symbol.toStringTag є перевизначеною для масивів у цьому прикладі, тому виклик Object.prototype.toString() дасть результат "[object Magic]". Я не раджу змінювати щось у нативних об’єктах таким чином, проте у мові немає нічого, що може заборонити вам це зробити.

Символ Symbol.unscopables

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

В результаті, оператор with заборонений у строгому режимі; це обмеження також стосується класів та модулів, що є у строгому режимі за замовчуванням без можливості вибору.

Тоді, у майбутньому оператор with безсумнівно використовуватись не буде, ECMAScript 6 досі підтримує with у нестрогому режимі для зворотньої сумісності. Таким чином, ми маємо знайти способи, щоб код, що досі використовує with, продовжував працювати справно.

Щоб зрозуміти складність цього завдання, розглянемо такий приклад:

let values = [1, 2, 3],
    colors = ["red", "green", "blue"],
    color = "black";

with(colors) {
    push(color);
    push(...values);
}

console.log(colors);    // ["red", "green", "blue", "black", 1, 2, 3]

У цьому прикладі, два виклики push() всередині оператора with є еквівалентними до colors.push() тому, що оператор with додає push як локальне зв’язування. color та values посилаються на змінні, що створені поза оператором with.

Проте ECMAScript 6 вводить для масивів метод values. (Про метод values детально піде мова у Главі 7, «Ітератори та генератори») Це б означало, що в оточенні ECMAScript 6, values всередині оператора with має посилатись не на локальну змінну values, а на метод масиву values, і це зламає код. Ось для чого існує символ Symbol.unscopables.

Символ Symbol.unscopables використовується на Array.prototype для індикації того, які властивості не мають створювати зв’язувань всередині оператора with. При наявності, Symbol.unscopables є об’єктом, чиї ключі є ідентифікаторами, які мають ігноруватись оператором with, і чиї значення рівні true мають прив’язуватись до блоку. Ось значення за замовчуванням властивості Symbol.unscopables для масивів:

// є в ECMAScript 6 за замовчуванням
Array.prototype[Symbol.unscopables] = Object.assign(Object.create(null), {
    copyWithin: true,
    entries: true,
    fill: true,
    find: true,
    findIndex: true,
    keys: true,
    values: true
});

Об’єкт Symbol.unscopables має прототип null, що створюється викликом Object.create(null) і містить всі нові методи масивів у ECMAScript 6. (Ці методи розглядатимуться у Главі 7, «Ітератори та генератори» та Главі 9, «Масиви».) Для цих методів, всередині оператора with, не створюються зв’язування, дозволяючи старому коду продовжувати працювати без проблем.

Загалом, вам не слід задавати Symbol.unscopables для ваших об’єктів, доки ви не використовуєте оператор with та робити зміни в об’єктах у вашому коді.

Підсумок

Символи — це новий тип примітивних значень у JavaScript і використовується для створення неперелічуваних властивостей, доступу до яких не буде без посилання на відповідний символ.

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

Ви можете передавати описи для символів, що дозволені для легшої ідентифікації символьних значень. Існує глобальний реєстр символів, що дозволяє вам поширювати символи між різними частинами вашого коду з допомогою однакових описів. Таким чином, один символ може використовуватись з однаковою метою у різних місцях.

Методи Object.keys() та Object.getOwnPropertyNames() не повертають символів, для цього в ECMAScript 6 було додано новий метод Object.getOwnPropertySymbols(), що дозволяє отримати символьні властивості. Ви можете вносити зміни у символьні властивості з допомогою методів Object.defineProperty() та Object.defineProperties().

Добревідомі символи визначають функціональність стандартних об’єктів, яка раніше була виключно внутрішньою і використовують символьні константи, що доступні глобально як от властивість Symbol.hasInstance. У специфікації ці символи мають префікс Symbol. та дозволяють розробникам змінювати поведінку стандартних об’єктів різноманітними способами.

results matching ""

    No results matching ""