Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Доброго времени суток, друзья!
Символ (Symbol) — это примитивный тип данных, представленный в ECMAScript2015 (ES6), позволяющий создавать уникальные идентификаторы: const uniqueKey = Symbol('SymbolName').
Вы можете использовать символы в качестве ключей для свойств объектов. Символы, которые JavaScript обрабатывает особым образом, называются хорошо известными символами. Эти символы используются встроенными алгоритмами 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 (которая признана устаревшей и запрещена в строгом режиме).
Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.