
Доброго времени суток, друзья!
Символ (Symbol) — это примитивный тип данных, представленный в ECMAScript2015 (ES6), позволяющий создавать уникальные идентификаторы: const uniqueKey = Symbol('SymbolName').
Вы можете использовать символы в качестве ключей для свойств объектов. Символы, которые JavaScript обрабатывает особым образом, называются хорошо известными символами (Well-known Symbols). Эти символы используются встроенными алгоритмами JavaScript. Например, Symbol.iterator используется для перебора элементов массивов, строк. Его также можно использовать для определения собственных функций-итераторов.
Данные символы играют важную роль, поскольку позволяют осуществлять тонкую настройку поведения объектов.
Будучи уникальными, использование символов в качестве ключей объектов (вместо строк) позволяет легко добавлять объектам новый функционал. При этом, не нужно беспокоиться о возникновении коллизий между ключами (поскольку каждый символ уникален), что может стать проблемой при использовании строк.
В данной статье речь пойдет о хорошо известных символах с примерами их использования.
В целях небольшого упрощения синтаксис хорошо известных символов Symbol.<name> представлен в формате @@<name>. Например, Symbol.iterator представлен как @@iterator, Symbol.toPrimitive — как @@toPrimitive и т.д.
Если мы говорим о том, что объект имеет метод @@iterator, значит, объект содержит свойство под названием Symbol.iterator, представленное функцией: { [Symbol.iterator]: function() { } }.
1. Краткое введение в символы
Символ — это примитивный тип (такой как число, строка или логическое значение), уникальный и неизменяемый (иммутабельный).
Для создания символа необходимо вызвать функцию Symbol() с опциональным аргументом — названием или, точнее, описанием символа:
const mySymbol = Symbol() const namedSymbol = Symbol('myName') typeof mySymbol // symbol typeof namedSymbol // symbol
mySymbol и namedSymbol — это символы-примитивы. namedSymbol имеет название 'myName', которое, обычно, используется в целях отладки кода.
При каждом вызове Symbol() создается новый уникальный символ. Два символа являются уникальными (или особыми), даже если имеют одинаковые названия:
const first = Symbol() const second = Symbol() first === second // false const firstNamed = Symbol('Lorem') const secondNamed = Symbol('Lorem') firstNamed === secondNamed // false
Символы могут быть ключами объектов. Для этого в объектном литерале или определении класса необходимо использовать синтаксис вычисляемых свойств ([symbol]):
const strSymbol = Symbol('String') const myObj = { num: 1, [strSymbol]: 'Hello World' } myObj[strSymbol] // Hello World Object.getOwnPropertyNames(myObj) // ['num'] Object.getOwnPropertySymbols(myObj) // [Symbol(String)]
Свойства-символы не могут быть получены с помощью Object.keys() или Object.getOwnPropertyNames(). Для доступа к ним нужно использовать специальную функцию Object.getOwnPropertySymbols().
Использование хорошо известных символов в качестве ключей позволяет изменять поведение объектов.
Хорошо известные символы доступны как неперечисляемые, неизменяемые и ненастраиваемые свойства объекта Symbol. Для их получения следует использовать точечную нотацию: Symbol.iterator, Symbol.hasInstance и т.д.
Вот как можно получить список хорошо известных символов:
Object.getOwnPropertyNames(Symbol) // ["hasInstance", "isConcatSpreadable", "iterator", "toPrimitive", // "toStringTag", "unscopables", "match", "replace", "search", // "split", "species", ...] typeof Symbol.iterator // symbol
Object.getOwnPropertyNames(Symbol) возвращает список собственных свойств объекта Symbol, включая хорошо известные символы. Разумеется, типом Symbol.iterator является symbol.
2. @@iterator, позволяющий делать объекты перебираемыми (итерируемыми)
Symbol.iterator — это, пожалуй, наиболее известный символ. Он позволяет определять, как объект должен перебираться с помощью инструкции for-of или spread-оператора (и должен ли он перебираться вообще).
Многие встроенные типы, такие как строки, массивы, карты (maps), наборы или коллекции (sets) являются итерируемыми по умолчанию, поскольку у них есть метод @@iterator:
const myStr = 'Hi' typeof myStr[Symbol.iterator] // function for (const char of myStr) { console.log(char) // по символу на каждой итерации: сначала 'H', затем 'i' } [...myStr] // ['H', 'i']
Переменная myStr содержит примитивную строку, у которой имеется свойство Symbol.iterator. Данное свойство содержит функцию, используемую для перебора символов строки.
Объект, в котором определяется метод Symbol.iterator, должен соответствовать протоколу перебора (итератора). Точнее, данный метод должен возвращать объект, соответствующий указанному протоколу. У такого объекта должен быть метод next(), возвращающий { value: <iterator_value>, done: <boolean_finished_iterator> }.
В следующем примере мы создаем итерируемый объект myMethods, позволяющий перебирать его методы:
function methodsIterator() { let index = 0 const methods = Object.keys(this) .filter(key => typeof this[key] === 'function') return { next: () => ({ done: index === methods.length, value: methods[index++] }) } } const myMethods = { toString: () => '[object myMethods]', sum: (a, b) => a + b, numbers: [1, 3, 5], [Symbol.iterator]: methodsIterator } for (const method of myMethods) { console.log(method) // toString, sum }
methodsIterator() — это функция, которая возвращает итератор { next: function() { } }. В объекте myMethods определяется вычисляемое свойство [Symbol.iterator] со значением methodsIterator. Это делает объект перебираемым с помощью цикла for-of. Методы объекта также можно получить с помощью [...myMethods]. Такой объект можно преобразовать в массив с помощью Array.from(myMethods).
Создание итерируемого объекта можно упростить с помощью функции-генератора. Данная функция возвращает объект Generator, соответствующий протоколу перебора.
Создадим класс Fibonacci с методом @@iterator, генерирующим последовательность чисел Фибоначчи:
class Fibonacci { constructor(n) { this.n = n } *[Symbol.iterator]() { let a = 0, b = 1, index = 0 while (index < this.n) { index++ let current = a a = b b = current + a yield current } } } const sequence = new Fibonacci(6) const numbers = [...sequence] console.log(numbers) // [0, 1, 1, 2, 3, 5]
*[Symbol.iterator]() { } определяет метод класса — функцию-генератор. Экземпляр Fibonacci соответствует протоколу перебора. spread-оператор вызывает метод @@iterator для создания массива чисел.
Если примитивный тип или объект содержит @@iterator, он может быть использован в следующих сценариях:
- Перебор элементов с помощью for-of
- Создание массива элементов с помощью spread-оператора
- Создание массива с помощью Array.from(iterableObject)
- В выражении yield* для передачи другому генератору
- В конструкторах Map(), WeakMap(), Set() и WeakSet()
- В статических методах Promise.all(), Promise.race() и т.д.
Подробнее про создание перебираемого объекта можно почитать здесь.
3. @@hasInstance для настройки instanceof
По умолчанию оператор obj instanceof Constructor проверяет, имеется ли в цепочке прототипов obj объект Constructor.prototype. Рассмотрим пример:
function Constructor() { // ... } const obj = new Constructor() const objProto = Object.getPrototypeOf(obj) objProto === Constructor.prototype // true obj instanceof Constructor // true obj instanceof Object // true
obj instanceof Constructor возвращает true, поскольку прототипом obj является Constructor.prototype (как результат вызова конструктора). instanceof при необходимости обращается к цепочке прототипов, поэтому obj instanceof Object также возвращает true.
Иногда в приложении требуется более строгая проверка экземпляров.
К счастью, у нас имеется возможность определить метод @@hasInstance для изменения поведения instanceof. obj instanceof Type является эквивалентом Type[Symbol.hasInstance](obj).
Давайте проверим, являются ли переменные итерируемыми:
class Iterable { static [Symbol.hasInstance](obj) { return typeof obj[Symbol.iterator] === 'function' } } const arr = [1, 3, 5] const str = 'Hi' const num = 21 arr instanceof Iterable // true str instanceof Iterable // true num instanceof Iterable // false
Класс Iterable содержит статический метод @@hasInstance. Данный метод проверяет, является ли obj перебираемым, т.е. содержит ли он свойство Symbol.iterator. arr и str являются итерируемыми, а num нет.
4. @@toPrimitive для преобразования объекта в примитив
Используйте Symbol.toPrimitive для определения свойства, значением которого является функция преобразования объекта в примитив. @@toPrimitive принимает один параметр — hint, которым может быть number, string или default. hint указывает на тип возвращаемого значения.
Усовершенствуем преобразование массива:
function arrayToPrimitive(hint) { if (hint === 'number') { return this.reduce((x, y) => x + y) } else if (hint === 'string') { return `[${this.join(', ')}]` } else { // hint имеет значение по умолчанию return this.toString() } } const array = [1, 3, 5] array[Symbol.toPrimitive] = arrayToPrimitive // преобразуем массив в число. hint является числом + array // 9 // преобразуем массив в строку. hint является строкой `array is ${array}` // array is [1, 3, 5] // преобразование по умолчанию. hint имеет значение default 'array elements: ' + array // array elements: 1,3,5
arrayToPrimitive(hint) — это функция, преобразующая массив в примитив на основе значения hint. Присвоение array[Symbol.toPrimitive] значения arrayToPrimitive заставляет массив использовать новый метод преобразования. Выполнение + array вызывает @@toPrimitive со значением hint, равным number. Возвращается сумма элементов массива. array is ${array} вызывает @@toPrimitive с hint = string. Массив преобразуется в строку '[1, 3, 5]'. Наконец 'array elements: ' + array использует hint = default для преобразования. Массив преобразуется в '1,3,5'.
Метод @@toPrimitive используется для представления объекта в виде примитивного типа:
- При использовании оператора нестрогого (абстрактного) равенства: object == primitive
- При использовании оператора сложения/конкатенации: object + primitive
- При использовании оператора вычитания: object — primitive
- В различных ситуациях преобразования объекта в примитив: String(object), Number(object) и т.д.
5. @@toStringTag для создания стандартного описания объекта
Используйте Symbol.toStringTag для определения свойства, значением которого является строка, описывающая тип объекта. Метод @@toStringTag используется Object.prototype.toString().
Спецификация определяет значения, возвращаемые Object.prototype.toString() по умолчанию, для многих типов:
const toString = Object.prototype.toString toString.call(undefined) // [object Undefined] toString.call(null) // [object Null] toString.call([1, 4]) // [object Array] toString.call('Hello') // [object String] toString.call(15) // [object Number] toString.call(true) // [object Boolean] // Function, Arguments, Error, Date, RegExp и т.д. toString.call({}) // [object Object]
Эти типы не имеют свойства Symbol.toStringTag, поскольку алгоритм Object.prototype.toString() оценивает их особым образом.
Рассматриваемое свойство определяется в таких типах, как символы, функции-генераторы, карты, промисы и др. Рассмотрим пример:
const toString = Object.prototype.toString const noop = function() { } Symbol.iterator[Symbol.toStringTag] // Symbol (function* () {})[Symbol.toStringTag] // GeneratorFunction new Map()[Symbol.toStringTag] // Map new Promise(noop)[Symbol.toStringTag] // Promise toString.call(Symbol.iterator) // [object Symbol] toString.call(function* () {}) // [object GeneratorFunction] toString.call(new Map()) // [object Map] toString.call(new Promise(noop)) // [object Promise]
В случае, когда объект не относится к группе со стандартным типом и не содержит свойства @@toStringTag, возвращается Object. Разумеется, мы можем это изменить:
const toString = Object.prototype.toString class SimpleClass { } toString.call(new SimpleClass) // [object Object] class MyTypeClass { constructor() { this[Symbol.toStringTag] = 'MyType' } } toString.call(new MyTypeClass) // [object MyType]
Экземпляр класса SimpleClass не имеет свойства @@toStringTag, поэтому Object.prototype.toString() возвращает [object Object]. В конструкторе класса MyTypeClass экземпляру присваивается свойство @@toStringTag со значением MyType, поэтому Object.prototype.toString() возвращает [object MyType].
Обратите внимание, что @@toStringTag был введен в целях обеспечения обратной совместимости. Его использование нежелательно. Для определения типа объекта лучше использоваить instanceof (совместно с @@hasInstance) или typeof.
6. @@species для создания производного объекта
Используйте Symbol.species для определения свойства, значением которого является функция-конструктор, используемая для создания производных объектов.
Значением @@species многих конструкторов являются сами конструкторы:
Array[Symbol.species] === Array // true Map[Symbol.species] === Map // true RegExp[Symbol.species] === RegExp // true
Во-первых, обратите внимание, что производным называется объект, возвращаемый после совершения определенной операции с исходным объектом. Например, вызов map() возвращает производный объект — результат преобразования элементов массива.
Обычно, производные объекты ссылаются на тот же конструктор, что и исходные объекты. Но порой возникает необходимость в определении другого конструктора (возможно, одного из стандартных классов): вот где может помочь @@species.
Предположим, что мы расширяем конструктор Array с помощью дочернего класса MyArray для добавления некоторых полезных методов. При этом мы хотим, чтобы конструктором производных объектов экземпляра MyArray был Array. Для этого необходимо определить вычисляемое свойство @@species со значением Array:
class MyArray extends Array { isEmpty() { return this.length === 0 } static get [Symbol.species]() { return Array } } const array = new MyArray(2, 3, 5) array.isEmpty() // false const odds = array.filter(item => item % 2 === 1) odds instanceof Array // true odds instanceof MyArray // false
В MyArray определено статическое вычисляемое свойство Symbol.species. Оно указывает, что конструктором производных объектов должен быть конструктор Array. Позже при фильтрации элементов массива array.filter() возвращает Array.
Вычисляемое свойство @@species используется методами массивов и типизированных массивов, такими как map(), concat(), slice(), splice(), возвращающими производные объекты. Использование данного свойства может быть полезным для расширения карт, регулярных выражений или промисов с сохранением оригинального конструктора.
7. Создание регулярного выражения в форме объекта: @@match, @@replace, @@search и @@split
Прототип строки содержит 4 метода, принимающих регулярные выражения в качестве аргумента:
- String.prototype.match(regExp)
- String.prototype.replace(regExp, newSubstr)
- String.prototype.search(regExp)
- String.prototype.split(regExp, limit)
ES6 позволяет этим методам принимать другие типы при условии определения соответствующих вычисляемых свойств: @@match, @@replace, @@search и @@split.
Любопытно, что прототип RegExp содержит указанные методы, также определенные с помощью символов:
typeof RegExp.prototype[Symbol.match] // function typeof RegExp.prototype[Symbol.replace] // function typeof RegExp.prototype[Symbol.search] // function typeof RegExp.prototype[Symbol.split] // function
В следующем примере мы определяем класс, который может использоваться вместо регулярного выражения:
class Expression { constructor(pattern) { this.pattern = pattern } [Symbol.match](str) { return str.includes(this.pattern) } [Symbol.replace](str, replace) { return str.split(this.pattern).join(replace) } [Symbol.search](str) { return str.indexOf(this.pattern) } [Symbol.split](str) { return str.split(this.pattern) } } const sunExp = new Expression('солнечный') 'солнечный день'.match(sunExp) // true 'дождливый день'.match(sunExp) // false 'солнечный day'.replace(sunExp, 'дождливый') // 'дождливый день' 'обещают солнечный день'.search(sunExp) // 8 'оченьсолнечныйдень'.split(sunExp) // ['очень', 'день']
В классе Expression определяются методы @@match, @@replace, @@search и @@split. Затем экземпляр этого класса — sunExp используется в соответствующих методах вместо регулярного выражения.
8. @@isConcatSpreadable для преобразования объекта в массив
Symbol.isConcatSpreadable представляет собой логическое значение, указывающее на возможность преобразования объекта в массив с помощью метода Array.prototype.concat().
По умолчанию, метод concat() извлекает элементы массива (раскладывает массив на элементы, из которых он состоит) при объединении массивов:
const letters = ['a', 'b'] const otherLetters = ['c', 'd'] otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']
Для объединения двух массивов letters передается в качестве аргумента методу concat(). Элементы массива letters становятся частью результата объединения: ['c', 'd', 'e', 'a', 'b'].
Для того, чтобы предотвратить разложение массива на элементы и сделать массив частью результата объединения как есть, свойству @@isConcatSpreadable следует присвоить значение false:
const letters = ['a', 'b'] letters[Symbol.isConcatSpreadable] = false const otherLetters = ['c', 'd'] otherLetters.concat('e', letters) // ['c', 'd', 'e', ['a', 'b']]
В противоположность массиву, метод concat() не раскладывает на элементы массивоподобные объекты. Это поведение также можно изменить с помощью @@isConcatSpreadable:
const letters = { 0: 'a', 1: 'b', length: 2 } const otherLetters = ['c', 'd'] otherLetters.concat('e', letters) // ['c', 'd', 'e', {0: 'a', 1: 'b', length: 2}] letters[Symbol.isConcatSpreadable] = true otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']
9. @@unscopables для доступа к свойствам посредством with
Symbol.unscopables — это вычисляемое свойство, собственные имена свойств которого исключаются из объекта, добавляемого в начало цепочки областей видимости с помощью инструкции with. Свойство @@unscopables имеет следующий формат: { propertyName: <boolean_exclude_binding> }.
ES6 определяет @@unscopables только для массивов. Это сделано в целях сокрытия новых методов, которые могут перезаписать одноименные переменные в старом коде:
Array.prototype[Symbol.unscopables] // { copyWithin: true, entries: true, fill: true, // find: true, findIndex: true, keys: true } let numbers = [1, 3, 5] with (numbers) { concat(7) // [1, 3, 5, 7] entries // ReferenceError: entries is not defined }
Мы можем получить доступ к методу concat() в теле with, поскольку данный метод не содержится в свойстве @@unscopables. Метод entries() указан в этом свойстве и имеет значение true, что делает его недоступным внутри with.
@@unscopables был введен исключительно для обеспечения обратной совместимости со старым кодом, где используется инструкция with (которая признана устаревшей и запрещена в строгом режиме).
Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.
