Symbolic primitives are one of the innovations of the ES6 standard that brought some valuable features to JavaScript. Symbols represented by the Symbol data type are especially useful when used as identifiers of object properties. In connection with such a scenario of their application, the question arises of what they can do, what strings cannot.

In the material, the translation of which we publish today, we will focus on the Symbol data type in JavaScript. We begin with an overview of some of the features of JavaScript, which need to be guided in order to deal with the characters.
Preliminary Information
In JavaScript, in fact, there are two kinds of values. The first type is the primitive values, the second is the object ones (functions are also included). Primitive values include simple data types like numbers (this includes everything from integers to floating-point numbers,
Infinity
and
NaN
values), logical values, strings,
undefined
values and
null
values. Notice that although
typeof null === 'object'
turns out
true
when checking the type,
null
is a primitive value.
')
Primitive values are immutable. They can not be changed. Of course, in a variable that stores a primitive value, you can write something new. For example, here the new value is written to the variable
x
:
let x = 1; x++;
But at the same time there is no change (mutation) of the primitive numerical value
1
.
In some languages, for example, in C, there are concepts of passing arguments of functions by reference and by value. JavaScript also has something similar. How exactly data management is organized depends on their type. If a primitive value represented by some variable is passed to the function, and then it is changed in this function, the value stored in the original variable does not change. However, if you pass an object value represented by a variable to the function and modify it, then what is stored in this variable changes.
Consider the following example:
function primitiveMutator(val) { val = val + 1; } let x = 1; primitiveMutator(x); console.log(x);
Primitive values (with the exception of the mysterious
NaN
, which is not equal to itself) always turn out to be equal to other primitive values that look the same as themselves. For example:
const first = "abc" + "def"; const second = "ab" + "cd" + "ef"; console.log(first === second);
However, the construction of object values that look alike does not lead to the fact that entities are obtained, comparing which will reveal their equality with each other. You can check it like this:
const obj1 = { name: "Intrinsic" }; const obj2 = { name: "Intrinsic" }; console.log(obj1 === obj2);
Objects play a fundamental role in javascript. They are applied literally everywhere. For example, they are often used as collections of the key / value type. But before the appearance of the
Symbol
data type, only strings could be used as object keys. This was a serious limitation on the use of objects in the form of collections. When attempting to assign a non-string value as an object key, this value was cast to a string. You can verify this by:
const obj = {}; obj.foo = 'foo'; obj['bar'] = 'bar'; obj[2] = 2; obj[{}] = 'someobj'; console.log(obj);
By the way, although this takes us a little away from the topic of symbols, we would like to note that the
Map
data structure was created to allow using key / value data stores in situations where the key is not a string.
What is a character?
Now that we’ve figured out the features of primitive values in JavaScript, we’re finally ready to start talking about characters. A symbol is a unique primitive value. If you approach the characters from this position, you can see that the characters in this regard are similar to objects, since the creation of several instances of characters will lead to the creation of different values. But symbols, moreover, are immutable primitive values. Here is an example of working with symbols:
const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2);
When creating an instance of a character, you can use the optional first string argument. This argument is a character description that is intended for use in debugging. This value does not affect the symbol itself.
const s1 = Symbol('debug'); const str = 'debug'; const s2 = Symbol('xxyy'); console.log(s1 === str);
Symbols as object property keys
Symbols can be used as property keys for objects. It is very important. Here is an example of using them in this quality:
const obj = {}; const sym = Symbol(); obj[sym] = 'foo'; obj.bar = 'bar'; console.log(obj);
Note that the keys specified by the characters are not returned when the
Object.keys()
method is
Object.keys()
. The code written before the appearance of symbols in JS knows nothing about them, as a result, information about the keys of objects represented by characters should not be returned by the ancient
Object.keys()
method.
At first glance, it may seem that the above-described features of symbols allow them to be used to create private properties of JS objects. In many other programming languages, you can create hidden properties of objects using classes. The lack of this feature has long been considered one of the disadvantages of JavaScript.
Unfortunately, the code that works with objects can easily access their string keys. The code can access the keys specified by the characters, moreover, even if the code from which the object is operated with does not have access to the corresponding character. For example, using the
Reflect.ownKeys()
method you can get a list of all the keys of an object, and those that are strings, and those that are symbols:
function tryToAddPrivate(o) { o[Symbol('Pseudo Private')] = 42; } const obj = { prop: 'hello' }; tryToAddPrivate(obj); console.log(Reflect.ownKeys(obj));
Please note that work is currently under way to equip classes with the ability to use private properties. This feature is called
Private Fields . True, it does not affect absolutely all objects, referring only to those of them that are created on the basis of previously prepared classes. Private field support is already available in Chrome browser version 72 and later.
Preventing collisions of object property names
Symbols, of course, do not add to JavaScript the ability to create private properties of objects, but they are a valuable innovation of the language for other reasons. Namely, they are useful in situations when certain libraries need to add properties to the objects described beyond their limits, and at the same time not to be afraid of the collision of the names of the properties of the objects.
Consider an example in which two different libraries want to add metadata to an object. It is possible that both libraries need to be equipped with an object with certain identifiers. If you simply use something like the two-letter
id
string for the name of such a property, you might encounter a situation where one library overwrites the property specified by the other.
function lib1tag(obj) { obj.id = 42; } function lib2tag(obj) { obj.id = 369; }
If we use symbols in our example, then each library can generate, at initialization, the symbols it needs. These symbols can then be used to assign properties to objects and to access these properties.
const library1property = Symbol('lib1'); function lib1tag(obj) { obj[library1property] = 42; } const library2property = Symbol('lib2'); function lib2tag(obj) { obj[library2property] = 369; }
Just looking at such a script, you can feel the benefits of the appearance of characters in JavaScript.
However, there may be a question regarding the use of libraries for property names of objects, random strings or strings with a complex structure, including, for example, the name of the library. Such strings can form something like namespaces for identifiers used by libraries. For example, it might look like this:
const library1property = uuid();
In general, you can do so. Such approaches, in fact, are very similar to what happens when using symbols. And if, using random identifiers or namespaces, a pair of libraries does not generate, by chance, the same property names, then there will be no problems with the names.
An astute reader would say now that the two approaches to naming properties of objects are not completely equivalent. Names of properties that are randomly generated or using namespaces have a drawback: the corresponding keys are very easy to detect, especially if the code is iterating over the keys of objects or serializing them. Consider the following example:
const library2property = 'LIB2-NAMESPACE-id';
If in this situation a symbol was used for the key name, then the JSON representation of the object would not contain the symbol value. Why is this so? The fact is that the fact that a new data type has appeared in JavaScript does not mean that the changes have been made to the JSON specification. JSON supports, as keys of properties of objects, only strings. When the object is serialized, no attempt is made to present the characters in any particular form.
The problem of getting property names into the JSON representation of objects can be solved by using
Object.defineProperty()
:
const library2property = uuid();
String keys that are “hidden” by setting their
enumerable
handle to
false
behave in much the same way as keys represented by characters. Both are not displayed when
Object.keys()
called, and both can be detected using
Reflect.ownKeys()
. Here's what it looks like:
const obj = {}; obj[Symbol()] = 1; Object.defineProperty(obj, 'foo', { enumberable: false, value: 2 }); console.log(Object.keys(obj));
Here, I must say, we almost recreated the possibilities of symbols using other means of JS. In particular, both keys represented by symbols and hidden keys do not fall into the JSON representation of the object. Both can be found by calling the
Reflect.ownKeys()
method. As a result, those and others can not be called truly private. If we assume that some random values or library namespaces are used to generate key names, this means that we have got rid of the risk of name collisions.
However, there is one small difference between using symbol names and names created using other mechanisms. Since the strings are immutable, and the characters are guaranteed unique, there is always the possibility that someone, going through all possible combinations of characters in a string, will cause a name collision. From a mathematical point of view, this means that the characters really give us a valuable opportunity, which is absent in lines.
In Node.js, when examining objects (for example, using
console.log()
), if an object method with the name
inspect
detected, then this method is used to get a string representation of the object and then display it on the screen. It is easy to understand that absolutely everyone cannot take this into account, so this behavior of the system can lead to a call to the method of an
inspect
object, which is intended for solving problems that are not related to the formation of a string representation of an object. This feature is deprecated in Node.js 10; in version 11, methods with a similar name are simply ignored. Now, to implement this feature, the
require('util').inspect.custom
. And this means that no one will ever be able to inadvertently disrupt the operation of the system by creating an object method called
inspect
.
Imitation of private properties
Here is an interesting approach that can be used to simulate the private properties of objects. This approach involves the use of another modern JavaScript feature - proxy objects. Such objects serve as wrappers for other objects that allow the programmer to interfere with the actions performed on these objects.
Proxy objects offer many ways to intercept actions performed on objects. We are interested in the ability to control the operations of reading the keys of an object. In detail about the proxy objects, we will not go deep here. If you are interested in them - take a look at
this publication.
We can use a proxy to control which properties of an object are visible from the outside. In this case, we want to create a proxy that hides two properties known to us. One has the string name
_favColor
, and the second is represented by the character written to the variable
favBook
:
let proxy; { const favBook = Symbol('fav book'); const obj = { name: 'Thomas Hunter II', age: 32, _favColor: 'blue', [favBook]: 'Metro 2033', [Symbol('visible')]: 'foo' }; const handler = { ownKeys: (target) => { const reportedKeys = []; const actualKeys = Reflect.ownKeys(target); for (const key of actualKeys) { if (key === favBook || key === '_favColor') { continue; } reportedKeys.push(key); } return reportedKeys; } }; proxy = new Proxy(obj, handler); } console.log(Object.keys(proxy));
Cope with the property, whose name is represented by the string
_favColor
, easy: just read the source code. Dynamic keys (like the uuid-keys that we saw above) can be picked up by brute force. But without a reference to the symbol, you cannot access the
Metro 2033
value from the
proxy
object.
Here it should be noted that in Node.js there is one feature that violates the privacy of proxy objects. This feature does not exist in the language itself, so it is not relevant for other JS runtimes such as the browser. The fact is that this feature allows you to access an object hidden behind a proxy object, if you have access to a proxy object. Here is an example demonstrating the ability to bypass the mechanisms shown in the previous code snippet:
const [originalObject] = process .binding('util') .getProxyDetails(proxy); const allKeys = Reflect.ownKeys(originalObject); console.log(allKeys[3]);
Now, to prevent this feature from being used in a particular Node.js instance, you need to either modify the global
Reflect
object, or bind the
util
process. However, this is another task. If you're interested, take a look at
this JavaScript-based API security post.
Results
In this article, we talked about the
Symbol
data type, what possibilities it gives JavaScript developers, and what existing language mechanisms can be used to simulate these capabilities.
Dear readers! Do you use symbols in your JavaScript projects?
