B.2.1 Encapsulation In JavaScript
Encapsulation is a cornerstone of many strong software development paradigms (see Encapsulation). This concept is relatively simply to achieve using closures in JavaScript, as shown in the following example stack implementation:
var stack = {}; ( function( exports ) { var data = []; exports.push = function( data ) { data.push( data ); }; exports.pop = function() { return data.pop(); }; } )( stack ); stack.push( 'foo' ); stack.pop(); // foo
Because functions introduce scope in JavaScript, data can be hidden within them. In Figure B.5 above, a self-executing function is used to encapsulate the actual data in the stack (data). The function accepts a single argument, which will hold the functions used to push and pop values to/from the stack respectively. These functions are closures that have access to the data variable, allowing them to alter its data. However, nothing outside of the self-executing function has access to the data. Therefore, we present the user with an API that allows them to push/pop from the stack, but never allows them to see what data is actually in the stack17.
Let’s translate some of the above into Object-Oriented terms:
- push and pop are public members of stack.
- data is a private member of stack.
- stack is a Singleton.
We can take this a bit further by defining a Stack
prototype so that
we can create multiple instances of our stack implementation. A single
instance hardly seems useful for reuse. However, in attempting to do so, we
run into a bit of a problem:
var Stack = function() { this._data = []; }; Stack.prototype = { push: function( val ) { this._data.push( val ); }, pop: function() { return this._data.pop(); }, }; // create a new instance of our Stack object var inst = new Stack(); // what's this? inst.push( 'foo' ); console.log( inst._data ); // [ 'foo' ] // uh oh. inst.pop(); // foo console.log( inst._data ); // []
By defining our methods on the prototype and our data in the constructor, we have created a bit of a problem. Although the data is easy to work with, it is no longer encapsulated. The _data property is now public, accessible for the entire work to inspect and modify. As such, a common practice in JavaScript is to simply declare members that are "supposed to be" private with an underscore prefix, as we have done above, and then trust that nobody will make use of them. Not a great solution.
Another solution is to use a concept called privileged members, which uses closures defined in the constructor rather than functions defined in the prototype:
var Stack = function() { var data = []; this.push = function( data ) { data.push( data ); }; this.pop = function() { return data.pop(); }; }; // create a new instance of our Stack object var inst = new Stack(); // can no longer access "privileged" member _data inst.push( 'foo' ); console.log( inst._data ); // undefined
You may notice a strong similarity between Figure B.5 and Figure B.7. They are doing essentially the same thing, the only difference being that Figure B.5 is returning a single object and Figure B.7 represents a constructor that may be instantiated.
When using privileged members, one would define all members that need access to such members in the constructor and define all remaining members in the prototype. However, this introduces a rather large problem that makes this design decision a poor one in practice: Each time Stack is instantiated, push and pop have to be redefined, taking up additional memory and CPU cycles. Those methods will be kept in memory until the instance of Stack is garbage collected.
In Figure B.7, these considerations may not seem like much of an issue. However, consider a constructor that defines tens of methods and could potentially have hundreds of instances. For this reason, you will often see the concepts demonstrated in Figure B.6 used more frequently in libraries that have even modest performance requirements.
Footnotes
(17)
The pattern used in the stack implementation is commonly referred to as the module pattern and is the same concept used by CommonJS. Another common implementation is to return an object containing the functions from the self-executing function, rather than accepting an object to store the values in. We used the former implementation here for the sake of clarity and because it more closely represents the syntax used by CommonJS.