JS Symbol and Well-known symbols
EcmaScript 6 gave us something new and special with name Symbol but we don’t wanna use it and the main reason is:
“because we do not know how exactly use it and why we need it”
From my point of view a lot of JavaScript developers (not all) are underestimate all power that gives Symbol and today we are going to spend some time for investigation in order to understand all mystic parts of Symbol.
I do not know how it was in your case but from the beginning of ES6 I didn’t use Symbol as much as I should, because it was like a black box for me, so I decided to explore symbol possibilities more deeply.
In this Article:
- Basic usage and main definition
- Global Symbols and static functions .for(key) .keyFor(symbol)
- Well-known symbols
- Symbol.iterator with Iterable and Iterator protocol
- Symbol.search
- Symbol.toStringTag
- Symbol.unscopables
- Symbol.isConcatSpreadable
- Symbol.toPrimitive
- Symbol.prototype
- Symbol.split
- Symbol.hasInstance
- Symbol.match
- Symbol.species
Basic usage and main definition.
Let’s firstly consider what says MDN Web Docs about Symbol:
The
Symbol()
function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()
".
So….
- We have a new factory function Symbol().
- We have new primitive type “symbol”.
- We do not need to use new keyword in order to get new Symbol.
But we do not believe we want to check:
let symbolValue = Symbol(); // here we are creating new symbol using Symbol() functionconsole.log(typeof symbolValue); // will be "symbol"// and now we should try to use new keyword in order to be sure that we can't create new Symbol in this waytry {
let symbolWithNewKeyword = new Symbol();
} catch(err) {
console.log(err.name); // "TypeError"
console.log(err.message); // "Symbol is not a constructor"
} // on this level we should understand and interpret Symbol() more as function but not as constructor
A few more important points:
- Symbol can take as parameter key.
- Every symbol value returned from Symbol is unique (currently we are not talking about Symbol.for(name)).
let symbol1 = Symbol('Some Description')
, symbol2 = Symbol('Some Description');console.log(symbol1); // "Symbol(Some Description)"console.log(symbol1 === symbol2); // here we will get `false`
Global Symbols and static functions .for(key) .keyFor(symbol)
Symbol provides two static functions .for(key) and .keyFor(symbol). Let’s take a look on them:
.for(key) function - method searches for existing symbols in a runtime-wide symbol registry with the given key and returns it if found. Otherwise a new symbol gets created in the global symbol registry with this key.
let symbol1 = Symbol.for('globalSymbol')
, symbol2 = Symbol.for('globalSymbol');console.log(symbol1 === symbol2); // true
console.log(Symbol.for('globalSymbol') === Symbol.for('globalSymbol')); // true
great, now we can create global symbol but what about .keyFor(symbol)?
.keyFor(symbol) - method retrieves a shared symbol key from the global symbol registry for the given symbol.
let globalSymbol = Symbol.for('foo'); // global symbolconsole.log(Symbol.keyFor(globalSymbol)); // "foo"
what we need to understand and take in account is that Symbol.keyFor(symbol) returns undefined for local symbol:
let localSymbol = Symbol('Hello');console.log(Symbol.keyFor(localSymbol)); // undefined
Well-known symbols
We can have not only custom but as well built in symbols representing internal language behaviors. And they are called well-known symbols. We will explore them one by one.
Symbol.iterator with Iterable and Iterator protocol
Iterable protocol - allows JavaScript objects to define or customize their iteration behavior, such as what values are looped over in a for..of
construct (for…in is not relevant to iteration protocols).
Iterator protocol - The iterator protocol defines a standard way to produce a sequence of values. But here object should have a .next() method that will return object with two properties:
done - should return true if the iterator is past the end of the iterated sequence.
value - any value returned by the iterator.
Here is an example of Symbol.iterator:
let range = {
from: 1,
to: 10
}range[Symbol.iterator] = function() {let current = this.from
, last = this.to;return {
next() {
return (current <= last)
? { done: false, value: current++ }
: { done: true };
}
};
};for (let num of range) {
console.log(num); // 1, 2, 3, 4, 5, ...
}
or we can use built in with the following construction:
let range = {
from: 1,
to: 10,
[Symbol.iterator]: function() {
return this;
},
next() {
this.current = this.current || this.from;
return (this.current <= this.to)
? { done: false, value: this.current++ }
: { done: true };
}
}for (let num of range) {
console.log(num); // 1, 2, 3, 4, 5, ...
}
Result will be the same.
But what is more interesting is that we can use as well spread operator (if you do not know what is spread operator you can find more information here).
console.log(...range); // 1, 2, 3, 4, 5, ...
Symbol.search
Definition: Specifies the method that returns the index within a string that matches the regular expression.
First we need to define some mock data:
let user1 = {id: 1, name: 'Liza'}
, user2 = {id: 2, name: 'John'};
Example 1: Using class construction
class IsJohn {
[Symbol.search](valueToCheck) {
return valueToCheck === 'John';
}
}console.log('Liza'.search(new IsJohn())); // false
console.log('John'.search(new IsJohn())); // true
Example 2: Using static variables on constructor function
function IsJohn() {}
IsJohn[Symbol.search] = (valueToCheck) => valueToCheck === 'John';
let user1 = {id: 1, name: 'Liza'}
, user2 = {id: 2, name: 'John'}console.log('Liza'.search(IsJohn)); // false
console.log('John'.search(IsJohn)); // true
Example 3: Using Prototype construction
function IsJohn() {}
IsJohn.prototype[Symbol.search] = valueToCheck => valueToCheck === 'John';console.log('Liza'.search(new IsJohn)); // false
console.log('John'.search(new IsJohn)); // true
Example 4: Using pure object
let IsJohn = {
[Symbol.search]: (valueToCheck) => valueToCheck === 'John'
}console.log('Liza'.search(IsJohn)); // false
console.log('John'.search(IsJohn)); // true
Symbol.toStringTag
Definition: Is a string valued property that is used in the creation of the default string description of an object.
function classOf(val) {
return Object.prototype.toString.call(val).slice(8, -1);
}class CustomClass {
get [Symbol.toStringTag]() {
return 'NewCustomClassName';
}
}console.log(classOf(new CustomClass())); // "NewCustomClassName"
console.log(''+new CustomClass()); // "[object NewCustomClassName]
Symbol.unscopables
Definition: Is used to specify an object value of whose own and inherited property names are excluded from the with
environment bindings of the associated object.
let obj = {
prop1: 'Hello'
};obj[Symbol.unscopables] = {
prop1: true
};with (obj) {
console.log(prop1); // Error: prop1 is not defined
}
Symbol.isConcatSpreadable
Definition: Is used to configure if an object should be flattened to its array elements when using the Array.prototype.concat method.
let arr1 = ['a', 'b', 'c'];
let arr2 = [1, 2, 3];
let concatenatedArr = arr1.concat(arr2);console.log(concatenatedArr); // ["a", "b", "c", 1, 2, 3]arr1[Symbol.isConcatSpreadable] = false;
concatenatedArr = arr1.concat(arr2);console.log(concatenatedArr); // [["a", "b", "c"], 1, 2, 3]
Symbol.toPrimitive
Definition: Specifies a function valued property that is called to convert an object to a corresponding primitive value.
let date = {
value: new Date(),
days: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
[Symbol.toPrimitive](hint) {
if (hint === 'number') return +this.value;
if (hint === 'string') return this.days[this.value.getDay()];
return this.value.toString();
}
}console.log(+date); // 1527617604951
console.log(`${date}`); // "Tuesday"
console.log(date+''); // "Tue May 29 2018 21:13:24 GMT+0300 (EEST)"
Symbol.prototype
Definition: Represents the prototype for the Symbol constructor.
Symbol.prototype.isGlobal = function() {
let current = this.valueOf();
let key = Symbol.keyFor(current);
return Symbol.for(key) === current;
}let globalSymbol = Symbol.for('s1');
let localSymbol = Symbol('s2');console.log(globalSymbol.isGlobal()); // true
console.log(localSymbol.isGlobal()); // false
By the way if we already made upgrades to Symbol.prototype we can do as well something for static usage (for example Symbol have static function Symbol.for(desc) and Symbol.keyFor(symbol)).
In previous example in order to use our .isGlobal() function we had to create new instance. Static means that there is no need to create new instance. It should be available from class.
In the next example we will implement own .for(desc) method in order give some ideas but it will be named as factory (cause we don’t need to override native .for(key) function), so here it comes…..
Symbol.factory = (function() {
let symbols = [];
return function(...args) {
if(args.length < 1) throw 'Symbol.factory function takes at least one argument';
let symbolObj = symbols.find(sym => sym.hasOwnProperty(name));
if(!symbolObj) {
symbolObj = {[name]: Symbol(name)};
symbols.push(symbolObj);
}
return symbolObj[name];
}
})();
Testing custom Symbol.factory(name) function:
let symbol = Symbol.factory('Hi');
console.log(symbol === Symbol.factory('Hi')); // true
console.log(Symbol.factory('Hi') === Symbol.factory('Hi')); // true
Testing build in Symbol.for(desc) function:
let symbol2 = Symbol.for('Hi');
console.log(symbol2 === Symbol.for('Hi')); // true
console.log(Symbol.for('Hi') === Symbol.for('Hi')); // true
There is a lot awesome thing that you can do with Symbol, for example:
- you can implement own built in factory that will manage different repositories of Symbols in order to implement own custom protocols (something similar is iteration protocols) that can make your life easier.
Symbol.split
Definition: specifies the method that splits a string at the indices that match a regular expression.
/* Helper functions */
function compact(source) {
return source.filter(elem => elem);
};function pipe(...fns) {
return (...vals) =>
fns.reduce((accumulator, fn) =>
fn(accumulator), fns.shift()(...vals));
}function upperFirst(str = '') {
let firstChar = str.charAt(0).toUpperCase()
, restOfString = str.substring(1, str.length);
return `${firstChar}${str.substring(1, str.length)}`;
}function words(strValue) {
let r = (str, ...args) => str.replace(...args);
return pipe(
s => r(s, /[^-a-z-A-Z0-9]+/g, ','),
s => r(s, /([a-z])([A-Z])/g, '$1 $2'),
s => r(s, /([A-Z])([a-z])/g, ' $1$2'),
s => r(s, /\ +/g, ','),
s => s.split(','),
compact
)(strValue);
};/* Symbol.split example */
class SnakeCase {
[Symbol.split](value) {
return words(value).join('_').toLowerCase();
}
}class KebabCase {
[Symbol.split](value) {
return words(value).join('-').toLowerCase();
}
}class CamelCase {
[Symbol.split](value) {
return words(value).reduce((accumulator, value) =>
(accumulator)
? `${accumulator}${upperFirst(value.toLowerCase())}`
: value.toLowerCase(), '');}
}console.log('JohnDoe Hi_You'.split(new SnakeCase())); // "john_doe_hi_you"
console.log('JohnDoe Hi_You'.split(new KebabCase())); // "john-doe-hi-you"
console.log('JohnDoe Hi_You'.split(new CamelCase())); // "johnDoeHiYou"console.log('JohnDoe Hi_You'.split(new SnakeCase)); // "john_doe_hi_you"
console.log('JohnDoe Hi_You'.split(new KebabCase)); // "john-doe-hi-you"
console.log('JohnDoe Hi_You'.split(new CamelCase)); // "johnDoeHiYou"
Symbol.hasInstance
Definition: Is used to determine if a constructor object recognizes an object as its instance.
It is just awesome and I will try to explain why. Unlike TypeScript, JavaScript dosn’t have a built in interfaces but we have ability to simulate something similar with Symbol.hasInstance and Duck Typing. Duck Typing means:
If it walks like a duck and it quacks like a duck, then it must be a duck.
Let’s consider the following example:
Here we are simulating interface:
class Flying { // expects fly implementation
static [Symbol.hasInstance](instance) {
return typeof instance.fly === 'function';
}
}
Firstly we create class Bird and Airplane and they are implementing Flying interface. Secondly we are creating Car class that dosn’t have that ability.
class Bird {
fly() {
console.log('Bird: I\'m flying!');
}
}class Airplane {
fly() {
console.log('Airplane: I\'m flying!');
}
}class Car {
drive() {
console.log('Car: I\'m driving!');
}
}
Testdrive:
let car = new Car();
let airplane = new Airplane();
let bird = new Bird();console.log(bird instanceof Flying); // true
console.log(car instanceof Flying); // false
console.log(airplane instanceof Flying); // true
More advanced testdrive:
// helper function
function implements(instance, clazz) {
return instance instanceof clazz;
}// demo function
function flyForMe(instance) {
if(!implements(instance, Flying)) throw new Error('Instance should implement Flying interface');
instance.fly();
}flyForMe(bird); // "Bird: I'm flying!"
flyForMe(airplane); // "Airplane: I'm flying!"try {
flyForMe(car);
} catch(err) {
console.log(err.message); // "Instance should implement Flying interface"
}
Symbol.match
Definition: specifies the matching of a regular expression against a string.
let regexp = /hello/;regexp[Symbol.match] = false;console.log('/hello/'.startsWith(regexp));
console.log('/hi/'.endsWith(regexp));
Symbol.species
Definition: Specifies a function-valued property that the constructor function uses to create derived objects.
class Array1 extends Array {
static get [Symbol.species]() {
return this;
}
}class Array2 extends Array {
static get [Symbol.species]() {
return Array;
}
}console.log(new Array1().map(function(){}) instanceof Array1); // true
console.log(new Array2().map(function(){}) instanceof Array2); // false
console.log(new Array2().map(function(){}) instanceof Array); // true
And that is all :)
Conclusion
Thank you guys for reading. I hope you enjoyed it and learned some new stuff related to JavaScript. Please subscribe and press ‘Clap’ button if you like this article.