ref –
- https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/
- https://flaviocopes.com/javascript-symbols/
- https://medium.com/intrinsic/javascript-symbols-but-why-6b02768f4a5c
- https://javascript.info/symbol
Basics info
A symbol represents a unique identifier.
A value of this type can be created using Symbol():
1 2 |
// id is a new symbol let id = Symbol(); |
Upon creation, we can give the symbol a description (also called a symbol name) , mostly useful for debugging purposes:
1 2 |
// id is a symbol with the description "id" let id = Symbol("id"); |
Symbols are guaranteed to be unique. Even if we create many symbols with the same description, they are different values. The description is just a label that doesn’t affect anything.
For instance, here are two symbols with the same description – they are not equal:
1 2 3 4 |
let id1 = Symbol("id"); let id2 = Symbol("id"); alert(id1 == id2); // false |
…But if we used a string “id” instead of a symbol for the same purpose, then there would be a conflict:
1 2 3 4 5 6 7 8 9 |
let user = { name: "John" }; // Our script uses "id" property user.id = "Our id value"; // ...Another script also wants "id" for its purposes... user.id = "Their id value" // Boom! overwritten by another script! |
“Hidden” properties
Symbols allow us to create hidden properties of an object, that no other part of code can accidentally access or overwrite.
For instance, if we’re working with user objects, that belong to third-party code. We’d like to add identifiers to them.
Let’s use a symbol key for it:
1 2 3 4 5 6 7 8 9 10 |
let user = { name: "John" }; let id = Symbol("id"); // key user[id] = 1; // use key to assign value alert( user[id] ); // we can access the value using the symbol as the key |
What’s the benefit of using Symbol(“id”) over a string “id”?
As user objects belong to another code, and that code also works with them, we shouldn’t just add any fields to it. That’s unsafe. Notice the public use of string “name” to change user object value.
1 |
user["name"] = "grover" |
But a symbol cannot be accessed accidentally, the third-party code (hidden away in some module) cannot be accessed by us.
Also, imagine that another script wants to have its own identifier inside ‘user’, for its own purposes. That may be another JavaScript library so that the scripts are completely unaware of each other.
Then that script can create its own Symbol(“id”), like this:
1 2 3 4 |
// let id = Symbol("id"); // another script's id user[id] = "another script's id value"; // use that symbol to put in value |
Now, there will be no conflict between the original and other id values, because symbols are always different, even if they have the same name.
…But if we used a string “id” instead of a symbol for the same purpose, then there would be a conflict:
1 2 3 4 5 6 7 8 9 |
let user = { name: "John" }; // Our script uses "id" property user.id = "Our id value"; // ...Another script also wants "id" for its purposes... user.id = "Their id value" // Boom! overwritten by another script! |
Symbols in a literal
If we want to use a symbol in an object literal {…}, we need square brackets around it.
Like this:
1 2 3 4 5 |
let id = Symbol("id"); let user = { name: "John", [id]: 123 // not "id: 123" }; |
That’s because we need the value from the variable id as the key, not the string “id”.
Symbols are skipped by for…in
For…in iterates through all the indexes of an object/array.
However, symbolic properties do not participate in for..in loop.
For instance:
1 2 3 4 5 6 7 8 9 10 11 |
let id = Symbol("id"); let user = { name: "John", age: 30, [id]: 123 }; for (let key in user) alert(key); // name, age (no symbols) // the direct access by the symbol works alert( "Direct: " + user[id] ); |
Object.keys(user) also ignores them.
That’s a part of the general “hiding symbolic properties” principle.
If another script or a library loops over our object, it won’t unexpectedly access a symbolic property.
In contrast, Object.assign copies both string and symbol properties:
1 2 3 4 5 6 7 8 |
let id = Symbol("id"); let user = { [id]: 123 }; let clone = Object.assign({}, user); alert( clone[id] ); // 123 |
There’s no paradox here. That’s by design. The idea is that when we clone an object or merge objects, we usually want all properties to be copied (including symbols like id).
Other Stuff
object keys could only be strings
If we ever attempt to use a non-string value as a key for an object, the value will be coerced to a string. We can see this feature here:
1 2 3 4 5 6 7 8 |
const obj = {}; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[{}] = 'someobj'; console.log(obj); // { '2': 2, foo: 'foo', bar: 'bar', '[object Object]': 'someobj' } |
A symbol is a primitive which cannot be recreated
They are useful in situations where disparate libraries want to add properties to objects without the risk of having name collisions.
For example:
1 2 3 4 5 6 |
function lib1tag(obj) { obj.id = 42; } function lib2tag(obj) { obj.id = 369; } |
By making use of symbols, each library can generate their required symbols upon instantiation. Then the symbols can be checked on objects, and set to objects, whenever an object is encountered.
1 2 3 4 5 6 7 8 |
const library1property = Symbol('lib1'); function lib1tag(obj) { obj[library1property] = 42; } const library2property = Symbol('lib2'); function lib2tag(obj) { obj[library2property] = 369; } |
For this reason, it would seem that symbols do benefit JavaScript.
However, you may be wondering, why can’t each library simply generate a random string, or use a specially namespaced string, upon instantiation?
1 2 3 4 5 6 7 8 |
const library1property = uuid(); // random approach function lib1tag(obj) { obj[library1property] = 42; } const library2property = 'LIB2-NAMESPACE-id'; // namespaced approach function lib2tag(obj) { obj[library2property] = 369; } |
Well, you’d be right. This approach is actually pretty similar to the approach with symbols. Unless two libraries would choose to use the same property name, then there wouldn’t be a risk of overlap.
Object.keys() does not return symbols
Here is an example of using a symbol as a key within an object:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const obj = {}; // create literal object const sym = Symbol(); // create symbol sym obj[sym] = 'foo'; // use symbol sym as property obj.bar = 'bar'; // use bar as property console.log(obj); // { bar: 'bar' } notice here only bar shows console.log(sym in obj); // true console.log(obj[sym]); // foo, specifically using the symbol will show the value console.log(Object.keys(obj)); // ['bar'], however, you do not see symbols as propertyes in keys |
Notice how they are not returned in the result of Object.keys(). This is, again, for the purpose of backward compatibility. Old code isn’t aware of symbols and so this result shouldn’t be returned from the ancient Object.keys() method.
1 2 3 4 5 6 7 8 9 10 11 12 |
function tryToAddPrivate(o) { o[Symbol('Pseudo Private')] = 42; } const obj = { prop: 'hello' }; // create our literal object tryToAddPrivate(obj); // insert into function so we can add our symbol console.log(Reflect.ownKeys(obj)); // [ 'prop', Symbol(Pseudo Private) ] console.log(obj[Reflect.ownKeys(obj)[1]]); // 42 |
In addition to that, Symbols do not show up on an Object using for in, for of or Object.getOwnPropertyNames – the only way to get the Symbols within an Object is Object.getOwnPropertySymbols:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var fooSym = Symbol('foo'); var myObj = {}; myObj['foo'] = 'bar'; myObj[fooSym] = 'baz'; Object.keys(myObj); // -> [ 'foo' ] Object.getOwnPropertyNames(myObj); // -> [ 'foo' ] Object.getOwnPropertySymbols(myObj); // -> [ Symbol(foo) ] assert(Object.getOwnPropertySymbols(myObj)[0] === fooSym); |
This means Symbols give a whole new sense of purpose to Objects – they provide a kind of hidden under layer to Objects – not iterable over, not fetched using the already existing Reflection tools and guaranteed not to conflict with other properties in the object!
Symbols are completely unique…
By default, each new Symbol has a completely unique value. If you create a symbol (var mysym = Symbol()) it creates a completely new value inside the JavaScript engine. If you don’t have the reference for the Symbol, you just can’t use it.
This also means two symbols will never equal the same value, even if they have the same description.
Well, there’s a small caveat to that – as there is also another way to make Symbols that can be easily fetched and re-used: Symbol.for(). This method creates a Symbol in a “global Symbol registry”. Small aside: this registry is also cross-realm, meaning a Symbol from an iframe or service worker will be the same as one generated from your existing frame:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var myObj = {}; // literal object var fooSym = Symbol.for('foo'); // symbol 'foo' var otherSym = Symbol.for('foo'); // symbol 'foo' // Since Symbol 'foo' was already declared and unique, declaring another gets you that same Symbol. // Now, fooSym and otherSym references the same Symbol myObj[fooSym] = 'baz'; // Symbol('foo') has value 'baz' myObj[otherSym] = 'bing'; // Symbol('foo') has value 'bing' // use symbols for values console.log(myObj[fooSym]) // bing console.log(myObj[otherSym]) // bing console.log(fooSym === otherSym); // true console.log(myObj[fooSym] === 'bing'); // true console.log(myObj[otherSym] === 'bing'); // true |
Cross Realm
Problem:
1 2 3 4 5 6 |
// Cross-Realm iframe = document.createElement('iframe'); iframe.src = String(window.location); document.body.appendChild(iframe); assert.notEqual(iframe.contentWindow.Symbol, Symbol); assert(iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')); |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Some symbols are created const symbol1 = Symbol.for('Geeks'); const symbol2 = Symbol.for(123); const symbol3 = Symbol.for("gfg"); const symbol4 = Symbol.for('789'); // Getting the same symbols if found // in the global symbol registry // otherwise a new created and returned console.log(symbol1); console.log(symbol2); console.log(symbol3); console.log(symbol4); |
main usage
Macros
As a unique value where you’d probably normally use a String or Integer:
Let’s assume you have a logging library, which includes multiple log levels such as logger.levels.DEBUG, logger.levels.INFO, logger.levels.WARN and so on. In ES5 code you’d like make these Strings (so logger.levels.DEBUG === ‘debug’), or numbers (logger.levels.DEBUG === 10). Both of these aren’t ideal as those values aren’t unique values, but Symbols are! So logger.levels simply becomes:
1 2 3 4 5 6 7 |
log.levels = { DEBUG: Symbol('debug'), INFO: Symbol('info'), WARN: Symbol('warn'), }; log(log.levels.DEBUG, 'debug message'); log(log.levels.INFO, 'info message'); |
A place to put metadata values in an Object
You could also use them to store custom metadata properties that are secondary to the actual Object. Think of this as an extra layer of non-enumerability (after all, non-enumerable keys still come up in Object.getOwnProperties). Let’s take our trusty Collection class and add a size reference, which is hidden behind the scenes as a Symbol (just remember that Symbols are not private – and you can – and should – only use them in for stuff you don’t mind being altered by the rest of the app):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var size = Symbol('size'); class Collection { constructor() { this[size] = 0; } add(item) { this[this[size]] = item; this[size]++; } static sizeOf(instance) { return instance[size]; } } var x = new Collection(); assert(Collection.sizeOf(x) === 0); x.add('foo'); assert(Collection.sizeOf(x) === 1); assert.deepEqual(Object.keys(x), ['0']); assert.deepEqual(Object.getOwnPropertyNames(x), ['0']); assert.deepEqual(Object.getOwnPropertySymbols(x), [size]); |