A detailed overview of Well-known Symbols





Good day, friends!



Symbol is a primitive data type introduced in ECMAScript2015 (ES6) that allows you to create unique identifiers: const uniqueKey = Symbol ('SymbolName').



You can use symbols as keys for object properties. Symbols that JavaScript handles in a special way are called Well-known Symbols . These characters are used by built-in JavaScript algorithms. For example, Symbol.iterator is used to iterate over the elements of arrays, strings. It can also be used to define your own iterator functions.



These symbols play an important role as they allow you to fine-tune the behavior of objects.



Being unique, using symbols as object keys (instead of strings) makes it easy to add new functionality to objects. At the same time, there is no need to worry about collisions between keys (since each character is unique), which can be a problem when using strings.



This article will focus on well-known symbols with examples of their use.



For the sake of simplicity, the syntax of the well-known Symbol. <name> symbols is in the @@ <name> format. For example, Symbol.iterator is represented as @@ iterator, Symbol.toPrimitive as @@ toPrimitive, etc.



If we say that an object has an @@ iterator method, then the object contains a property called Symbol.iterator, represented by a function: {[Symbol.iterator]: function () {}}.



1. Brief introduction to symbols



A character is a primitive type (such as a number, string, or boolean), unique, and immutable (immutable).



To create a symbol, call the Symbol () function with an optional argument - the name or, more precisely, the description of the symbol:



const mySymbol = Symbol()
const namedSymbol = Symbol('myName')
typeof mySymbol // symbol
typeof namedSymbol // symbol


mySymbol and namedSymbol are primitive symbols. namedSymbol is named 'myName', which is usually used for debugging code.



Each call to Symbol () creates a new unique symbol. Two characters are unique (or special) even if they have the same name:



const first = Symbol()
const second = Symbol()
first === second // false

const firstNamed = Symbol('Lorem')
const secondNamed = Symbol('Lorem')
firstNamed === secondNamed // false


Symbols can be keys of objects. To do this, use the computed property syntax ([symbol]) in an object literal or class definition:



const strSymbol = Symbol('String')

const myObj = {
  num: 1,
  [strSymbol]: 'Hello World'
}

myObj[strSymbol] // Hello World
Object.getOwnPropertyNames(myObj) // ['num']
Object.getOwnPropertySymbols(myObj) // [Symbol(String)]


Symbol properties cannot be retrieved using Object.keys () or Object.getOwnPropertyNames (). To access them, you need to use the special function Object.getOwnPropertySymbols ().



Using well-known symbols as keys allows you to change the behavior of objects.



Well-known symbols are available as non-enumerable, immutable, and non-configurable properties of the Symbol object. To get them, use the dot notation: Symbol.iterator, Symbol.hasInstance, etc.



Here's how to get a list of well-known symbols:



Object.getOwnPropertyNames(Symbol)
// ["hasInstance", "isConcatSpreadable", "iterator", "toPrimitive",
//  "toStringTag", "unscopables", "match", "replace", "search",
//  "split", "species", ...]

typeof Symbol.iterator // symbol


Object.getOwnPropertyNames (Symbol) returns a list of the native properties of the Symbol object, including well-known symbols. Symbol.iterator is of type symbol, of course.



2. @@ iterator, which allows you to make objects iterable (iterable)



Symbol.iterator is perhaps the best known symbol. It allows you to define how an object should be iterated over using a for-of statement or a spread operator (and whether it should be iterated over at all).



Many built-in types such as strings, arrays, maps, sets, or sets are iterable by default because they have an @@ iterator method:



const myStr = 'Hi'
typeof myStr[Symbol.iterator] // function
for (const char of myStr) {
  console.log(char) //     :  'H',  'i'
}
[...myStr] // ['H', 'i']


The variable myStr contains a primitive string that has a Symbol.iterator property. This property contains a function used to iterate over characters in a string.



The object in which the Symbol.iterator method is defined must conform to the iteration (iterator) protocol . More precisely, this method must return an object that conforms to the specified protocol. Such an object must have a next () method that returns {value: <iterator_value>, done: <boolean_finished_iterator>}.



In the following example, we create an iterable myMethods object that allows us to iterate over its methods:



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 () is a function that returns an iterator {next: function () {}}. The myMethods object defines a computed property [Symbol.iterator] with the value methodsIterator. This makes the object iterable using a for-of loop. Object methods can also be obtained using [... myMethods]. Such an object can be converted to an array using Array.from (myMethods).



The creation of an iterable object can be simplified by using a generator function . This function returns a Generator object that conforms to the iteration protocol.



Let's create a Fibonacci class with an @@ iterator method that generates a sequence of Fibonacci numbers:



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] () {} defines a class method - a generator function. The Fibonacci instance follows the brute-force protocol. The spread operator calls the @@ iterator method to create an array of numbers.



If a primitive type or object contains @@ iterator, it can be used in the following scenarios:



  • Looping through elements with for-of
  • Creating an array of elements using the spread operator
  • Creating an array using Array.from (iterableObject)
  • In a yield * expression to pass to another generator
  • In constructors Map (), WeakMap (), Set () and WeakSet ()
  • In static methods Promise.all (), Promise.race (), etc.


You can read more about creating an iterable object here .



3. @@ hasInstance for setting up instanceof



By default, the obj instanceof Constructor operator checks if there is a Constructor.prototype object in the obj prototype chain. Let's consider an example:



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 returns true because obj's prototype is Constructor.prototype (as a result of calling the constructor). instanceof refers to the prototype chain as needed, so obj instanceof Object also returns true.



Sometimes an application needs more stringent instance checking.



Fortunately, we have the ability to define an @@ hasInstance method to change the behavior of instanceof. obj instanceof Type is equivalent to Type [Symbol.hasInstance] (obj).



Let's check if the variables are iterable:



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


The Iterable class contains a static method @@ hasInstance. This method checks if obj is iterable, i.e. whether it contains a Symbol.iterator property. arr and str are iterable, but num is not.



4. @@ toPrimitive to convert an object to a primitive



Use Symbol.toPrimitive to define a property whose value is an object to primitive conversion function. @@ toPrimitive takes one parameter, hint, which can be number, string, or default. hint indicates the type of the return value.



Let's improve the transformation of the array:



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) is a function that converts an array to a primitive based on the hint value. Setting array [Symbol.toPrimitive] to arrayToPrimitive forces the array to use the new transform method. Doing + array calls @@ toPrimitive with a hint value of number. The sum of the array elements is returned. array is $ {array} calls @@ toPrimitive with hint = string. The array is converted to the string '[1, 3, 5]'. Finally 'array elements:' + array uses hint = default to transform. The array is converted to '1,3,5'.



The @@ toPrimitive method is used to represent an object as a primitive type:



  • When using the loose (abstract) equality operator: object == primitive
  • When using the addition / concatenation operator: object + primitive
  • When using the subtraction operator: object - primitive
  • In various situations, converting an object to a primitive: String (object), Number (object), etc.


5. @@ toStringTag to create a standard object description



Use Symbol.toStringTag to define a property whose value is a string that describes the type of the object. The @@ toStringTag method is used by Object.prototype.toString ().



The spec defines the default values ​​returned by Object.prototype.toString () for many types:



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]


These types do not have a Symbol.toStringTag property because the Object.prototype.toString () algorithm evaluates them in a special way.



The property in question is defined in types such as symbols, generator functions, cards, promises, etc. Consider an example:



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]


In the case where the object is not of a standard type group and does not contain the @@ toStringTag property, Object is returned. Of course, we can change this:



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]


An instance of the SimpleClass class does not have a @@ toStringTag property, so Object.prototype.toString () returns [object Object]. The constructor of the MyTypeClass class assigns the @@ toStringTag property to the instance with the value MyType, so Object.prototype.toString () returns [object MyType].



Note that @@ toStringTag was introduced for backward compatibility. Its use is undesirable. It is better to use instanceof (together with @@ hasInstance) or typeof to determine the type of an object.



6. @@ species to create a derived object



Use Symbol.species to define a property whose value is a constructor function used to create derived objects.



The @@ species value of many constructors is the constructors themselves:



Array[Symbol.species] === Array // true
Map[Symbol.species] === Map // true
RegExp[Symbol.species] === RegExp // true


First, notice that a derived object is an object that is returned after performing a specific operation on the original object. For example, calling map () returns a derived object - the result of transforming the elements of the array.



Usually, derived objects refer to the same constructor as the original objects. But sometimes it becomes necessary to define another constructor (perhaps one of the standard classes): this is where @@ species can help.



Suppose we extend the Array constructor with the MyArray child class to add some useful methods. In doing so, we want the constructor of the derived objects of the MyArray instance to be Array. To do this, you need to define a computed property @@ species with an Array value:



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 defines the static computed property Symbol.species. It specifies that the constructor for derived objects should be the Array constructor. Later when filtering the elements of the array, array.filter () returns Array.



The @@ species computed property is used by array and typed array methods such as map (), concat (), slice (), splice (), which return derived objects. Using this property can be useful to extend maps, regular expressions, or promises while preserving the original constructor.



7. Create a regular expression in the form of an object: @@ match, @@ replace, @@ search and @@ split



The string prototype contains 4 methods that take regular expressions as an argument:



  • String.prototype.match (regExp)
  • String.prototype.replace (regExp, newSubstr)
  • String.prototype.search (regExp)
  • String.prototype.split (regExp, limit)


ES6 allows these methods to accept other types as long as they define the corresponding computed properties: @@ match, @@ replace, @@ search, and @@ split.



Curiously, the RegExp prototype contains the specified methods, also defined using symbols:



typeof RegExp.prototype[Symbol.match]   // function
typeof RegExp.prototype[Symbol.replace] // function
typeof RegExp.prototype[Symbol.search]  // function
typeof RegExp.prototype[Symbol.split]   // function


In the following example, we are defining a class that can be used in place of a regular expression:



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) // ['', '']


The Expression class defines @@ match, @@ replace, @@ search, and @@ split methods. Then an instance of this class - sunExp is used in the appropriate methods instead of a regular expression.



8. @@ isConcatSpreadable to convert object to array



Symbol.isConcatSpreadable is a Boolean value indicating that the object can be converted to an array using the Array.prototype.concat () method.



By default, the concat () method retrieves the elements of the array (decomposes the array into the elements of which it consists) when concatenating the arrays:



const letters = ['a', 'b']
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', 'a', 'b']


To concatenate the two arrays, pass letters as an argument to the concat () method. The elements of the letters array become part of the result of the concatenation: ['c', 'd', 'e', ​​'a', 'b'].



In order to prevent the array from being decomposed into elements and to make the array part of the union result as is, the @@ isConcatSpreadable property should be set to false:



const letters = ['a', 'b']
letters[Symbol.isConcatSpreadable] = false
const otherLetters = ['c', 'd']
otherLetters.concat('e', letters) // ['c', 'd', 'e', ['a', 'b']]


In contrast to an array, the concat () method does not decompose array-like objects into elements. This behavior can also be changed with @@ 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 to access properties with with



Symbol.unscopables is a computed property whose proper names are excluded from the object added to the beginning of the scope chain using the with statement. The @@ unscopables property has the following format: {propertyName: <boolean_exclude_binding>}.



ES6 defines @@ unscopables for arrays only. This is done in order to hide new methods that can overwrite variables of the same name in the old code:



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
}


We can access the concat () method in the with body, since this method is not contained in the @@ unscopables property. The entries () method is specified in this property and is set to true, which makes it unavailable inside with.



@@ unscopables was introduced solely for backward compatibility with legacy code using the with statement (deprecated and disallowed in strict mode).



I hope you found something interesting for yourself. Thank you for attention.



All Articles