This article discusses 4 approaches to managing private data of ES6 classes:
1. Data storage in the class constructor.
2. Marking of private properties through a naming convention (for example, a prefix underscore).
3. Store private data in WeakMaps.
4. The use of characters in the form of keys for private properties.
The first and second approaches were widely used in ES5, and the third and fourth - appeared only in ES6. Let's take a look at each one by one.
')
1. Data storage in class constructorOur current example is the Countdown class, which calls the action function when the counter becomes zero. At the same time, counter and action should be saved as private variables.
First, we save action and counter to the context of the constructor class. A context is an internal data structure, where the JavaScript engine stores parameters and local variables that exist when a new scope is introduced (for example, by calling a function or constructor). Here is the actual code:
class Countdown { constructor(counter, action) { Object.assign(this, { dec() { if (counter < 1) return; counter--; if (counter === 0) { action(); } } }); } }
Using Countdown is as follows:
> let c = new Countdown(2, () => console.log('DONE')); > c.dec(); > c.dec(); DONE
Benefits:
● Private data is completely safe.
● The names of private properties will not conflict with the names of other private properties of the parent and child classes.
Disadvantages:
● The code becomes less elegant due to the need to define all instance methods in the constructor (at least those that need access to private data).
● That is why the code spends a lot of memory. If prototype methods were used, they will be distributed.
Read more about this approach in the Speaking JavaScript section
of the Constructor (Crockford Privacy Pattern) section.
2. Marking of private properties through a naming conventionThe following code stores private data in properties with prefix underlined names:
class Countdown { constructor(counter, action) { this._counter = counter; this._action = action; } dec() { if (this._counter < 1) return; this._counter--; if (this._counter === 0) { this._action(); } } }
Benefits:
● The code looks beautiful.
● Prototype methods can be used.
Disadvantages:
● Insecure. This is just an instruction for client code.
● Private property names may conflict.
3. Store private data in WeakMapsThis method combines the advantages of the first and second approaches: security and the possibility of using methods of prototypes. WeakMaps _counter and _action are used to save private data:
let _counter = new WeakMap(); let _action = new WeakMap(); class Countdown { constructor(counter, action) { _counter.set(this, counter); _action.set(this, action); } dec() { let counter = _counter.get(this); if (counter < 1) return; counter--; _counter.set(this, counter); if (counter === 0) { _action.get(this)(); } } }
The _counter and _action variables store objects matching their private data. Based on how WeakMaps works, objects can be deleted by the garbage collector. Private data is safe as long as WeakMaps is hidden. To protect yourself, you can also save WeakMap.prototype.get and WeakMap.prototype.set to temporary variables and call them instead of dynamically invoking methods. Even if the malicious code replaces these methods with those that have access to private data, it will not affect our code. However, the protection extends only to the code that was run after ours - unfortunately, it is impossible to protect the one that was running before it.
Benefits:
● Prototype methods can be used.
● Safer than a naming convention for property keys.
● Private property names cannot conflict.
Disadvantage:
● The code is not as elegant as the naming convention.
4. Using characters as keys for private properties.Another location of the private data storage is properties with keys as symbols:
const _counter = Symbol('counter'); const _action = Symbol('action'); class Countdown { constructor(counter, action) { this[_counter] = counter; this[_action] = action; } dec() { if (this[_counter] < 1) return; this[_counter]--; if (this[_counter] === 0) { this[_action](); } } }
A property with a key-symbol will never conflict with another property, because each character is unique. In addition, the symbols, though not completely, are hidden from external influence:
let c = new Countdown(2, () => console.log('DONE')); console.log(Object.keys(c));
Benefits:
● Prototype methods can be used.
● Private property names cannot conflict.
Disadvantages:
• Compared to the naming convention, the code is not so elegant.
• Insecure, since all property keys for an object, including symbols, can be defined using Reflect.ownKeys ().
5. Additional literature:●
Speaking. JavaScript, section Keeping Data Private;
●
Exploring ES6 , Chapter Classes;
●
Exploring ES6 , head of Symbols.