This page looks best with JavaScript enabled

Different ways to achieve encapsulation in JavaScript(ES6):

 ·  ☕ 6 min read  ·  ✍️ Iskander Samatov

Encapsulation



JavaScript is a powerful language full of different paradigms that let you write interesting and flexible code. However, at the same time it lacks some of the basic structural features the other languages have. By far one of the biggest anomalies of JavaScript is its inability to natively support encapsulation. 

The scoping system was introduced with TypeScript, which is a superset of JavaScript. But unfortunately its not a clear victory yet since, while it does give you a warning, TypeScript code still compiles and runs even when you access the private variables.

Nevertheless, people came up with ways to achieve encapsulation using other features of the language. And in this post I’m going over the most widely used ones.

Easy way

The easiest way to achieve pseudo-encapsulation would be to prefix your private member with a special symbol that indicates the private scope to the client. It is a common convention to use the _ symbol as a prefix. Of course this won’t actually prevent anyone from accessing your private variables so we won’t go in too much detail here.

Factory functions and closures

Simply put, factory functions are functions used to create new instances of the object. Factory functions are often a preferred choice over the direct object creation using new keyword. The reason is because using factory function gives you the freedom and flexibility to change the object’s instantiation process without client ever being aware of the change.

Factory functions used with closures are often a go-to method for achieving encapsulation because of it’s simplicity. Let’s take a look at the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function Hedgehog () {
    let speed = 10000; // this is private
    this.name = 'Sonic';
    this.zoom = function () {
      // both name and speed are accessible from here
        console.log(`${this.name} zooms with the speed of ${speed} miles per second!`);
    }
}

const sonic = new Hedgehog();
sonic.zoom();

console.log(sonic.name) //valid value
console.log(sonic.speed) // undefined

The code above is a typical example of attaining encapsulation via factory function and closure. While the implementation is pretty straightforward, it does come with a memory usage penalty. The reason is because method zoom will be recreated for every new instance of the Hedgehog function. Here’s an identical implementation that uses ES6 syntax:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Hedgehog {
    constructor() {
        let speed = 10000; //this is private
        this.name = 'Sonic';
        this.zoom = function () {
            // can access both name and speed from here
            console.log(`${this.name} zooms with the speed of ${speed} miles per second!`);
        }
    }


    jump() {
        // cannot access speed from here
        console.log(`${this.name} jumps on top of the building!`);
    }

}

As you can see in order for zoom method to access both speed and name we had to put it in the constructor. In some cases this penalty is acceptable and you might be okay using it. Also, to reduce the overheat you can define classes that don’t use private variables outside of the constructor, like we did with jump method.

Weak maps and namespaces

The memory penalty caused by using factory functions and closures can be avoided by using <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap">WeakMap</a> objects to store the private members. The penalty is avoided since one WeakMap can be used to store private members of multiple instances of the class. Let’s look at the example to understand the concept better:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
let Hedgehog = (function () {
    let privateProps = new WeakMap();

    class Hedgehog {
        constructor(name) {
            this.name = name; // this is public
            privateProps.set(this, { speed: 1000 }); // this is private
        }

        zoom() {
            console.log(`${this.name} zooms with the speed of ${privateProps.get(this).speed} miles per second!`);
        }
    }

    return Hedgehog;
})();


let sonic = new Hedgehog('Sonic');
console.log(sonic.zoom());

Here’s what’s happening here:

  • We wrap the Hedgehog class inside of the self-invoking function and return it. This way we ensure that our private data is only created once and our class is instantly available to the client.
  • Within the self-invoking function but outside of the Hedgehog class, we store our WeakMap. This map is used for storing private variables for each instance of the Hedgehog class. Each value inside of the map is an object that we call namespace. In essence, namespace is a private object with key-value pairs that is only available to the particular instance of the Hedgehog class that holds a reference to it.
  • As mentioned before, the overheat is greatly reduced due to the fact that we’re only storing one map for multiple instances of Hedgehog class.

There’s a reason why we chose WeakMap for storage instead of using a regular Map or a plain JavaScript object:

  • Weak maps allow you to use JavaScript objects as keys. Other kinds of dictionary collections will only let you use primitives.
  • Memory leaks are avoided because WeakMap holds weak references to its items. This allows those items to be garbage collected when they loose all other references.
  • Weak maps guarantee that objects are only accessible using get method. WeakMap does not have any other methods for accessing its items(looping, <em>Object.keys()</em> and etc).

Using Symbols (kind of encapsulated)

The last approach is using new ES6 feature – symbols. Symbol is a type of primitive that is guaranteed to be unique. One of its primary purposes is to be used as key for dictionary collections.

The trick here is to use closure to define a private symbol. Let’s look at the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let Hedgehog = (function () {
    const speed = Symbol();
    class Hedgehog {
        constructor(name) {
            this.name = name;
            this[speed] = 1000; // this is not directly accessible
        }

        zoom() {
            console.log(`${this.name} zooms with the speed of ${this[speed]} miles per second!`);
        }
    }

    return Hedgehog;
})();


let sonic = new Hedgehog('Sonic');
sonic.zoom();

Using this method each instance of the Hedgehog class has its own instance of the this[speed] variable which is still accessible to other methods in the class thanks to the speed symbol defined at the top. Symbols are not accessible when using dot notation and iterating over the collection of objects or using Object.keys(). And so it does provide some level of encapsulation.

While symbols do provide a sufficient level of encapsulation for the most cases, it can still be breached usingObject.getOwnPropertySymbols. Although, most of the time the client should get the hint and won’t try to use those private properties.

And that’s it for this post. Hopefully, you learned some new ways to achieve encapsulation in JavaScript. Thanks for reading!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on web and mobile development.