Если вы когда-либо искали информацию о типе данных Symbol, то наверняка уже знаете, что Symbol - это уникальный и неизменяемый тип данных, который может быть использован как идентификатор для свойств объектов (MDN). Символы относятся к примитивным типам данным, к которым также относят числа, строки, булевы величины. Однако не стоит забывать, что в JavaScript все является объектом, т.к. и у примитивных типов данных есть свойства и методы.
Зачем нужен тип Symbol?
- Избегание коллизии имён - символ уникален даже при совпадающем имени, поэтому переменные типа Symbol не повторяются и не перезаписываются сторонними библиотеками
- Символы, как свойства (ключи) объектов - эти свойства недоступны для обычного обхода всех свойств циклом for...in, поэтому могут быть таким образом частично скрыты
- Для создания функций-итераторов для неитерируемых изначально объектов.
Объявление переменных типа Symbol
Тип символ объявляется без ключевого слова new. В скобках можно указать какой-либо параметр, но нужно понимать, что особенностью типа данных Symbol является уникальность, поэтому 2 символа с одинаковыми параметрами не будут равны друг другу.
При создании символу можно дать описание (или другими словами имя). Для этого нужно написать Symbol()
, указав какую-либо строку в качестве описания этого символа, например let symb2 = Symbol("hello")
. Описание – это просто метка, которая ни на что особо не влияет, но помогает идентифицировать символ при отладке в консоли. Однако помните о том, что тип Symbol гарантированно уникален. Даже если вы создадите множество символов с одинаковым описанием, это всё равно будут разные символы.
Создаются новые символы с помощью функции Symbol()
:
1 2 3 |
let symb1 = Symbol(); let symb2 = Symbol("id"); // символ с описанием console.log(Symbol("id") === Symbol("id")); // false |
Вы можете получить описание символа с помощью свойства Symbol.description
:
1 |
console.log(symb2.description); // id |
Если вы попытаетесь объявить символ с помощью ключевого слова new
, то увидите ошибку в консоли браузера:
1 |
let mySym = new Symbol(); // TypeError |
Обратите внимание, что ошибка появляется в этом случае только при попытке создать именно тип Symbol с помощью ключевого слова new. Для других примитивных типов создание явных объектов-обёрток вполне работоспособно, например: new Boolean(true)
, new String("My String")
, new Number(456)
.
Иными словами — символа нужен именно в качестве уникального идентификатора, а new
возвращает объект; объект же здесь не нужен.
Если вам по какой-то причине необходимо, чтобы символ был объектом, то придется обернуть символ в объект, используя функцию Object()
:
1 2 3 4 |
let symbol1 = Symbol("somedescr"); console.log(typeof symbol1); // "symbol" let symbolObj1 = Object(symbol1); console.log(typeof symbolObj1); // "object" |
Глобальные символы
Для создания символов, доступных во всех файлах и в глобальной области, вы можете использовать методы Symbol.for()
и Symbol.keyFor()
, чтобы задать или получить символ из глобального символьного реестра. То есть такие глобальные символы доступны во всех частях вашей программы.
Даже, если вы создадите 2 символа с одинаковым ключом, в реальности обе переменные будут вести к одному и тому же символу, т.е. это будет уникальная, но одна и та же переменная. Поэтому переменные будут равны, в отличие от тех, которые создавались с одинаковым описанием. Для этого нам понадобится метод Symbol.for()
:
1 2 3 4 5 |
let s1 = Symbol.for("global"); let s2 = Symbol.for("global"); console.log(Symbol.for("global"), s1); // Symbol(global) Symbol(global) Symbol(global) console.log(Symbol.for("global") == s1); // true console.log(s2 == s1); // true |
Если вам нужно получить значение ключа для символа, используйте метод Symbol.keyFor()
:
1 2 3 |
let globalSymbol = Symbol.for("allGlobal"), localSymbol = Symbol("onlyLocal"); console.log(Symbol.keyFor(globalSymbol), Symbol.keyFor(localSymbol)); //allGlobal undefined |
В результате использования метода Symbol.keyFor()
вы получите строку с ключом указанного символа, если он есть в глобальном реестре символов, либо undefined
, если он там отсутствует.
Метод toString()
Символы - это особый тип данных, и они автоматически не преобразуются в строку. Однако, если нужно получить строковое представление символа, то нужно использовать метод toString()
:
1 2 3 |
console.log(Symbol("desc").toString()); //Symbol(desc) console.log(Symbol.for("temp").toString()); //Symbol(temp) console.log(Symbol.iterator.toString()); //Symbol(Symbol.iterator |
Доступ к символам, как ключам объекта
Поскольку символы - это уникальные идентификаторы, которые не изменяются извне при совпадении имени переменной, в качестве значения которой они выступают, их можно использовать в ситуации, когда возможна перезапись одной переменной другой. Если другая библиотека или внешний скрипт будут работать с вашим объектом, то при переборе свойств этого объекта, доступ к символьному свойству будет закрыт. Метод Object.keys(some_object)
также игнорирует символы.
Мы можем указать символ в качестве ключа (поля, или свойства) объекта:
1 2 3 4 5 6 7 8 |
let person = { name: "Alex", age: 23, [Symbol("id")]: 123, [Symbol("info")]: "user" }; console.log(person); //{name: 'Alex', age: 23, Symbol(id): 123, Symbol(info): 'user'} |
При выводе в консоль мы увидим свойство id
и info
. То же самое, но уже без свойства id и без свойства info мы увидим, если попробуем в ту же консоль вывести каждое свойство с помощью цикла for...in
:
1 2 3 4 5 6 |
for(let key in person){ console.log(key, person[key]); } //Выводится: // name Alex // age 23 |
То есть то свойство, которое имеет ключ в виде символа, не выводится в цикле. Если вам нужно получить только те свойства, которые имеют тип Symbol, вы можете использовать метод объекта Object, который для этого предназначен - Object.getOwnPropertySymbols(objectName)
:
1 2 |
console.log(Object.getOwnPropertySymbols(person)); //[Symbol(id), Symbol(info)] |
Метод Object.getOwnPropertySymbols()
возвращает массив символов и позволяет найти свойства символов для данного объекта.
Метод Symbol.iterator()
Symbol.iterator()
- это, пожалуй, самый используемый метод из тех, которые есть у символов. Он возвращает итератор по умолчанию для объекта. Symbol.iterator()
позволяет перебирать элементы в массивах и массивоподобных объектах. Они еще называются итерируемыми. Это такие объекты, как Array
, String
, Map
, Set
, объект аргументов функции arguments, коллекции элементов на странице и т.д. Итерируемым объектом является любой объект, который реализует интерфейс Iterable, то есть такой, у которого есть метод Symbol.iterator
, возвращающий объект с методом next()
.
Сложно? Давайте разберемся с этим методом в несколько этапов.
Для того чтобы понять, что такое итератор, напишем функцию, которая будет проходить по массиву и возвращать следующее значение массива до тех пор, пока это будет возможно, т.е. до последнего элемента включительно. В случае, если значение можно получить, внутренняя функция next()
возвращает значение элемента массива и метку done
со значением false
, а если значение недоступно (превышен номер индекса), то значение будет равно undefined
, а метка done
будет true
.
В коде мы передаем в функцию массив и проходим по нему с помощью вложенной функции next()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function likeIterator(arr){ let nextIndex = 0; return { next(){ return nextIndex < arr.length ? { value: arr[nextIndex++], done: false } : { done: true } } } } let numbers = likeIterator([100, 200, 300]); console.log(numbers.next().value); //100 console.log(numbers.next().value); //200 console.log(numbers.next().value); //300 console.log(numbers.next().value); //undefined |
Однако дело в том, что такие сложности с массивами излишни. Они из коробки (т.е. из ядра JavaScript) имеют встроенный итератор, который позволяет перебирать их значения в цикле for...of
. Он появился в JavaScript как раз для обхода любых итерируемых объектов, и имеет возможность прерывания его выполнения оператором break
.
Допустим, нам нужно сделать итерируемым некий объект, в котором есть свойства в виде чисел, т.е. некий диапазон, по которому мы должны пройтись с неким шагом. В качестве функции, осуществляющей обход, будем использовать Symbol.iterator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const diapason = { from: 100, to: 500, step: 100 } diapason[Symbol.iterator] = function(){ let current = this.from; let last = this.to; let step = this.step; return { next(){ return current < last ? {done: false, value: current+=step } : {done: true} } } } for(let temp of diapason){ console.log(temp); //100 200 300 400 500 } |
Генераторы. Как использовать для итерируемых объектов?
Генератор - это особый вид функций, который может приостанавливать своё выполнение, возвращать промежуточный результат, а также возобновлять своё выполнение в произвольный момент времени. То есть это замена той функции, которая у нас генерировала next()
, но с особым синтаксисом.
Объявление функции-генератора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function* someGenerator() { ... } function *someGenerator() { ... } //пример генератора function *someGenerator() { function *someGenerator() { let subject = "JavaScript"; yield subject; return "The End of Generator"; } let tempIterator = someGenerator(); //вызываем функцию next() console.log(tempIterator.next());// {value: 'JavaScript', done: false} console.log(tempIterator.next());// {value: 'The End of Generator', done: true} } |
Как работает генератор?
someGenerator()
при вызове функцииnext()
вернёт объект-генератор с данными в виде{value: 'JavaScript', done: false}
- Генераторы используют специальный оператор
yield
для возврата данных. - Оператор
yield
отслеживает предыдущие вызовы и просто продолжает работу функции с последнего места прерывания. - Если мы используем
yield
внутри цикла, то он будет выполняться только один раз, когда будет вызываться методnext()
.
Например, мы можем объявить простой генератор в качестве итератора для пустого объекта:
1 2 3 4 5 6 7 8 9 10 |
const simpleIterable = {}; simpleIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; yield "stop"; }; console.log([...simpleIterable]); // Array [1, 2, 3, "stop"] |
Несколько иначе будет выглядеть итерация объекта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const student = { name: "Lynda Ferrow", age: 20, hobbies: ['swimming', 'biking', 'treveling'], *[Symbol.iterator](){ for(let key in this) { yield {[key]: this[key] } } } } for(let prop of student) { console.log(prop); } //{name: 'Lynda Ferrow'} // {age: 20} // {hobbies: Array(3)} console.log([...student]); //[{name: 'Lynda Ferrow'},{age: 20},{hobbies: Array(3)}] |
В коде выше функция-генератор предназначена для обхода объекта циклом for...of
.
Кроме цикла for...of
, JavaScript использует Symbol.iterator в следующих конструкциях: spread-оператор, yield, destructuring assignment - деструктивное присваивание.
Больше о генераторах вы можете узнать в статье "Использование генераторов в JavaScript на примерах"