Що таке Symbol в JavaScript
Symbol — один з примітивних типів даних. Це дещо своєрідне та може бути складнішим в розумінні.
Виклик `Symbol()` створює новий символ, значення якого гарантовано відрізняється від будь-яких створений раніше символів.
const sym1 = Symbol();
const sym2 = Symbol();
sym1 === sym2; // false
Що ж це за значення? Це не текст, не літера, не число. У символів немає якогось текстового представлення. Тому якщо ви спробуєте вивести символ в консоль, ви побачите лише “Symbol()” — факт того, що це символ, але не його “значення”, бо його заховано глибоко в реалізації рушія.
console.log( Symbol() ); // Symbol() 🤷♂️
Саме гарантія неповторюваності є ключовою механікою символів. Новостворена змінна єдиний спосіб використовувати той самий символ.
function createObj() {
const sym = Symbol()
const obj = {
[sym]: 'Привіт, Світе!'
};
console.log( obj[sym] ) // Привіт, Світе!
return obj;
}
const obj2 = createObj();
console.log(obj) // { Symbol(): "Привіт, Світе!" }
Вивівши структуру obj2 ви можете побачити, що в ньому є властивість за ключем-символом, але ви не можете отримати доступ до цієї властивості, оскільки змінна-ключ залишилась в області видимості createObj().
Для чого вони потрібні
Оскільки символ це примітив, то його можна використовувати як ключ для властивостей об’єктів.
В моїй практиці при розробці бібліотеки я кілька разів опирався на символи, щоб домішувати щось в користувацькі об’єкти та гарантувати, що я не перезапишу якісь дані.
const cachedKey = Symbol()
export function doSomeJobWithObjectAndCacheResult(obj) {
if (obj[cachedKey] === undefined) {
obj[cachedKey] = doSomeJobWithObject(obj)
}
return obj[cachedKey]
}
Оскільки кожен створений символ це унікальний примітив, то функція вище гарантує, що в переданому об’єкті не може бути поля за цим символом створеним десь окрім як цією функцією.
Доступ до цього поля неможливий за межами цієї функції.
Які є вбудовані Symbol
В JavaScript чимало символів створюються та використовуються замовчуванням. Всі вони визначені у статичних властивостях Symbol.
console.dir(Symbol) /*
{
name: "Symbol"
asyncIterator: Symbol("Symbol.asyncIterator"),
hasInstance: Symbol("Symbol.hasInstance"),
isConcatSpreadable: Symbol("Symbol.isConcatSpreadable"),
iterator: Symbol("Symbol.iterator"),
match: Symbol("Symbol.match"),
matchAll: Symbol("Symbol.matchAll"),
replace: Symbol("Symbol.replace"),
search: Symbol("Symbol.search"),
species: Symbol("Symbol.species"),
split: Symbol("Symbol.split"),
toPrimitive: Symbol("Symbol.toPrimitive"),
toStringTag: Symbol("Symbol.toStringTag"),
unscopables: Symbol("Symbol.unscopables"),
}
*/
І ви їх використовуєте, навіть не усвідомлюючи цього. Наприклад, викликаючи цикл for на масиві:
const arr = [1,2,3]
for (const num of arr) {
console.log(num)
}
// 1
// 2
// 3
У масивів є прихований метод, який вираховує та повертає значення для кожної ітерації (такі функції називаються ітераторами). А захований цей метод за символом Symbol.iterator.
Тобто, під час роботи циклу рушій JavaScript викликає прихований метод `arr[Symbol.iterator]()` та записує його результат в змінну num. І повторює цей процес доти, доки `arr[Symbol.iterator]()` повертає бодай щось.
І ми можемо змінювати поведінку JavaScript змінюючи приховані методи за такими символами:
const arr = [1,2,3]
arr[Symbol.iterator] = function* () {
yield 'Що це?';
yield 'Це взагалі законно?';
yield 'Чортівня! 🪄';
}
for (const num of arr) {
console.log(num)
}
// Що це?
// Це взагалі законно?
// Чортівня! 🪄
Змінивши метод за Symbol.iterator ми фактично переписали реалізацію перебору значень масиву. І це працює не лише для циклів
const newArr = [...arr]
console.log(newArr)
// [ "Що це?", "Це взагалі законно?", "Чортівня! 🪄" ]
Шпаргалка по JavaScript Symbol-ам
Symbol.hasInstance
Метод, який визначає, чи розпізнає об’єкт-конструктор інший об’єкт як свій екземпляр. Використовується в `instanceof`.
obj instanceof Array // obj[Symbol.hasInstance](Array)
Symbol.isConcatSpreadable
Логічне значення, яке визначає, чи потрібно “розгортати” значення при конкатенації з масивом, чи залишати як є. Використовується в `Array.prototype.concat()`.
const arr1 = [1,2,3]
const arr2 = [4,5,6]
console.log(arr1.concat(arr2)) // [1,2,3,4,5,6]
arr2[Symbol.isConcatSpreadable] = false
console.log(arr1.concat(arr2)) // [1,2,3,[4,5,6]]
Symbol.iterator та Symbol.asyncIterator
Про цього написав трохи вище. Методи, які повертають ітератори об’єкта.
Symbol.match та Symbol.matchAll
Методи які використовуються в `String.prototype.match()` та `String.prototype.matchAll()` відповідно.
'foo'.match(obj) // obj[Symbol.match]('foo')
'foo'.matchAll(obj) // obj[Symbol.matchAll]('foo')
Symbol.replace
Метод, який визначає як буде замінено частину рядка. Використовується в `String.prototype.replace()`.
'foo'.replace(obj, 'bar') // obj[Symbol.replace]('foo', 'bar')
Symbol.search
Метод який повертає індекс символу який відповідає об’єкту. Використовується в `String.prototype.search()`.
'foo'.search(obj) // obj[Symbol.search]('foo')
Symbol.species
Функція конструктор. Методи, що створюють копії об’єктів, можуть звертатись до цього символу, щоб змінювати прототип для новоствореної копії.
class MyArray extends Array {
// Визначає, що всі копії будуть вважати Array за батьківський конструктор
static get [Symbol.species]() {
return Array;
}
}
const a = new MyArray(1, 2, 3);
const mapped = a.map((x) => x * x);
console.log(a.constructor); // MyArray
console.log(mapped.constructor); // Array
Symbol.split
Метод, який розрізає рядок за переданим регулярним виразом. Використовується в `String.prototype.split()`.
'foo'.split(obj, limit) // obj[Symbol.split]('foo', limit)
Symbol.toPrimitive
Метод, який конвертує об’єкт в примітив. Використовується при приведенні типів.
const obj = {};
console.log(+obj); // NaN
console.log(`${obj}`); // "[object Object]"
console.log(obj + ""); // "[object Object]"
// Змінюємо алгоритм конвертації об'єкту в примітивні значення.
obj[Symbol.toPrimitive] = function(hint) {
if (hint === "number") {
return 42;
}
if (hint === "string") {
return "Привіт";
}
return true;
};
console.log(+obj); // 42 — hint === "number"
console.log(`${obj}`); // "Привіт" — hint === "string"
console.log(obj + ""); // "true" — hint === "default"
Symbol.unscopables
Об’єкт, ключі якого буде виключено з області видимості with
const obj {
name: 'Alex'
password: '1234'
[Symbol.unscopables]: {
password: true
}
}
with(obj) {
console.log(name) // Alex
console.log(password) // undefined
}
Підсумок
Як бачите, чимала частина “внутрішньої” поведінки JavaScript визначена в прихованих за символами методах. І хоч я й називаю їх прихованими, насправді ви вільні маніпулювати ними як завгодно, створювати власні унікальні класи з нестандартною поведінкою.