In this post, I am going to explain what is a Symbol in JavaScript, when and how to use it. At the end of the post are a few exercises. You can check yourself and post solutions in the comment. First few answers I'll code review 😉
So, let's learn something new!
What is a Symbol?
The Symbol is a new primitive data type, introduced with ECMAScript 6. Every symbol created with basic constructor is unique.
const symbol1 = Symbol(); // create first symbol
const symbol2 = Symbol(); // create second symbol
console.log(symbol1 == symbol2); // false
console.log(symbol1 === symbol2); // false
Symbol can be created with description in the constructor. However, it shouldn't be used for any other purpose than debuging. Don't relay on the description!
const niceSymbol = Symbol('Yup 👩💻');
console.log(niceSymbol.description); // Yup 👩💻
Global symbol registry
The symbol can be also created from method for with custom string as the argument. So you can create few instances of symbol with the same value under the hood. After creating symbol by method for, the description is set to the same value as key and the symbol itself is store in global symbol registry
.
const symbol1 = Symbol.for('devto');
const symbol2 = Symbol.for('devto');
console.log(symbol1 == symbol2); // true
console.log(symbol1 === symbol2); // true
console.log(symbol1.description); // devto
Global symbol registry is a location where all symbols created with for method are store across all contexts in the runtime. When you are using for method for the first time, new symbol is attached to the registry. Next time is retrieving from it.
What important, symbols created with for method are distinct from those created with the basic constructor. You can check key for symbol registered globally with method Symbol.keyFor().
const a = Symbol.for('devto'); // globally registered symbol
console.log(Symbol.keyFor(a)); // devto
const b = Symbol(); // local unique symbol
console.log(Symbol.keyFor(b)); // undefined
Symbols don't have string literals. So if you try to explicitly convert a symbol to a string, you get TypeError.
console.log(`${Symbol()}`); // TypeError: Can't convert Symbol to string
Hide access to property
Symbols are commonly used for hiding direct access to properties in objects. With Symbol, you can create a semi-private field.
Props are hidden like pink panther ;) They exist, you can retrieve them with some effort but at first glance, you cannot see and cannot get them!
const tree = {
[Symbol('species')]: 'birch',
[Symbol('height')]: 7.34,
};
console.log(tree);
Without reference to a symbol, you don't have value under which properties are bound to tree.
Enum
Another awesome trick to do with symbols is to create Enum. Enums in another programming languages are types with all possible values. For instance, you may want to have exactly two states of car: DRIVE and IDLE and make sure, car state comes from this enum so you can't use string or numbers.
Example of enum with symbols:
const CarState = Object.freeze({
DRIVE: Symbol('drive'),
IDLE: Symbol('idle'),
});
const car = {
state: CarState.DRIVE
}
if (car.state === CarState.DRIVE) {
console.log('Wroom, wroom 🚙!');
} else if (car.state === CarState.IDLE) {
console.log('Waiting for ya ⏱!');
} else {
throw new Error('Invalid state');
}
// Wroom, wroom 🚙!
Why symbols are so important? Check this example. If you try to mutate object with other value than is behind symbol from enum you will get an error.
// correct way of creating enum - with symbols
const CarState = Object.freeze({
DRIVE: Symbol('drive'),
IDLE: Symbol('idle'),
});
const car = {
state: CarState.DRIVE
}
// you cannot set the state without reference to symbol-based enum
car.state = 'idle';
if (car.state === CarState.DRIVE) {
console.log('Wroom, wroom 🚙!');
} else if (car.state === CarState.IDLE) {
console.log('Waiting for ya ⏱!');
} else {
throw new Error('Invalid state');
}
// Error: Invalid state
Similiar code with strings will be valid, and this is a problem! We want to control all possible states.
// invalid way of creating enum - with other data types
const CarState = Object.freeze({
DRIVE: 'drive',
IDLE: 'idle',
});
const car = {
state: CarState.DRIVE
}
// you can set car state without calling for enum prop, so data may be lost or incorrect
car.state = 'idle';
if (car.state === CarState.DRIVE) {
console.log('Wroom, wroom 🚙!');
} else if (car.state === CarState.IDLE) {
console.log('Waiting for ya ⏱!');
} else {
throw new Error('Invalid state');
}
// Waiting for ya ⏱!
Well-known Symbols
The last thing is a set of well-known symbols. They are built-in properties and are used for different internal object behaviours. This is a little tricky topic. So let say we want to override Symbol. iterator
, the most popular well-known symbol for objects.
Iterator is responsible for behaviour when we are iterating with for of
loop.
const tab = [1, 7, 14, 4];
for (let num of tab) {
console.log(num);
}
// 1
// 7
// 14
// 4
But what if we want to return all numbers but in the Roman numeral and without changing for of loop? We can use Symbol.iterator and override function responsible for returning values.
const tab = [1, 7, 14, 4];
tab[Symbol.iterator] = function () {
let index = 0;
const total = this.length;
const values = this;
return {
next() {
const romanize = num => {
const dec = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
const rom = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
let output = "";
for (let i = 0; i < dec.length; i++) {
while (dec[i] <= num) {
output += rom[i];
num -= dec[i];
}
}
return output;
};
return index++ < total ? {
done: false,
value: romanize(values[index - 1])
} : {
done: true
};
}
};
};
for (let num of tab) {
console.log(num);
}
// I
// VII
// XIV
// IV
Other well-known symbols:
- asyncIterator,
- match,
- replace,
- search,
- split,
- hasInstance,
- isConcatSpreadable,
- unscopables,
- species,
- toPrimitive,
- toStringTag,
That's all about the Symbols! Now time to practice ;)
A1. Create custom logger function, which as one of parameter accept one of enum value and data to log. If an invalid value will be passed, throw an error.
// expected result
log(LogLevel.INFO, 'Important information :O');
log(LogLevel.WARN, 'Houston, We Have a Problem!');
log('info', 'Hi!'); // Error: Invalid log level
A2. By default instance of class returns with ToString() [object Object]
. But you want to return some, more looking nice name! Create a Logger class. Move function from first exercise inside. Override getter for a Symbol.toStringTag
property of the class and return 'Logger' instead.
// expected result
console.log((new Logger()).toString()); // [object Logger]
Want more knowledge and exercises? Follow me on Dev.to and stay tuned!
Top comments (2)
Nice explanation.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.