Если вы когда-либо искали информацию о типе данных 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 на примерах", а про символы - на английском в статье JS Symbol, what the heck?