Hiding data, creating encapsulation, in JavaScript ES-2015 classes

; Date: Tue Feb 27 2018

Tags: Node.JS »»»» JavaScript

ES2015 added a ton of useful new features, and with ES2016/2017/2018 we're getting a whole new language with a list of exciting new capabilities. One of them is the new Class defintion syntax that brings Class definitions closer to what's done in other languages. Unfortunately the new Class syntax does not provide a robust implementation hiding mechanism. Hiding the implementation is important in object oriented programming, if only to have the freedom to change the implementation at any time.

Important attributes of object-oriented programming is data encapsulation and implementation hiding. The ES2015 JavaScript class definition offers excellent data encapsulation - but implementation hiding just isn't there. Okay, this is JavaScript, a Functional Programming lanaguage, where object-oriented programming is kind of shunned, but there are techniques regarding Class definitions and practices in other languages which are very useful.

With the default syntax for defining a JavaScript class, there is no implementation hiding. The fields are simply there, and any code anywhere in the program can manipulate the fields. Okay, this is JavaScript, a language where anything can be manipulated at will. Let me ask you if that is safe? For small scripts on a web page it's okay to manipulate all kinds of things. For huge applications meant to have great security, it's not a good idea.

The best recommendation for implementation hiding, TODAY, in JavaScript ES2015 Classes is to use Symbol instances to define the fields. We have an example of this later. Unfortunately while this approach adds a measure of implementation hiding, there is no actual protection. It's relatively easy to for 3rd party code to access the fields protected by that technique. The details for this are shown below.

Introduction

In traditional JavaScript we would tend to create anonymous objects like so:

{
    key: 'foo',
    title: 'The Rain in Spain',
    body: 'Falls mainly on the plain'
}

These objects are quick and easy to create and pass around. But there are at least two problems:

  • You can't robustly tell one anonymous object from another
    • That "if it walks like a duck, talks like a duck, it's a duck" paradigm doesn't work for software. What if you honestly need to give the same name to fields in different objects, but the fields have different semantics because of domain-specific attributes. In such a case one object is a duck, and the other a quark, and they're completely different from each other, even if they have a field or two with the same name.
  • You have no form of implementation hiding

A simple Class

class Note {
    constructor(key, title, body) {
        this.key = key;
        this.title = title;
        this.body = body;
    }
}

The new Class syntax in JavaScript is this. It's similar to Class definitions in other languages, but with a JavaScript flavor to it. We've also written

Rewrite it a little, and save it to note1.js:

module.exports = class Note {
    constructor(key, title, body) {
        this.key = key;
        this.title = title;
        this.body = body;
    }
}

Then run these commands:

$ vi note1.js
david@nuc2:~/t$ node
> const Note = require('./note1');
undefined
> typeof Note
'function'
> const aNote = new Note('foo', 'The Rain In Spain', 'Falls mainly on the plain');
undefined
> var notNote = {}
undefined
> notNote instanceof Note
false
> aNote instanceof Note
true
> typeof aNote
'object'
> 

This shows we can robustly identify the object type using the instanceof operator.

> aNote.title
'The Rain In Spain'
> aNote.title = 'gibberish';
'gibberish'
> aNote.title
'gibberish'
> 

But we have no protection against 3rd party code manipulating object instances. We have no form of implementation hiding.

What if we needed a Note class that retrieved information from a server? That's not possible here.

The best implementation hiding method for JavaScript classes

Create note2.js containing this:

const _note_key = Symbol('key');
const _note_title = Symbol('title');
const _note_body = Symbol('body');

module.exports = class Note {
    constructor(key, title, body) {
        this[_note_key] = key;
        this[_note_title] = title;
        this[_note_body] = body;
    }
    get key() { return this[_note_key]; }
    get title() { return this[_note_title]; }
    set title(newTitle) { this[_note_title] = newTitle; }
    get body() { return this[_note_body]; }
    set body(newBody) { this[_note_body] = newBody; }   
};

This Note class is meant to be equivalent to the first, but with some improvements.

We've used the new getter and setter syntax, and even have one field that's read-only. A get function is a getter, and as implied by the code returns a value corresponding to a field in the object. A set function is a setter, and it sets the corresponding field in the object.

The method we chose for implementation-hiding uses the Symbol class, which is also new with ES-2015. A Symbol is an opaque object with two main use cases:

  • Generating unique keys to use as property fields - as in the Note class above
  • Symbolic identifiers that you can use for concepts like COLOR_RED

You define a Symbol through a factory method that generates Symbol instances:

> let symfoo = Symbol('foo')

Each time you invoke the Symbol factory method a new, and unique, instance is created. For example, Symbol('foo') === Symbol('foo') is false, as is symfoo === Symbol('foo'), because a new instance is created on each side of the equality operator. However, symfoo === symfoo is true, because they are the same instance.

This means the Symbol('key') stored in _note_key is used as the index into Note instances where the key field is stored. The same is true for the other fields.

That particular Symbol instance is only accessible inside the module where this Class is defined. Therefore, 3rd party code outside the module would have a very hard time getting access to that specific Symbol instance and would therefore find it incredibly difficult to access the field in the object.

This means for most intents and purposes the only access to the field implementation is through those getter/setter functions. Therefore, we have a measure of implementation hiding.

Or, do we?

Accessing the hidden fields through the side door

$ node
> const Note = require('./note2');
undefined
> const aNote = new Note('foo', 'The Rain In Spain', 'Falls mainly on the plain');
undefined
> console.log(aNote);
Note {
  [Symbol(key)]: 'foo',
  [Symbol(title)]: 'The Rain In Spain',
  [Symbol(body)]: 'Falls mainly on the plain' }
undefined

This is what we talked about. The fields in the Note instance are somewhat visible, but the index is this Symbol instance whose value is known only inside the note2.js module.

> console.log(aNote[Symbol('key')])
undefined

The straight-forward method to access this value does not work, because the Symbol instance here is not the same as the Symbol instance inside the note2.js module.

But...

> Object.keys(aNote)
[]
> Reflect.ownKeys(aNote)
[ Symbol(key), Symbol(title), Symbol(body) ]

With Object.keys the JavaScript language is set up to ignore the fields whose index is a Symbol. But, using the Reflect class we CAN get ahold of the actual Symbol instances stashed inside the note2.js module.

> aNote[Reflect.ownKeys(aNote)[2]]
'Falls mainly on the plain'
> aNote[Reflect.ownKeys(aNote)[2]] = 'gibberish'
'gibberish'
> aNote.body
'gibberish'
> 

In other words, someone with enough dedication can access those private fields.

This method of using Symbol instances is the best mechanism to privately store object fields. While it provides a measure of implementation hiding, it is not a perfect barrier. The fields are easily accessible using the method shown here.

About the Author(s)

(davidherron.com) David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.

Books by David Herron

(Sponsored)