JavaScript: Символы / Symbol — детальное рассмотрение

Новая структура данных Symbol — это новый примитивный тип. Он также немутируемый (immutable).

Основная задача Символа — вернуть уникальное значение.

Частиный перевод статей «Deep dive into ES6 Symbols» и «A practical guide to ES6 Symbol» с сайта medium.com.


Синтаксис

Чтобы создать новый символ достаточно использовать функцию конструктор Symbol().

var symA = Symbol();
var symB = Symbol("B");
var symC = Symbol("B");

Все символы выше — уникальны.

symB === symC; // false

Если нужно получить доступ к символу из любого места в коде, нужно использовать метод Symbol.for().

// Символ создается в глобальном реестре
const symbol = Symbol.for('the-symbol');

// Где-то в другом месте
const sym = Symbol.for('the-symbol'); // Символ получен из глобального реестра

// Это один и тот же символ
console.log(symbol === sym); // true

// Можно получить доступ к описанию символа 
console.log(Symbol.keyFor(sym)); // the-symbol

Символ можно определить по типу используя typeof.

let symbol = Symbol();
typeof symbol; // symbol

Всего выделяют три вида Символов:

  • Пользовательские Символы — созданные с помощью функции конструктора Symbol();
  • Глобальные Символы — созданные с помощью метода Symbol.for();
  • Зарезервированные Символы — определенные как статические свойства объекта Symbol.

Для чего используются Символы

Самый распрастраненный случай использования Символов это использование их в качестве свойств объектов. Преимущества Символов перед обычными свойствами в том, что они не видны в списке свойств при итерировании, что делает их отличным инструментом для метапрограммирования и сокрытия внутренних методов и объектов.

Также, Символы используются JS движками для реализации различных фичей. Например, Symbol.iterator используется для реализации Iterable объектов. Symbol.toPrimitive используется во время преобразования объектов в примитивные типы. Примеры рассмотрим далее в этой статье.

Список всех стандартных Символов:

// Iteration symbols
Symbol.iterator
Symbol.asyncIterator

// RegEx symbols
Symbol.match
Symbol.matchAll
Symbol.replace
Symbol.search
Symbol.split

// Other symbols
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.unscopables
Symbol.species
Symbol.toPrimitive
Symbol.toStringTag

Примеры использования

Символ как свойство объекта:

const s = Symbol('secret');
let data = {};
data[s] = 1;
let newData = Object.assign({}, data);
 
Object.getOwnPropertyNames(data); // []
( s in data ); // true
( s in newData ); // true
( Symbol('secret') in data ); // false !!!

Можно реализовывать нечто вроде перегрузки или переопределения стандартных фичей:

const MATCH = RegExp.prototype[Symbol.match];

RegExp.prototype[Symbol.match] = function(searchString) {
 return !!MATCH.call(this, searchString) // return true or false
}
 
"foo".match(/foo/) // true
"foo".match(/bar/) // false

Стандартные символы

Как пример возьмем Класс реализовывающий объект для работы с паролем.

const PWD = Symbol('pwd')
const LEN = Symbol('len')
 
const hashCode = str => Array.from(str)
  .reduce((h, c) => Math.imul(31, h) + c.charCodeAt(0) | 0, 0)
 
class Password {
  constructor(pwdStr) {
    this[PWD] = hashCode(pwdStr)
    this[LEN] = pwdStr.length
  }
  
  [Symbol.toPrimitive](hint) {
    return '*'.repeat(this[LEN])
  }
 
  [Symbol.match](pwdStr) {
    return this[PWD] === hashCode(pwdStr)
  }
 
  get [Symbol.toStringTag]() {
    return 'Password';
  }
 
  static [Symbol.hasInstance](instance) {
    return typeof instance === 'string';
  }
 
  [Symbol.iterator]() {
    return ('' + this)[Symbol.iterator]()
  }
}
 
let securePassword = new Password('password');
 
securePassword.length // 8
('' + securePassword) // "********"
[...securePassword] // ["*","*","*","*","*","*","*","*"]
'password'.match(securePassword) // true
'notmypassword'.match(securePassword) // false
securePassword.toString() // "[object Password]"