Chris Middleton
Chris Middleton

Reputation: 5914

Making private instance variable accessible to prototype methods enclosed in anonymous function

Background

I decided I would practice by making a simple calculator app in JS. The first step was to implement a stack class. I ran into some problems however in achieving data encapsulation with the revealing prototype pattern (?). Here's how it looks right now:

Stack "class":

var Stack = (function () { 

    var Stack = function() {
        this.arr = []; // accessible to prototype methods but also to public
    };

    Stack.prototype = Object.prototype; // inherits from Object

    Stack.prototype.push = function(x) {
        this.arr.push(x);
    };

    Stack.prototype.pop = function() {
        return this.arr.length ? (this.arr.splice(this.arr.length - 1, 1))[0] : null;
    };

    Stack.prototype.size = function() {
        return this.arr.length;
    };

    Stack.prototype.empty = function() {
        return this.arr.length === 0;
    };

    return Stack;

})();

Test code:

var s1 = new Stack();
var s2 = new Stack();

for(var j = 1, k = 2; j < 10, k < 11; j++, k++) {

  s1.push(3*j);
  s2.push(4*k);

}

console.log("s1:");
while(!s1.empty()) console.log(s1.pop());
console.log("s2:");
while(!s2.empty()) console.log(s2.pop());

The Problem

The only problem is that the arr is accessible. I would like to hide the arr variable somehow.

Attempts at a Solution

My first idea was to make it a private variable like Stack:

var Stack = (function () {

    var arr = []; // private, but shared by all instances

    var Stack = function() { };
    Stack.prototype = Object.prototype;

    Stack.prototype.push = function(x) {
        arr.push(x);
    };

    // etc.

})();

But of course this approach doesn't work, because then the arr variable is shared by every instance. So it's a good way of making a private class variable, but not a private instance variable.

The second way I thought of (which is really crazy and definitely not good for readability) is to use a random number to restrict access to the array variable, almost like a password:

var Stack = (function() {

    var pass = String(Math.floor(Math.pow(10, 15 * Math.random()));
    var arrKey = "arr" + pass;

    var Stack = function() {
        this[arrKey] = []; // private instance and accessible to prototypes, but too dirty
    };

    Stack.prototype = Object.prototype;

    Stack.prototype.push = function(x) {
        this[arrKey].push(x);
    };

    // etc.

})();

This solution is... amusing. But obviously not what I want to do.

The last idea, which is what Crockford does, allows me to create a private instance member, but there's no way I can tell to make this visible to the public prototype methods I'm defining.

var Stack = (function() {

    var Stack = function() {
        var arr = []; // private instance member but not accessible to public methods
        this.push = function(x) { arr.push(x); }; // see note [1]
    }

})();

[1] This is almost there, but I don't want to have the function definitions within the var Stack = function() {...} because then they get recreated every time that an instance is created. A smart JS compiler will realize that they don't depend on any conditionals and cache the function code rather than recreating this.push over and over, but I'd rather not depend on speculative caching if I can avoid it.

The Question

Is there a way to create a private instance member which is accessible to the prototype methods? By somehow utilizing the 'bubble of influence' created by the enclosing anonymous function?

Upvotes: 1

Views: 536

Answers (3)

Chris Middleton
Chris Middleton

Reputation: 5914

A real solution

EDIT: It turns out this solution is basically the same as the one described here, first posted by HMR in a comment to my question above. So definitely not new, but it works well.

var Stack = (function Stack() {

    var key = {};

    var Stack = function() {
        var privateInstanceVars = {arr: []};
        this.getPrivateInstanceVars = function(k) {
            return k === key ? privateInstanceVars : undefined;
        };
    };

    Stack.prototype.push = function(el) {
        var privates = this.getPrivateInstanceVars(key);
        privates.arr.push(el);
    };

    Stack.prototype.pop = function() {
        var privates = this.getPrivateInstanceVars(key);
        return privates.arr.length ? privates.arr.splice(privates.arr.length - 1, 1)[0] : null;
    };

    Stack.prototype.empty = function() {
        var privates = this.getPrivateInstanceVars(key);
        return privates.arr.length === 0;
    };

    Stack.prototype.size = function() {
        var privates = this.getPrivateInstanceVars(key);
        return privates.arr.length;
    };

    Stack.prototype.toString = function() {
        var privates = this.getPrivateInstanceVars(key);
        return privates.arr.toString();
    };

    Stack.prototype.print = function() {
        var privates = this.getPrivateInstanceVars(key);
        console.log(privates.arr);
    }

    return Stack;

}());


// TEST

// works - they ARE separate now
var s1 = new Stack();
var s2 = new Stack();
s1.push("s1a");
s1.push("s1b");
s2.push("s2a");
s2.push("s2b");
s1.print(); // ["s1a", "s1b"]
s2.print(); // ["s2a", "s2b"]

// works!
Stack.prototype.push.call(s1, "s1c");
s1.print(); // ["s1a", "s1b", "s1c"]


// extending the Stack

var LimitedStack = function(maxSize) {

  Stack.apply(this, arguments);
  this.maxSize = maxSize;

}

LimitedStack.prototype = new Stack();
LimitedStack.prototype.constructor = LimitedStack;

LimitedStack.prototype.push = function() {

  if(this.size() < this.maxSize) {
    Stack.prototype.push.apply(this, arguments);
  } else {
    console.log("Maximum size of " + this.maxSize + " reached; cannot push.");
  }

  // note that the private variable arr is not directly accessible
  // to extending prototypes
  // this.getArr(key) // !! this will fail (key not defined)

};

var limstack = new LimitedStack(3);

limstack.push(1);
limstack.push(2);
limstack.push(3);
limstack.push(4); // Maximum size of 3 reached; cannot push
limstack.print(); // [1, 2, 3]

Cons: basically none, other than remembering a little extra code

Original solution

(The first method originally posted was substantially different from what is below, but through some careless editing I seem to have lost it. It didn't work as well anyway, so no real harm done.)

Here a new object/prototype is created with every instantiation, but it borrows much of the code from the static privilegedInstanceMethods. What still fails is the ability to do Stack.prototype.push.call(s1, val), but now that the prototype is being set on the object, I think we're getting closer.

var Stack = (function() {

    var privilegedInstanceMethods = {
        push: function(x) { 
            this.arr.push(x);
        },
        pop: function() {
            return this.arr.length ? this.arr.splice(this.arr.length - 1, 1)[0] : null;
        },
        size: function() {
            return this.arr.length;
        },
        empty: function() {
            return this.arr.length === 0;
        },
        print: function() {
            console.log(this.arr);
        },
    };

    var Stack_1 = function() {
        var Stack_2 = function() {
            var privateInstanceMembers = {arr: []};
            for (var k in privilegedInstanceMethods) {
                if (privilegedInstanceMethods.hasOwnProperty(k)) {
                    // this essentially recreates the class each time an object is created,
                    // but without recreating the majority of the function code
                    Stack_2.prototype[k] = privilegedInstanceMethods[k].bind(privateInstanceMembers);
                }
            }
        };
        return new Stack_2(); // this is key
    };

    // give Stack.prototype access to the methods as well.
    for(var k in privilegedInstanceMethods) {
        if(privilegedInstanceMethods.hasOwnProperty(k)) {
            Stack_1.prototype[k] = (function(k2) {
                return function() {
                    this[k2].apply(this, arguments);
                };
            }(k)); // necessary to prevent k from being same in all
        }
    }

    return Stack_1;

}());

Test:

// works - they ARE separate now
var s1 = new Stack();
var s2 = new Stack();
s1.push("s1a");
s1.push("s1b");
s2.push("s2a");
s2.push("s2b");
s1.print(); // ["s1a", "s1b"]
s2.print(); // ["s2a", "s2b"]

// works!
Stack.prototype.push.call(s1, "s1c");
s1.print(); // ["s1a", "s1b", "s1c"]

Pros:

  • this.arr is not directly accessible
  • method code is only defined once, not per instance
  • s1.push(x) works and so does Stack.prototype.push.call(s1, x)

Cons:

  • The bind call creates four new wrapper functions on every instantiation (but the code is much smaller than creating the internal push/pop/empty/size functions every time).
  • The code is a little complicated

Upvotes: 1

LetterEh
LetterEh

Reputation: 26696

The short answer here, is that you can't have all things, without sacrificing a little.

A Stack feels like a struct of some kind, or at very least, a data-type which should have either a form of peek or read-access, into the array.

Whether the array is extended or not, is of course up to you and your interpretation...

...but my point is that for low-level, simple things like this, your solution is one of two things:

function Stack () {
    this.arr = [];
    this.push = function (item) { this.arr.push(item); }
    // etc
}

or

function Stack () {
    var arr = [];
    var stack = this;

    extend(stack, {
        _add  : function (item) { arr.push(item); },
        _read : function (i) { return arr[i || arr.length - 1]; },
        _remove : function () { return arr.pop(); },
        _clear  : function () { arr = []; }
    });
}

extend(Stack.prototype, {
    push : function (item) { this._add(item); },
    pop  : function () { return this._remove(); }
    // ...
});

extend here is just a simple function that you can write, to copy the key->val of objects, onto the first object (basically, so I don't have to keep typing this. or Class.prototype..

There are, of course, dozens of ways of writing these, which will all achieve basically the same thing, with modified styles.

And here's the rub; unless you do use a global registry, where each instance is given its own unique Symbol (or unique-id) at construction time, which it then uses to register an array... ...which of course, means that the key then needs to be publicly accessible (or have a public accessor -- same thing), you're either writing instance-based methods, instance-based accessors with prototyped methods, or you're putting everything you need in the public scope.

In the future, you will be able to do things like this:

var Stack = (function () {
    var registry = new WeakMap();

    function Stack () {
        var stack = this,
            arr   = [];

        registry[stack] = arr;
    }

    extend(Stack.prototype, {
        push (item) { registry[this].push(item); }
        pop () { return registry[this].pop(); }
    });

    return Stack;
}());

Nearly all bleeding-edge browsers support this, currently (minus the shorthand for methods).
But there are ES6 -> ES5 compilers out there (Traceur, for instance).
I don't think WeakMaps are supported in Traceur, as an ES5 implementation would require a lot of hoops, or a working Proxy, but a Map would work (assuming that you handled GC yourself).

This lends me to say that from a pragmatic standpoint, for a class as small as Stack you might as well just give each instance its own methods, if you really want to keep the array internal.

For other harmless, tiny, low-level classes, hiding data might be pointless, so all of it could be public.

For larger classes, or high-level classes, having accessors on instances with prototyped methods stays relatively clean; especially if you're using DI to feed in lower-level functionality, and the instance accessors are just bridging from the interface of the dependency, into the shape you need them to be, for your own interface.

Upvotes: 1

Bjorn
Bjorn

Reputation: 71830

You could use a factory function that creates an instance for you:

function createStack() {
    var arr = [];

    function Stack() {

    };

    Stack.prototype = Object.prototype; // inherits from Object

    Stack.prototype.push = function(x) {
        arr.push(x);
    };

    Stack.prototype.pop = function() {
        return arr.length ? (this.arr.splice(this.arr.length - 1, 1))[0] : null;
    };

    Stack.prototype.size = function() {
        return arr.length;
    };

    Stack.prototype.empty = function() {
        return arr.length === 0;
    };

    return new Stack();

}

You would be defining the class on every execution of the factory function, but you could get around this by changing this to define most of Stack outside the constructor function, like the parts that dont use arr could be further up the prototype chain. Personally I use Object.create instead of prototype now and I almost always use factory functions to make instances of these types of objects.

Another thing you could do is maintain a counter that keeps track of the instance and holds on to an array of arrays.

var Stack = (function() {

    var data = [];


    var Stack = function() {
        this.id = data.length;
        data[this.id] = [];
    };

    Stack.prototype = Object.prototype;

    Stack.prototype.push = function(x) {
        data[this.id].push(x);
    };

    // etc.

}());

Now you have the hidden data multi dimensional array, and every instance just maintains its index in that array. You have to be careful to manage the memory now though, so that when your instance isn't being used anymore you remove what's in that array. I don't recommend doing it this way unless you are disposing your data carefully.

Upvotes: 1

Related Questions