📜 ⬆️ ⬇️

Exploring JavaScript Symbols. Use of symbols

This is the second article in a series on characters and their use in JavaScript. The previous part: “Symbol is a new data type in JavaScript” .

With the advent of symbols, the Object been extended with one method that allows you to get all the symbols of an object:

  var role = Symbol('role'); var score = Symbol('score'); var id = 100; var name = 'Moderator'; var user = { id: id, name: name }; user[role] = 'admin'; user[score] = 50000; Object.getOwnPropertySymbols( user ); // [Symbol(role), Symbol(score)] 

The presence of this method makes it impossible for us to create truly private properties.

Here, for example, how to get a user role:
')
  var userSymbols = Object.getOwnPropertySymbols( user ); var roleSymbol = userSymbols[0]; user[ roleSymbol ]; // 'admin' 

If we take into account the fact that it is not possible to hide data on the client side, then this function should be considered as an auxiliary, which will be needed to get a list of characters, similarly to the situation when we get a list of all object property names using Object.getOwnPropertyNames .

I also want to note that the ECMAScript 6 specification has added a new Reflect object which allows reflection (or reflection) in JavaScript. And one of the methods of the Reflect object allows you to get all the properties that are declared through both strings and characters.

  var properties = Reflect.ownKeys( user ); console.log( user ); // ['id', 'name', Symbol(role), Symbol(score)] 

Symbols make it possible to divide properties into public and internal, which store internal information and are used only in the process of accessing public information.

  var Project = (function () { var projectData = Symbol('project'); var projectStatus = Symbol('status'); var _getTitle = Symbol('title'); function Project( data, status ) { this[ projectData ] = data; this[ projectStatus ] = status; } Project.prototype.getProjectTitle = function () { return this[ _getTitle ](); }; Project.prototype[ _getTitle ] = function () { return this[ projectData ].name + ' (' + this[ projectData ].description + ')'; }; Project.prototype.getStatus = function () { return this[ projectStatus ]; }; Project.prototype.changeStatus = function ( status ) { this[ projectStatus ] = status; }; return Project; }()); var project = new Project({ name: 'Application', description: 'favorite project' }, 'open'); console.log( project.getStatus() ); // 'open' project.changeStatus('finished'); console.log( project.getStatus() ); // 'finished' console.log( project.getProjectTitle() ); // 'Application (favorite project)' 

In this example, the project information and its status are added through symbols, which makes it impossible to directly read / change them. There is also an internal _getTitle method to which you can only refer to a symbol, which hides it from the public API and is used only when calling getProjectTitle . Symbols also allowed us to bring all the methods to the prototype object.

But still, the main reason for adding characters is not the need to create private properties (this possibility, by the way, was also proposed — private name objects , but for some reason it was not included in the current version of the specification). The main problem that the symbols will solve is the creation of unique properties. Library developers using symbols can add properties that are guaranteed to be unique, which means that they will not conflict with other properties (they will not override properties that are added by other modules, nor can custom code override these properties).

  var Request = (function () { var requestState = Symbol('state'); var states = { NOT_INITIALIZED: Symbol(), RECEIVED: Symbol(), PROCESSING: Symbol(), FINISHED: Symbol() }; function Request() { this[ requestState ] = states.NOT_INITIALIZED; } Request.prototype.getStates = function () { return states; }; Request.prototype.close = function () { this[ requestState ] = states.FINISHED; }; Request.prototype.changeState = function ( state ) { this[ requestState ] = state; }; return Request; }()) var request = new Request(); var handledState = Symbol('state'); request[ handledState ] = false; // code request[ handledState ] = true; request.close(); 

The code above demonstrates a situation where we have a third-party module that the Request entity provides. This module defines the requestState state required for the module to work using the symbol. The developer also enters his state (for example, the state that displays or the request is processed), also using the symbol. This way of declaring properties through symbols gives us a guarantee that the behavior of the module will not be redefined.

In this example, another way of using symbols is also provided. The states object can be treated as an enumerated type with unique values ​​(which allows you to check or pass the correct value).

Now let's look at the methods that allow you to create characters in the global registry of characters.

The code runs in the global context in which this code is declared, but we have the ability to execute code in a different context and this may already cause some problems. For example, in a browser, code that is declared in an iframe will have a different global context and each iframe will have its own copy for RegExp , Array , Date , etc. And the problem is that the characters also exist and are unique only within the context in which they are declared. If your code interacts with the code in the iframe, then in this iframe you will not have references to symbols and you will not be able to access the properties that will be declared through these symbols.

To solve this problem, a global registry has been introduced for characters. To create a symbol that will be available in all contexts, use the Symbol.for method:

  var UUIDSymbol = Symbol.for('uuid'); 

This method is also used to retrieve a character from the global registry. The logic of the method is as follows: if the symbol is found, then it will be returned as the result of the method; if the character is not found in the registry, it will be created, added to the global registry and returned as a result.

A similar situation with the need to use the global registry is on the server side. For example, in Node.js, there is a vm module that allows you to execute code in a different context.

  var vm = require('vm'); var ourArray = Array; var ourSymbol = Symbol('uuid'); var theirArray = vm.runInNewContext('Array'); var theirSymbol = vm.runInNewContext('Symbol("uuid")'); ourArray === theirArray; // false ourSymbol === theirSymbol; // false 

You can see that Array in the current context is not equal to Array in a different context (each has its own copy). Similar behavior as in the situation with iframe and with symbols, they are not equal to each other in different contexts.

  var vm = require('vm'); var ourSymbol = Symbol.for('uuid'); var theirSymbol = vm.runInNewContext('Symbol.for("uuid")'); ourSymbol === theirSymbol; // true 

Symbol.for declaring symbols through Symbol.for we are able to use them in all contexts. If your project uses a vm module with code execution with specific contexts, using Symbol.for would be an excellent solution in this situation.

There is also a method that allows you to get the key by which the symbol is added to the registry - Symbol.keyFor :

  var UUIDSymbolKey = Symbol.keyFor( UUIDSymbol ); 

If the specified character is not found in the registry, it will return undefined .

I want to note once again that the simple creation of a symbol through the Symbol('score') - will not create a symbol in the global registry.

In the next part, we will continue to consider the Symbol object, as well as touch on such a thing as Well-known symbols and see what opportunities they open up for us.

Source: https://habr.com/ru/post/255389/


All Articles