Evolution of encapsulation in JavaScript

OOP

Encapsulation is one of four major principles of OOP (Encapsulation, Abstraction, Inheritance, Polymorphism). Encapsulation restricts direct access to an object’s data. In many classic object-oriented programming languages like Java and C# this principle is supported at a language level by constructs like theprivate keyword, for example.

However, JavaScript is not an OOP language and in order to implement some of these principles, it needs to apply some tricks. Today I want to talk about some of them which allow for the implementation of encapsulation.

Naming convention

The first way to work around the lack of encapsulation supported by the language is using naming convention, usually underscore as a prefix. All private members have the prefix and should not be accessed directly:

class User {
constructor(name) {
this._name = name;
}

getName() {
return this._name;
}
    setName(value) {
this._name = value;
}
    _checkNameType(value) {
if (value && typeof value !== 'string') {
throw new TypeError('Invalid type of "name"');
}
}
}

This approach has several benefits. It’s very simple, fast and memory-leak free.

However, naming convention based encapsulation also has several drawbacks. Since, it’s just a convention, it does not provide any real restrictions against the access of internal object state and, as a result, there is no way to prevent someone from abusing your classes’ implementations.

Closure

For the most part naming convention works fine for applications with a small team. However it does not work so well for bigger applications with a lot of developers on board and/or for public frameworks and libraries, where restricted access to internal data is crucial. Closure based encapsulation keeps private data secure by declaring it inside the closure of an object during object initialization. Then access to the data is implemented by functions which have an access to the same closure:

// Factory based with plain JS object
function User(name) {
let _name = name;

return {
getName() {
return _name;
},

setName(value) {
_name = value;
}
}
// ES6 Class based
class User {
constructor(name) {
let _name = name;
        this.getName = () => {
return _name;
};
        this.setName = (value) => {
_name = value;
};
}
}

This approach has several benefits. It gives real data restriction, there is no way to get direct access to an internal object’ state and it works in all browsers (you may use function based constructor in order to support older browsers).

However, closure based encapsulation has also several drawbacks. First of all, it’s a more memory consuming approach, since every object has its own closure and new instances of each function. When we create functions as a part of a class/prototype definition, every instance reuses them from its prototype. However in this case, we have to create new functions during object initialization dynamically for each instance. Even though their names are the same, they are different for the JS runtime. Another main drawback is an increased risk for memory leakage. JavaScript garbage collectors are not so good at freeing memory from closures, and therefore, it’s a potential place of memory leakage. That’s why, if your objects have a short lifetime, you need to provide functionality for manual memory clean up (like setting all variables to null).

ES6 Symbol

Symbols are a new primitive type in ES6. This data type can be used as the key for an object property when the property is intended to be private, for the internal use of a class or an object type:

const _NAME = Symbol('name');
class User {
constructor(name) {
this[_NAME] = name;
}
    getName() {
return this[_NAME];
}
    setName(value) {
this[_NAME] = value;
}
}

This approach has several benefits. It’s rather simple and fast. Similar to closure based approach, it gives full restriction of direct access to the internal object’s state (although, you can apply some kind of meta programming by using getOwnPropertySymbols to get all object’s symbols). It also doesn’t have any issues with potential memory leaks, since symbol instances are part of an object.

But ES6 Symbol based approach has also some drawbacks. It’s only compatible with the latest browsers, if you want to support older browsers, you will need to use shims. Also, it needs to hide references to symbol instances in order to protect internal data and make it private for real; perfect place for it is ES6 module closure.

Conclusion

I have used ES6 Symbols for more than 2 years in all my projects and they work like a charm — it’s easy to read and understand. Symbols are also fast and memory safe in terms of leaks. Until the proposal which defines the use of private fields gets approved, and we see any Babel implementations of it, I recommend using ES6 Symbols.

Show your support

Clapping shows how much you appreciated Tim Voronov’s story.