Next: , Previous: Encapsulation In JavaScript, Up: Visibility Implementation


B.2.2 Hacking Around the Issue of Encapsulation

Since neither Figure B.5 nor Figure B.7 are acceptable implementations for strong Classical Object-Oriented code, another solution is needed. Based on what we have seen thus far, let's consider our requirements:

We can accomplish the above by using the encapsulation concepts from Figure B.5 and the same prototype model demonstrated in Figure B.6. The problem with Figure B.5, which provided proper encapsulation, was that it acted as a Singleton. We could not create multiple instances of it and, even if we could, they would end up sharing the same data. To solve this problem, we need a means of distinguishing between each of the instances so that we can access the data of each separately:

var Stack = ( function()
{
    var idata = [],
        iid   = 0;

    var S = function()
    {
        // set the instance id of this instance, then increment it to ensure
        // the value is unique for the next instance
        this.__iid = iid++;

        // initialize our data for this instance
        idata[ this.__iid ] = {
            stack: [],
        };
    }:

    S.prototype = {
        push: function( val )
        {
            idata[ this.__iid ].stack.push( val );
        },

        pop: function()
        {
            return idata[ this.__iid ].stack.pop();
        }
    };

    return S;
} )();

var stack1 = new Stack();
var stack2 = new Stack();

stack1.push( 'foo' );
stack2.push( 'bar' );

stack1.pop(); // foo
stack2.pop(); // bar

Figure B.8: Encapsulating data per instance

This would seem to accomplish each of our above goals. Our implementation does not break encapsulation, as nobody can get at the data. Our methods are part of the Stack prototype, so we are not redefining it with each instance, eliminating our memory and processing issues. Finally, Stack instances can be instantiated and used just like any other object in JavaScript; the developer needn't adhere to any obscure standards in order to emulate encapsulation.

Excellent! However, our implementation does introduce a number of issues that we hadn't previously considered:

What do we mean by “memory leaks”? Consider the usage example in Figure B.8. What happens when were are done using stack1 and stack2 and they fall out of scope? They will be GC'd. However, take a look at our idata variable. The garbage collector will not know to free up the data for our particular instance. Indeed, it cannot, because we are still holding a reference to that data as a member of the idata array.

Now imagine that we have a long-running piece of software that makes heavy use of Stack. This software will use thousands of instances throughout its life, but they are used only briefly and then discarded. Let us also imagine that the stacks are very large, perhaps holding hundreds of elements, and that we do not necessarily pop() every element off of the stack before we discard it.

Imagine that we examine the memory usage throughout the life of this software. Each time a stack is used, additional memory will be allocated. Each time we push() an element onto the stack, additional memory is allocated for that element. Because our idata structure is not freed when the Stack instance goes out of scope, we will see the memory continue to rise. The memory would not drop until Stack itself falls out of scope, which may not be until the user navigates away from the page.

From our perspective, this is not a memory leak. Our implementation is working exactly as it was developer. However, to the user of our stack implementation, this memory management is out of their control. From their perspective, this is indeed a memory leak that could have terrible consequences on their software.

This method of storing instance data was ease.js's initial “proof-of-concept” implementation (see Class Storage). Clearly, this was not going to work; some changes to this implementation were needed.

B.2.2.1 Instance Memory Considerations

JavaScript does not provide destructors to let us know when an instance is about to be GC'd, so we unfortunately cannot know when to free instance data from memory in Figure B.8. We are also not provided with an API that can return the reference count for a given object. We could provide a method that the user could call when they were done with the object, but that is not natural to a JavaScript developer and they could easily forget to call the method.

As such, it seems that the only solution for this rather large issue is to store instance data on the instance itself so that it will be freed with the instance when it is garbage collected (remember, we decided that privileged members were not an option in the discussion of Figure B.7). Hold on - we already did that in Figure B.6; that caused our data to be available publicly. How do we approach this situation?

If we are adding data to an instance itself, there is no way to prevent it from being accessed in some manner, making true encapsulation impossible. The only options are to obscure it as best as possible, to make it too difficult to access in any sane implementation. For example:

Regardless, it is clear that our data will only be “encapsulated” in the sense that it will not be available conveniently via a public API. Let's take a look at how something like that may work:

var Stack = ( function()
{
    // implementation of getSomeRandomName() is left up to the reader
    var _privname = getSomeRandomName();

    var S = function()
    {
        // define a non-enumerable property to store our private data (will
        // only work in ES5+ environments)
        Object.defineProperty( this, _privname, {
            enumerable:   false,
            writable:     false,
            configurable: false,

            value: {
                stack: []
            }
        } );
    };

    S.prototype = {
        push: function( val )
        {
            this[ _privname ].stack.push( val );
        },

        pop: function()
        {
            return this[ _privname ].stack.pop();
        },
    };

    return S;
} );

var inst = new Stack();
inst.push( 'foo' );
inst.pop(); // foo

Figure B.9: Using a random, non-enumerable property name to store private members

Now we are really starting to hack around what JavaScript provides for us. We seem to be combining the encapsulation issues presented in Figure B.6 and the obscurity demonstrated in Figure B.8. In addition, we our implementation depends on ECMAScript 5 (ideally, we would detect that and fall back to normal, enumerable properties in pre-ES5 environments, which ease.js does indeed do). This seems to be a case of encapsulation through obscurity3. While our implementation certainly makes it difficult to get at the private member data, it is also very obscure and inconvenient to work with. Who wants to write Object-Oriented code like that?

B.2.2.2 Other Considerations

We have conveniently omitted a number of other important factors in our discussion thus far. Before continuing, they deserve some mention and careful consideration.

How would we implement private methods? We could add them to our private member object, just as we defined stack in Figure B.9, but that would cause it to be redefined with each instance, raising the same issues that were discussed with Figure B.7. Therefore, we would have to define them in a separate “prototype”, if you will, that only we have access to:

var Stack = ( function()
{
    // implementation of getSomeRandomName() is left up to the reader
    var _privname = getSomeRandomName();

    var S = function()
    {
        // define a non-enumerable property to store our private data (will
        // only work in ES5+ environments)
        Object.defineProperty( this, _privname, {
            // ... (see previous example)
        } );
    };

    // private methods that only we will have access to
    var priv_methods = {
        getStack: function()
        {
            // note that, in order for 'this' to be bound to our instance,
            // it must be passed as first argument to call() or apply()
            return this[ _privname ].stack;
        },
    };

    // public methods
    S.prototype = {
        push: function( val )
        {
            var stack = priv_methods.getStack.call( this );
            stack.push( val );
        },

        pop: function()
        {
            var stack = priv_methods.getStack.call( this );
            return stack.pop();
        },
    };

    return S;
} );

var inst = new Stack();
inst.push( 'foo' );
inst.pop(); // foo

Figure B.10: A possible private method implementation

While this does solve our problem, it further reduces code clarity. The implementation in Figure B.10 is certainly a far cry from something like ‘this._getStack()’, which is all you would need to do in ease.js.

Another consideration is a protected (see Access Modifiers) member implementation, the idea being that subtypes should inherit both public and protected members. Inheritance is not something that we had to worry about with private members, so this adds an entirely new layer of complexity to the implementation. This would mean somehow making a protected prototype available to subtypes through the public prototype. Given our implementation in the previous figures, this would likely mean an awkward call that somewhat resembles: ‘this[ _protname ].name’.

Although the implementations show in Figure B.9 and Figure B.10 represent a creative hack, this is precisely one of the reasons ease.js was created - to encapsulate such atrocities that would make code that is difficult to use, hard to maintain and easy to introduce bugs. One shouldn't have to have a deep understanding of JavaScript's prototype model in order to write the most elementary of Classical Object-Oriented code. For example, the constructors in the aforementioned figures directly set up an object in which to store private members. ease.js will do this for you before calling the __construct() method. Furthermore, ease.js does not require referencing that object directly, like we must do in our methods in Figure B.9. Nor does ease.js have an awkward syntax for invoking private methods. We will explore how this is handled in the following section.


Footnotes

[1] We could encapsulate this lookup code, but we would then have the overhead of an additional method call with very little benefit; we cannot do something like: ‘this.stack’.

[2] Note that ease.js does not currently randomize its visibility object name.

[3] A play on “security through obscurity”.