Cleaning Up Ajax

* - *
|

I generally line up on the "Worse is Better" side of things, so it's rare that I'm critical of development tools that appear to be working. As Laszlo has dived into its DHTML port, though, we've been looking at the existing open-source frameworks -- especially at their class models -- to see if there's a standard we can follow, or even a useful bit of code to pick up. I've been a bit disappointed in the way that the various Ajax frameworks treat fundamental object model of the Javascript client that's running in the browser.

Of course, as Bill Scott would point out, there's a huge spectrum of applications that fall under the rubric of Ajax, and I suppose that in the vast majority of them, Ajax is used like a spice: it's the parsley in your basic beef+server side development stew. The kind of Ajax that interests me, though, is the stuff where the client is really doing the work: for manipulation and visualization of large datasets or for interactive media viewing and manipulation. For these kinds of the apps, the hard part of the development is often on a client -- not only because the application really does most of its work there, but also because programming the client to offer a robust and continuous user-experience is just harder than sending down a series of web pages. For this kind of stuff, you need industrial-strength OOP with objects and classes; inheritance and override.

Our first stop in exploring the various Ajax frameworks was Prototype, which is pretty popular as a low level basis for other Ajax apps and frameworks. The meta-tag on the Prototype site calls it "Class-style OO," but a close look at the framework reveals that the class mechanism is implemented as a copy of attributes and methods. I would call this approach a form of "implementation inheritance": sure it's probably pretty handy, but it doesn't allow a subclass to augment a superclass method with a combination of override and call-inherited. Prototype has a number of useful features, but a decent framework has to go beyond features to an organizing principle that helps developers structure and think about their code. Sure, these criticisms are a bit snobbish, but it seems withwhile to dig up that computer science shit when we're talking about implementing the basis upon which (hopefully) millions of lines of code will be written.

Next we took a look at Douglas Crockford's scheme for implementing inheritance in Javascript, which was dug up by Max Carlson, Laszlo's Client Tech Lead. This was more promising, because it correctly uses Javascript's prototype property to tie a subclass to its superclass. Calling a superclass's method is always tricky though, and this is where Crawford's stuff goes awry. For the purposes of this discussion, I've adapted Crockford's code a little bit, but preserved the semantics. Anyway, here's his inheritance function:

Function.prototype.inherits( parent ) {
    var d = 0, p = (this.prototype = new parent());
    this.prototype.uber = function {
        var f, r, t = d, v = parent.prototype;
        if (t) {
            while (t) {
                v = v.constructor.prototype;
                t -= 1;
            }
            f = v[name];
        } else {
            f = p[name];
            if (f == this[name]) {
                f = v[name];
            }
        }
        d += 1;
        r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
        d -= 1;
        return r;
    });
    return this;
});

Let's take a moment and look over the code line by line. First we have this:

    var d = 0, p = (this.prototype = new parent());

This line has several functions. First (this.prototype = new
parent() )
sets the prototype of the new class to be an instance of the
superclass. This is the way you do it in Javascript, and it works as long as
you make a separate initialization pass. I won't go into detail about that now
because Max already covered it here. The second effect of this line is to declare two variables that are closed over the scope of the function that follows. p is just a shortcut that points to the constructor's prototype, and d is going to be a static counter that tracks how many instances of the function that follows are on the call stack. (Bonus question: why can't the inherits method just use this.constructor.prototype.constructor.prototypeto refer to the superclass prototype?)

What follows is a method that lets a subclass call its superclass method by name. The tricky part is figuring out which method to call. We don't want to have to be too explicit about this (for instance, by passing the prototype that contains the method to call) because that would violate encapsulation. The intention is that we can use the method like this (code follows):

<button onClick='
    mysuper = function (){ };
    mysuper.prototype.amethod = function (){ return 1; }
    mysub = function(){ };
    mysub.inherits( mysuper );
    mysub.prototype.amethod = function(){
        return 10 + this.uber( "amethod" );
    }
    asub = new mysub();
    alert( "asub.amethod() == 11? " +
                (asub.amethod() == 11 ) );
'>test one
</button>

So let's break down that uber method. First we have the local variable declarations.

 var f, r, t = d, v = parent.prototype;

f is going to be set to the value of the function object that we will eventually call. r is going to hold the return value. t is a counter that initialized to the value of the closed counter d. More about that later. v is set to the value of superclass prototype using the closed parent variable.
As we step into the function, the first thing we're going to do is check to see if d is non-zero (the local variable t has the initial value of d.)

         if (t) {
            ...
        } else {
            f = p[name];
            if (f == this[name]) {
                f = v[name];
            }
        }
        d += 1;
        r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));

Remember that d was initialized to zero in the outer scope, so when we first encounter this function it will still be zero. This puts us in the second block. In this case we look up the named function on the constructor's prototype. The next thing we do is check to see whether the version of that function is the same as the one on the instance. This is important, as it allows us to override a class method on an instance. We can write a little test that builds on the last one to verify this:

<button onClick='
    bsub = new mysub();
    bsub.amethod = function(){
         return 100 + this.uber( "amethod" );
    }
    alert( "bsub.amethod() == 111? " +
             (bsub.amethod() == 111 ) );
'>test two
</button>

In the case where we do override the method in the instance, we won't enter that if clause -- remember that p is the class prototype. In the usual case, where the instance's method is the same as the class method, we want to call the superclass method which lives on the superclass prototype, so we enter that second if clause and look up the function on the superclass prototype. If this logic doesn't make sense right away, spend some time looking at it, because it relates directly to one of the problems with this scheme.

In either case, we assign the function to f and call it, using the built-in Javascript Function method apply which calls a function as if it were a method of an instance.

Now let's look at the case where we call the uber twice in the same call chain, so that we can see what happens in that first if clause. First let's test it this way:

<button onClick='
    mysubsub.inherits( mysub );
    mysubsub.prototype.amethod = function(){
        return 0.1 + this.uber( "amethod" );
    }
    asubsub = new mysubsub();
    alert( "asub.amethod() == 11.1? " +
             (asub.amethod() == 11.1 ) );
'>test three
</button>

In this case, the first uber call, made by the method on mysubsub behaves exactly as above. Think about what happens now, though, when the method on mysubsub uses uber to call the one on mysub. Since the closed variable d is incremented before the call, the uber call on mysub will enter the first clause of the if statement. Here the code enters the while loop that walks up the prototype chain one step for each currently running call to uber.

            while (t) {
                v = v.constructor.prototype;
                t -= 1;
            }
            f = v[name];

Sounds like the right idea, but one problem should already be apparent after looking in depth at the other part of the main if clause. What if the mysub class doesn't define the overriden method? Let's write a little code to find out:

<button onClick='
    mysuper2 = function (){ };
    mysuper2.prototype.bmethod = function ( ){
        return 1;
    }
    mysub2 = function(){ };
    mysub2.inherits( mysuper2 );
    mysub2.prototype.bmethod = function( ){
        return 5 + this.uber( "bmethod" );
    }
    mysubsub2 = function(){ };
    mysubsub2.inherits( mysub2 );
    asubsub2 = new mysubsub2();
    alert( "asubsub2.bmethod( ) == 8? " +
           ( asubsub2.bmethod( ) == 8 ) + "\n" +
            "It is really: " + asubsub2.bmethod( ) );
'>test four
</button>

So this case fails because uber isn't smart enough to detect the fact that there's no definition for bmethod on mysubsub2. It ends up calling the the mysub2 bmethod twice. To do this correctly, uber would need the smarts to tell if the function that it is about to call is really the next one that is defined in the prototype chain.

This might be tolerable if there weren't a bigger problem with this implementation. Note that the uber method uses a single counter (d) for each invocation. This doesn't work in the common case of one over-ridden method calling another. Let's verify this with a test:

<button onClick='
    mysuper3 = function (){ };
    mysuper3.prototype.cmethod = function ( ){
        return this.dmethod();
    }
    mysuper3.prototype.dmethod = function ( ){
         return 0.1;
    }
    mysub3 = function(){ };
    mysub3.inherits( mysuper3 );
    mysub3.prototype.cmethod = function ( ){
        return 1 + this.uber( "cmethod" );
    }
    mysub3.prototype.dmethod = function ( ){
        return 10 + this.uber( "dmethod" );
    }
    mysubsub3 = function(){ };
    mysubsub3.inherits( mysub3 );
    mysubsub3.prototype.cmethod = function ( ){
        return 100 + this.uber( "cmethod" );
    }
    mysubsub3.prototype.dmethod = function ( ){
        return 1000 + this.uber( "dmethod" );
    }
    asubsub3 = new mysubsub3();
    alert( "asubsub3.cmethod( ) == 1111.1? " +
            ( asubsub3.cmethod( ) == 1111.1 ) + "\n" +
            "It is really: " + asubsub3.cmethod( ) );
'>test five
</button>

Because the counter is already incremented when the call to dmethod is made, uber skips the call to mysub3's dmethod.

This has been a long article and we've found problems, but haven't gotten to any solutions. Here at Laszlo, we've developed an implementation of classes in Javascript that is the basis for our new DHTML runtime. Tucker has promised to write about that, so I wouldn't want to spoil the fun. In the mean time, I'll try to follow up with some of the nifty stuff we're doing as we make our move directly into the browser.I love Javascript, and I'm glad we're getting a chance at Laszlo to make it easier for everyone to use.

Comments

I wonder what would happen

I wonder what would happen if people *embraced* JavaScript's prototype nature instead of trying to implement class systems in it.

I'm surprised you didn't write about Dojo instead of Prototype. It seems obvious that while Prototype is successful, the Dojo guys seem to have a better understanding of doing the RIGHT THING in JavaScript --yeah, I know, worse is better ;)

Anyway, I'd love to have a talk with you concerning prototype systems. I forget if you're local or not, but I'd be willing to invite you to dinner and bring Eric Bloch along if you're interested ;)

Your examples don't work in

Your examples don't work in Safari, apparently because Safari's Javascript does not set the constructor field of constructor prototypes correctly. If you add:

mysuper.prototype = {constructor: mysuper}

after the declaration of mysuper, then the examples will 'work' in Safari.

Also, you have a type-oh in Test 3. The correct value should be 6, not 8. (5 from mysub2 and 1 from mysuper).

[...] Max, Adam and I have

[...] Max, Adam and I have been working on a scheme to support OpenLaszlo’s LZX language in pure Javascript. As explained in our blog entries, LZX is a blend of class-based and prototype-based object-oriented programming, with an emphasis on class-based programming (because that seems the more widely accepted paradigm). Our current project is to build a new back-end for the LZX compiler to emit “browser Javascript” (i.e., Javascript that will run in all browsers) and to adjust the OpenLaszlo Runtime to also run in browser Javascript. The goal is to make OpenLaszlo run directly in the browser as any good AJAX platform should. [...]

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Copy the characters (respecting upper/lower case) from the image.
|
* * *