A life in the day

1/4/2011

No Classes in Javascript

Filed under: — Scott Sauyet @ 9:39 pm

A colleague has published a series of posts (parts 1, 2, 3, 4, and 5) detailing the evolution of a Javascript class. We discussed this at our recent JS Club meeting. I had several concerns there; some I was able to raise and discuss, others I was not. Here I have the leisure to explain my objections more completely. But before I dive into it, I want to point out that while Paul and I disagree on a fair bit, I have a great deal of respect for him. He’s one of the people I most enjoy working with.

My first problem is the very notion of calling something a “class” when discussing Javascript. Javascript is a class-free language. The closest things Javascript has to the object-oriented notion of classes are constructor functions. But they are really not that close. Trying to impose OO thinking on what more closely resembles a functional language seems to start out in the wrong direction. But this may be simply a matter of terminology. We certainly want the ability to dynamically create multiple objects with similar interfaces and to encapsulate their data. If that’s all that’s meant by “classes”, then perhaps we are not too far apart.

The interface that he is creating can perhaps best be described by a few unit tests. Here are my own (QUnit) unit tests showing a partial set of the capabilities of Paul’s “Guy” constructor:

1
 
test("Simple, index-based storage", function() {
    var bob = new Guy("Bob");
    bob.keep({socks: 2});
    equals(bob.show("0"), "{\"socks\":2}", "Expected Bob's socks to be in slot 1");
    bob.keep({looseChange: 6});
    equals(bob.show("1"), "{\"looseChange\":6}", "Expected Bob's loose change to be in slot 2");
    ok(bob.have("socks"), "Bob should still have his socks");
    ok(bob.have("looseChange"), "Bob should still have his loose change");
});
 
test("Named storage location", function() {
    var fred = new Guy("Fred");
    fred.keep({creditCards: 4}, "wallet");
    equals(fred.show("wallet"), "{\"creditCards\":4}", "Expected Fred's credit cards to be in his wallet");
    fred.keep({cash: 50}, "wallet");
    equals(fred.show("wallet"), "{\"creditCards\":4}", "Nothing else can go in Fred's wallet");
    ok(fred.have("creditCards"), "Bob should still have his credit cards");
});

A live version of these tests is at:

http://scott.sauyet.com/Javascript/Test/Guy/2011-01-04a/

Essentially, a Guy has a name and three public functions. He can store various objects, either in indexed locations or in specifically named ones. He can show JSON representations of the objects he’s stored when supplied their name. And he can report whether or not he’s stored something for a given name. These are represented by the functions keep, show, and have. This is a bit oversimplified, but it’s enough for these discussions. The unit tests above are by no means a complete test suite for the constructor function, but they do show much of the expected usage of Guys.

Here is Paul’s final implementation:

1
 
var Person = function (name, basics) {
    this.name = name;
    this.stuff = basics || {};
    this.index = -1;
};
 
Person.prototype.find = function (where, keep) {
    var label = where.split("."),
    box = this.stuff;
    while (label.length > 1) {
        if (typeof box[label[0]] === "undefined") {
            if (keep) {
                box[label[0]] = {};
            } else {
                return;
            }
        } else {
            box = box[label.shift()];
        }
    }
    return [box, label[0]];
};
 
Person.prototype.keep = function (what, where) {
    var found, box, label;
    if (where) {
        found = this.find(where, true);
    } else {
        found = [this.stuff, this.index += 1];
    }
    box = found[0],
    label = found[1];
    if (typeof box[label] === "undefined") {
        box[label] = what;
    }
};
 
var Guy = function (name, basics) {
    var person = new Person(name, basics);
    this.have = function (what) {
        if (typeof person.find(what) !== "undefined") {
            return true;
        } else {
            return false;
        }
    };
    this.show = function (what) {
        var found, box, label;
        if (typeof what === "undefined") {
            found = [person, "stuff"];
        } else {
            found = person.find(what);
        }
        box = found[0];
        label = found[1];
        if (typeof box[label] === "object") {
            return JSON.stringify(box[label]);
        } else {
            return box[label];
        }
    };
    this.please = function (action) {
        var request = Array.prototype.slice.apply(arguments);
        request.shift();
        if (typeof action === "function") {
            action.apply(person, request);
        } else {
            if (person[action]) {
                person[action].apply(person, request);
            }
        }
    };
};
 
Guy.prototype.keep = function (what, where) {
    this.please("keep", what, where);
};

In this implementation, a Guy is created with a hidden reference to a Person. His own-properties, such as have and show, as well as his helper function, please, have access to this Person, but his prototype functions cannot directly access this Person; they need to elevate their privileges by calling please.

The first concern, and the one I raised in our discussion, but couldn’t fully articulate, was my objection to the expanded API this implementation entails. The unit tests above suggest a fairly simple API: There needs to be a constructor function which accepts a name. (The name is not actually used anywhere, but it’s easy enough to imagine there would be a use for it soon.) And the objects created from this constructor should have as properties the functions keep, show, and have. That’s all that’s really required. But this implementation exposes the Guy constructor, the prototype for that Constructor containing a keep function, the Person constructor, and its prototype containing find and keep. The objects created from the Guy constructor have the expected keep, show, and have properties, but they also have a please property exposed. If all those additional API capabilities were meant to be public, I would have no objection. But they are implementation details, which to my mind should be kept private.

A lesser concern has to do with the additional parameter which can be passed into the constructor. This was to allow us to start each Guy off with a certain set of stuff; it’s not tested in the code above. The problem is that, for all the concern about encapsulation in the discussions about the five steps, the implementation uses that object directly if supplied to store its stuff. So code that kept a reference to the object passed to this constructor would have a handle on what’s supposed to be entirely private stuff. This would be a major concern except that if this were actually in a larger system, there would probably some utility clone function which could be readily employed to fix this problem. This one can be chalked up to it being demo code.

The next objection is more substantial, but it might be the hardest to overcome if there is a need for a Guy’s configuration to remain safely encapsulated: Every Guy created has its own copy of the hide, show, and please functions. This could be quite memory-intensive if there are a lot of Guys in the system. Moreover, it seems very contrary to the overall direction of trying to make Javascript a little closer to classical OO languages. I’ll discuss my alternative implementation below. In it I don’t solve this issue; I don’t know of a solution to this. But I do reduce the custom functions to very thin wrappers around otherwise shared functions. Certainly it reduces the problem. This should demonstrate what I mean:

1
 
    var bob = new Guy("Bob");
    var fred = new Guy("Fred");
    console.log(fred.keep === bob.keep);     // true
    console.log(fred.show === bob.show);     // false
    console.log(fred.have === bob.have);     // false
    console.log(fred.please === bob.please); // false

But my biggest concern is that even though a major goal in the discussion was to encapsulate private data in a way that made it safe from prying eyes, this implementation does not manage to do so. Paul said that, “To make sure you don’t trick [a Guy] into giving away his stuff, please will return undefined.” But I can still get at it quite easily:

1
 
    var george = new Guy("George");
    george.keep({creditCards: ["Visa", "Amex"]}, "wallet");
    george.please(function() {pickpocket = this.stuff;});
    console.log(pickpocket); // {"wallet":{"creditCards":["Visa","Amex"]}}
    pickpocket.wallet.creditCards = [];
    console.log(george.show("wallet")); // {"creditCards":[]}

The pickpocket was able to get at George’s stuff, and not just a JSON-stringified copy, but a reference to the original. We could similarly get the entire Person, by simply storing this in the variable. This is rather disheartening for George, and demonstrates a real problem — possibly even an intractable one — with trying to hold data out of view in the closure of a constructor but still allowing access to it from prototype methods. Although it’s nice that we had to say “please” to get this data, it seems wrong to make that the only level of protection.


I created an alternate implementation. It solves the first and last issues completely. The public API is just what we would like, and the private data remains private. I didn’t try to solve the non-cloned constructor parameter issue; that’s simple enough to do, and the same sort of code would solve it for both implementations. For the remaining issue, I reduced the memory requirements for each Guy, but individual objects still have their own implementations of the public functions. Those are simple wrappers around common functions, so the cost is lower, but it does not solve this problem entirely. I’d love to see a technique that manages to do this and still keep the small API and properly encapsulate its data.

This solution is less object-oriented than Paul’s. I choose a more functional approach. As best I could, I kept his functions intact. The test page is here:

http://scott.sauyet.com/Javascript/Test/Guy/2011-01-04c/

The code looks like this:

1
 
var Guy = (function() {
    var 
    find = function (where, keep) {
        var label = where.split("."),
        box = this.stuff;
        while (label.length > 1) {
            if (typeof box[label[0]] === "undefined") {
                if (keep) {
                    box[label[0]] = {};
                } else {
                    return;
                }
            } else {
                box = box[label.shift()];
            }
        }
        return [box, label[0]];
    },
    have = function (what) {
        if (typeof find.call(this, what) !== "undefined") {
            return true;
        } else {
            return false;
        }
    },
 
    keep = function (what, where) {
        var found, box, label;
        if (where) {
            found = find.call(this, where, true);
        } else {
            found = [this.stuff, this.index += 1];
        }
        box = found[0],
        label = found[1];
        if (typeof box[label] === "undefined") {
            box[label] = what;
        }
    },
    show = function (what) {
        var found, box, label, result;
        if (typeof what === "undefined") {
            result = this.stuff;
        } else {
            found = find.call(this, what);
            box = found[0];
            label = found[1];
            result = box[label];
        }
        if (typeof result === "object") {
            return JSON.stringify(result);
        } else {
            return result;
        }
    },
    Guy = function(name, basics) {
        if (!(this instanceof Guy)) {
            return new Guy(name, basics);
        }
        var cfg = {stuff: basics || {}, index: -1};
        this.keep = function(what, where) {keep.call(cfg, what, where);};
        this.have = function(what) {return have.call(cfg, what);};
        this.show = function(what) {return show.call(cfg, what);};
    };
    return Guy;
}());

I don’t have very much to say about this implementation. It’s a few lines shorter than the original. The techniques are nothing new. The actual constructor function is wrapped in a closure along with the basic functions to be used. The objects created have thin wrappers calling these functions, supplying the configuration information.

I should note that one of Paul’s concerns was being able to break apart his implementation into pieces that could be maintained separately. This technique would make that more difficult. This does not worry me, because I believe that such constructor functions should be cohesive pieces that are relatively short and easy for a single person to maintain. But if that’s a bigger issue for the reader than the ones above, then s/he is encouraged to look for other approaches.

In any case, I hope this sheds some light on the different concerns that developers might have on to how to build constructor functions in Javascript. A central issue is that we should be using the strengths of the language rather than trying to make it look like some other type of language. Javascript’s flexible semi-functional nature can be very powerful. Trying to graft on a substantial OO gloss can serve to hide the beauty and the power of Javascript.
make

Powered by WordPress