Детальный обзор Well-known Symbols с примерами их использования

Доброго времени суток, друзья!

Символ (Symbol) — это примитивный тип данных, представленный в ECMAScript2015 (ES6), позволяющий создавать уникальные идентификаторы: const uniqueKey = Symbol(‘SymbolName’).

Вы можете использовать символы Детальный обзор Well-known Symbols в качестве ключей для свойств объектов. Символы, которые JavaScript обрабатывает особым образом, называются хорошо известными символами (Well-known Symbols). Эти символы используются встроенными алгоритмами JavaScript. Например, Symbol.iterator используется для перебора элементов массивов, строк. Его также можно использовать для определения собственных функций-итераторов.

Данные символы играют важную роль, поскольку позволяют осуществлять тонкую настройку поведения объектов. Возможно будет интересно 10 полезных приёмов для JavaScript-программистов.

Будучи уникальными, использование символов в качестве ключей объектов (вместо строк) позволяет легко добавлять объектам новый функционал. При этом, не нужно беспокоиться о возникновении коллизий между ключами (поскольку каждый символ уникален), что может стать проблемой при использовании строк.

В данной статье речь пойдет о хорошо известных Детальный обзор Well-known Symbols с примерами их использования.

В целях небольшого упрощения синтаксис хорошо известных символов Symbol.<name> представлен в формате @@<name>. Например, Symbol.iterator представлен как @@iterator, Symbol.toPrimitive — как @@toPrimitive и т.д.

Если мы говорим о том, что объект имеет метод @@iterator, значит, объект содержит свойство под названием Symbol.iterator, представленное функцией: { [Symbol.iterator]: function() { } }.

1. Краткое введение в символы

Символ — это примитивный тип (такой как число, строка или логическое значение), уникальный и неизменяемый (иммутабельный).

Для создания символа необходимо вызвать функцию Symbol() с опциональным аргументом — названием или, точнее, описанием символа:

mySymbol и namedSymbol — это символы-примитивы. namedSymbol имеет название ‘myName’, которое, обычно, используется в целях отладки кода.

При каждом вызове Symbol() создается новый уникальный символ. Два символа являются уникальными (или особыми), даже если имеют одинаковые названия:

Символы могут быть ключами объектов. Для этого в объектном литерале или определении класса необходимо использовать синтаксис вычисляемых свойств ([symbol]):

Свойства-символы не могут быть получены с помощью Object.keys() или Object.getOwnPropertyNames(). Для доступа к ним нужно использовать специальную функцию Object.getOwnPropertySymbols().

Использование хорошо известных символов в качестве ключей позволяет изменять поведение объектов.

Хорошо известные символы доступны как неперечисляемые, неизменяемые и ненастраиваемые свойства объекта Symbol. Для их получения следует использовать точечную нотацию: Symbol.iterator, Symbol.hasInstance и т.д.

Вот как можно получить список хорошо известных символов:

Object.getOwnPropertyNames(Symbol) возвращает список собственных свойств объекта Symbol, включая хорошо известные символы. Разумеется, типом Symbol.iterator является symbol.

2. @@iterator, позволяющий делать объекты перебираемыми (итерируемыми)

Symbol.iterator — это, пожалуй, наиболее известный символ. Он позволяет определять, как объект должен перебираться с помощью инструкции for-of или spread-оператора (и должен ли он перебираться вообще).

Многие встроенные типы, такие как строки, массивы, карты (maps), наборы или коллекции (sets) являются итерируемыми по умолчанию, поскольку у них есть метод @@iterator:

Переменная myStr содержит примитивную строку, у которой имеется свойство Symbol.iterator. Данное свойство содержит функцию, используемую для перебора символов строки.

Объект, в котором определяется метод Symbol.iterator, должен соответствовать протоколу перебора (итератора). Точнее, данный метод должен возвращать объект, соответствующий указанному протоколу. У такого объекта должен быть метод next(), возвращающий { value: <iterator_value>, done: <boolean_finished_iterator> }.

В следующем примере мы создаем итерируемый объект myMethods, позволяющий перебирать его методы:

methodsIterator() — это функция, которая возвращает итератор { next: function() { } }. В объекте myMethods определяется вычисляемое свойство [Symbol.iterator] со значением methodsIterator. Это делает объект перебираемым с помощью цикла for-of. Методы объекта также можно получить с помощью […myMethods]. Такой объект можно преобразовать в массив с помощью Array.from(myMethods).

Создание итерируемого объекта можно упростить с помощью функции-генератора. Данная функция возвращает объект Generator, соответствующий протоколу перебора.

Создадим класс Fibonacci с методом @@iterator, генерирующим последовательность чисел Фибоначчи:

*[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. Рассмотрим пример:

obj instanceof Constructor возвращает true, поскольку прототипом obj является Constructor.prototype (как результат вызова конструктора). instanceof при необходимости обращается к цепочке прототипов, поэтому obj instanceof Object также возвращает true.

Иногда в приложении требуется более строгая проверка экземпляров.

К счастью, у нас имеется возможность определить метод @@hasInstance для изменения поведения instanceof. obj instanceof Type является эквивалентом Type[Symbol.hasInstance](obj).

Давайте проверим, являются ли переменные итерируемыми:

Класс Iterable содержит статический метод @@hasInstance. Данный метод проверяет, является ли obj перебираемым, т.е. содержит ли он свойство Symbol.iterator. arr и str являются итерируемыми, а num нет.

4. @@toPrimitive для преобразования объекта в примитив

Используйте Symbol.toPrimitive для определения свойства, значением которого является функция преобразования объекта в примитив. @@toPrimitive принимает один параметр — hint, которым может быть number, string или default. hint указывает на тип возвращаемого значения.

Усовершенствуем преобразование массива:

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() по умолчанию, для многих типов:

Эти типы не имеют свойства Symbol.toStringTag, поскольку алгоритм Object.prototype.toString() оценивает их особым образом.

Рассматриваемое свойство определяется в таких типах, как символы, функции-генераторы, карты, промисы и др. Рассмотрим пример:

В случае, когда объект не относится к группе со стандартным типом и не содержит свойства @@toStringTag, возвращается Object. Разумеется, мы можем это изменить:

Экземпляр класса SimpleClass не имеет свойства @@toStringTag, поэтому Object.prototype.toString() возвращает [object Object]. В конструкторе класса MyTypeClass экземпляру присваивается свойство @@toStringTag со значением MyType, поэтому Object.prototype.toString() возвращает [object MyType].

Обратите внимание, что @@toStringTag был введен в целях обеспечения обратной совместимости. Его использование нежелательно. Для определения типа объекта лучше использоваить instanceof (совместно с @@hasInstance) или typeof.

6. @@species для создания производного объекта

Детальный обзор Well-known Symbols

Используйте Symbol.species для определения свойства, значением которого является функция-конструктор, используемая для создания производных объектов.

Значением @@species многих конструкторов являются сами конструкторы:

Во-первых, обратите внимание, что производным называется объект, возвращаемый после совершения определенной операции с исходным объектом. Например, вызов map() возвращает производный объект — результат преобразования элементов массива.

Обычно, производные объекты ссылаются на тот же конструктор, что и исходные объекты. Но порой возникает необходимость в определении другого конструктора (возможно, одного из стандартных классов): вот где может помочь @@species.

Предположим, что мы расширяем конструктор Array с помощью дочернего класса MyArray для добавления некоторых полезных методов. При этом мы хотим, чтобы конструктором производных объектов экземпляра MyArray был Array. Для этого необходимо определить вычисляемое свойство @@species со значением Array:

В MyArray определено статическое вычисляемое свойство Symbol.species. Оно указывает, что конструктором производных объектов должен быть конструктор Array. Позже при фильтрации элементов массива array.filter() возвращает Array.

Вычисляемое свойство @@species используется методами массивов и типизированных массивов, такими как map(), concat(), slice(), splice(), возвращающими производные объекты. Использование данного свойства может быть полезным для расширения карт, регулярных выражений или промисов с сохранением оригинального конструктора Детальный обзор Well-known Symbols.

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 содержит указанные методы, также определенные с помощью символов:

В следующем примере мы определяем класс, который может использоваться вместо регулярного выражения:

В классе Expression определяются методы @@match, @@replace, @@search и @@split. Затем экземпляр этого класса — sunExp используется в соответствующих методах вместо регулярного выражения.

8. @@isConcatSpreadable для преобразования объекта в массив

Symbol.isConcatSpreadable представляет собой логическое значение, указывающее на возможность преобразования объекта в массив с помощью метода Array.prototype.concat().

По умолчанию, метод concat() извлекает элементы массива (раскладывает массив на элементы, из которых он состоит) при объединении массивов:

Для объединения двух массивов letters передается в качестве аргумента методу concat(). Элементы массивы letters становятся частью результата объединения: [‘c’, ‘d’, ‘e’, ‘a’, ‘b’].

Для того, чтобы предотвратить разложение массива на элементы и сделать массив частью результата объединения как есть, свойству @@isConcatSpreadable следует присвоить значение false:

В противоположность массиву, метод concat() не раскладывает на элементы массивоподобные объекты. Это поведение также можно изменить с помощью @@isConcatSpreadable:

 

9. @@unscopables для доступа к свойствам посредством with

Symbol.unscopables — это вычисляемое свойство, собственные имена свойств которого исключаются из объекта, добавляемого в начало цепочки областей видимости с помощью инструкции with. Свойство @@unscopables имеет следующий формат: { propertyName: <boolean_exclude_binding> }.

ES6 определяет @@unscopables только для массивов. Это сделано в целях сокрытия новых методов, которые могут перезаписать одноименные переменные в старом коде:

Мы можем получить доступ к методу concat() в теле with, поскольку данный метод не содержится в свойстве @@unscopables. Метод entries() указан в этом свойстве и имеет значение true, что делает его недоступным внутри with.

@@unscopables был введен исключительно для обеспечения обратной совместимости со старым кодом, где используется инструкция with (которая признана устаревшей и запрещена в строгом режиме).

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.

источник