codete Private State in Java Script 1 main bd7c414b62
Codete Blog

Private State in JavaScript

avatar male f667854eaa

30/01/2017 |

7 min read

Michał Wadas

The ld procedural code is dying out. The future of JavaScript is divided between object-oriented and functional paradigms. But... What's finally the best approach to encapsulation?

JavaScript has come a long way from simple scripting language used for animating snow on personal webpages to recognized multiple paradigm language, used by the biggest enterprises to build complex applications.  

Object-oriented programming wasn’t eagerly embraced by the community – prototype-based programming, one of the JavaScript paradigms, isn’t intuitive for skilled developers switching to it from classical object-oriented languages, like Java or C++. As there was no way to declaratively define “class” nor syntax to conveniently call method or constructor from the superclass, we can understand their confusion.


The first approach to object-oriented programming

Here we have a canonical example of “class” in JavaScript.

function Foo() {}


function Bar() {
Foo.apply(this, arguments);
this.baz = 0;
}


Bar.prototype = new Foo(); // We have to do more complicated stuff if Foo constructor has side effects
Bar.prototype.constructor = Bar;
Bar.prototype.method = function() {
return this.baz++;
};


bar = new Bar();


bar.method();

This is how we had to define objects in the dark ages before the ecosystem of custom framework-specific class helpers exploded. We can see three lines of boilerplate code here – we have to manually change Bar.prototype to correct object and manually apply Foo constructor as a mixin.

This code isn’t defensive. What if the Foo constructor has side effects? What should happen if a constructor is called without “new”? What if we want bar.method to call it “super”?

var registry = {
add: function() {}
};


function Foo() {
if (this === undefined) {
     throw new Error();
}
registry.add(this);
}


Foo.prototype.method = function () {
return 3;
};


function Bar() {
if (this === undefined) {
     throw new Error();
}
Foo.apply(this, arguments);
this.baz = 0;
}


Bar.prototype = function(){
function F() {}
F.prototype = Foo.prototype;
var ret = new F();
F.constructor = Bar;
return ret;
}();


Bar.prototype.method = function () {
return this.baz += Foo.prototype.method.call(this);
};

We wrote 10 lines of boilerplate already and we don’t even check if this == global or other edge cases. Calling super from subclass requires us to explicitly refer to superclass, so changes in the class hierarchy (eg. our class used to inherit directly from Component but now it’s going to inherit from Component through its subclass, AppletComponent) require careful inspection of all methods. 

Moreover, Foo.prototype.method.call is just an awkward thing to write.

Next try - factory functions

 These problems were addressed by a pattern called “factory function”, later popularized by Douglas Crockford. It focuses on avoiding the use of “this” and constructors and favoring composition over inheritance.

function createObject() {
var privateState = 0;
var myObject = {
     property: 'foo',
     bar: 42,
     myMethod: function() {
        myObject.bar++;
     },
     getPrivateState: function () {
        return privateState;
     }
};
return myObject;
}

For a long time, it was the most popular method of object-oriented programming in JS. Mostly declarative, methods are bound to an instance, it’s impossible to accidentally share state among instances - and there comes the killer feature - we have private state.

But there is no free lunch, and it has major drawbacks – every function has to be allocated over and over with its enclosing scope, resulting in a significantly higher memory footprint. Currently, that’s not such a  big deal, because we have more available RAM and modern JavaScript engines optimize functions with the same source code for lower memory usage. 

Other disadvantages include no inheritance (specific forms of composition can be used to emulate inheritance in many cases), no exposed methods to be .call-ed, forced duck typing, limited composition.

Many abstractions over object-oriented programming

A popular alternative that has risen against factory function were special functions exposed by frameworks and libraries like Backbone’s Model.extend ( http://backbonejs.org/#Model-extend ) or Prototype.js’ Class.create (http://prototypejs.org/doc/latest/language/Class/create/ ). We will not go into further details, but these functions are basically bags of helpers to reduce boilerplate code from prototypical inheritance. There is no private state there, as there is no way to define such a private state in classical JS.

Yes, JavaScript used to lack a method to define truly private state associated with instances of “class”. Closures exploited by factory function patterns can fully encapsulate private states, but they come with their own limitations, eliminating this pattern from modern web applications.

So how can we encapsulate data and methods? Right now, the most popular way is to use methods prefixed with an underscore or two. Private by convention is a technique borrowed from Python – you aren’t expected to use such methods (“we're all consenting adults”) and proper linting combined with code review would work almost as well as the static compiler to ensure lack of access to private members.

class Foo {
constructor() {
     this._i = 0;
}
method() {
     return this._method();
}
_method() {
     return (this._i++);
}
}

However, with ECMAScript 2015 (previously known as ES6) we got two better ways to encapsulate our precious internal state.

“Symbol” means (almost) private

The easier of them is to use Symbols. A symbol is a new primitive type in JavaScript. Each symbol is unique (Symbol() !== Symbol()) and can be used as a property key in addition to strings. Its main advantage over unguessable strings is invisibility for classic iteration methods like for-in, Object.keys, Object.getOwnPropertyNames.

const _method = Symbol();
const _i = Symbol();
class Foo {
constructor() {
         this[_i] = 0;
}
method() {
         return this[_method]();
}


[_method]() {
         return (this[_i]++);
}
}

That’s my method of choice - it’s easy to use, easy to define a way to prevent your code consumers from messing with your class guts[1]. But, what is the catch? Encapsulation isn’t perfect – properties can be retrieved by using the new Object method, Object.getOwnPropertySymbols

Some people will say “wait, but these fields aren’t actually private, because you can use reflection to inspect them”. Yes, reflection can expose private fields, in the same way, that it can do in Java, PHP, C# using easily accessible reflection API. Even in C++, a language without runtime reflection, it’s possible to access internal members by pointer voodoo.

Though, there is another method to encapsulate our state, that will be appreciated by purists.

Ideally private state achievable by weak maps

We can achieve our goal using WeakMap. The basic idea is to weakly link the object to another object holding data. To access private fields both weak map and class instance are required – but a weak map is never leaked from module/closure [2].

const wm = new WeakMap();


function privateMethod(self) {
const state = wm.get(self);
return state.i++;
}


class Foo {
constructor() {
     wm.set(this, {
           i: 0
     });
}


method() {
     return privateMethod(this);
}
}

That’s probably the most defensive way to implement a private state in modern JavaScript. 

 

Conclusions

Those are four ways to encapsulate a private state. Each of them has its own advantages and disadvantages. For maximal compatibility I would use private by convention, because of its minimal dependency - it will work out-of-the-box in all possible browsers, including IE5. 

My method of choice, preferred for new code, would be Symbols - it’s easy and has only one minor drawback. WeakMap based encapsulation isn’t really fit for that, as it decreases code readability and maintainability.  On the other hand, I would not use a function factory because it imposes too many limitations.


[1] Programmers shouldn’t mess with your guts too, it’s legal only for doctors.

[2] Overwriting WeakMap.prototype.get to leak “this” doesn’t count.

Rated: 5.0 / 1 opinions
avatar male f667854eaa

Michał Wadas

Senior Java Developer

Our mission is to accelerate your growth through technology

Contact us

Codete Global
Spółka z ograniczoną odpowiedzialnością

Na Zjeździe 11
30-527 Kraków

NIP (VAT-ID): PL6762460401
REGON: 122745429
KRS: 0000983688

Get in Touch
  • icon facebook
  • icon linkedin
  • icon instagram
  • icon youtube
Offices
  • Kraków

    Na Zjeździe 11
    30-527 Kraków
    Poland

  • Lublin

    Wojciechowska 7E
    20-704 Lublin
    Poland

  • Berlin

    Bouchéstraße 12
    12435 Berlin
    Germany