Nicolas Petton

Nicolas Petton

Web developer, Lisper, Smalltalker & Emacs maniac.

A simple object model in JavaScript

A simple object model in JavaScript

Feb 12, 2016

The problem

At Företagsplatsen we are developing a web application with a fairly large JavaScript codebase. We have been using for the past 5 years a coding style described by Douglas Crockford in his book "JavaScript, the good parts" as "functional inheritance", which looks like the following:

function animal(spec, my) {
    spec = spec || {};
    my = my || {};

    var that = {};

    my.name = spec.name;

    that.getName = function() {
        return my.name;
    };

    return that;
}

function dog(spec, my) {
    spec = spec || {};
    my = my || {};

    var that = animal(spec, my);

    that.getName = function() {
        return 'dog named' + my.name;
    };

    return that;
}

This pattern has several benefits over using the JavaScript prototype chain, but to me the most important are the ability to encapsulate behavior and properties, and the use of the my object to inherit "protected" properties.

So far so good, we've been using this with great success in both open source and commercial software.

This pattern is not perfect though, and it has some flaws. One issue is that we keep repeating the same code over and over again:

function foo(spec, my) {
    spec = spec || {};
    my = my || {};

    var that = {};

    [...]

    return that;
}

As you can see, the declaration of spec, my and that has to be duplicated in each "class"1.

There is a bigger problem though: we tend to use this pattern as a classical class-based OO pattern, and it is really not, for instance:

  • there is no way to call super.
  • we do not have proper classes, and we cannot inherit "class-side" – or static – methods.
  • there is no proper way to initialize objects, it's all done within the main function (animal in the above example).
  • we cannot "reopen" (extend) classes after their definitions.

We have some tricks and conventions to go around some of the problems above, but all of them as unsightly and inconvenient. For example, to circumvent the lack of "super", we modify the super class and extend it with a "base" method each time we would need to perform a super call, ending up with code like the following:

function a(spec, my) {
    spec = spec || {};
    my = my || {};

    var that = {};

    that.foo = function() {
        return that.basicFoo();
    };

    that.basicFoo = function() {
        [...]
    };

    return that;
}

function b(spec, my) {
    spec = spec || {};
    my = my || {};

    var that = a(spec, my);

    that.foo = function() {
        var basicFoo = that.basicFoo();
            [...]
    };

    return that;
}

Urgh, not very good.

Fixing the problem, but…

I've been thinking about this for a while, but we have a major constraint: backward compatibibity. I said earlier that we have a fairly large JS codebase (think tens of thousands of LOC), and rewriting everything using a different paradigm is not an option for us.

Whatever solution we choose, we need to be able do a smooth transition, and any new code should be compatible with the current codebase.

We sat down with Benjamin and quickly came with a nice alternative that fixes most of the points above. First, we need a base class, called object (with a lowercase "o"):

function object(spec, my) {
    var that = {};

    that.initialize = function() {};

    return that;
}

To create subclasses, we can add a method subclass to object, that would encapsulate the object creation:

object.subclass = function(builder) {
    var that = this;

    function klass(spec, my) {
        spec = spec || {};
        my = my || {};

        var instance = that(spec, my);

        builder(instance, spec, my);
        instance.initialize();

        return instance;
    }

    return klass;
};

Ok, let's see how the first example looks like now:

var animal = object.subclass(function(that, spec, my) {

    that.initialize = function() {
        my.name = spec.name;
    }

    that.getName = function() {
        return my.name;
    };
});

var dog = animal.subclass(function(that, spec, my) {

    that.getName = function() {
        return 'dog named' + my.name;
    };
});

That's much nicer already. As you can see, we have an explicit way to declare subclasses, and the code duplication is gone:

  • there is no need to initialize spec and my anymore.
  • the instance (that) is created for us.
  • there is no need to return that at the end of each class definition.
  • we have a proper initialize method that is called by the framework for us upon object instantiation.

Adding support for super

We still cannot perform super calls though, but now that we have the beginnings of an infrastructure, that's something easy enough to fix!

object.subclass = function(builder) {
    var that = this;

    function klass(spec, my) {
            spec = spec || {};
            my = my || {};

            var instance = that(spec, my);

            // access to super for public and protected properties.
            instance.super = Object.assign({}, instance);
            my.super = Object.assign({}, my);

            builder(instance, spec, my);
            instance.initialize();

            return instance;
    }

    return klass;
};

Now, we have access to super for both public and protected methods through that.super.foo and my.super.foo:

var animal = object.subclass(function(that, spec, my) {

    that.initialize = function() {
        my.name = spec.name;
    }

    that.getName = function() {
        return my.name;
    };
});

var dog = animal.subclass(function(that, spec, my) {

    that.getName = function() {
        return 'dog named' + that.super.getName();
    };
});

Great! The next snippet puts everything together, adds inheritance for "static" methods, access to the subclasses, and some other goodies.

function object(spec, my) {
    var that = {};

    that.klass = object;

    /**
     * initialize is called by the framework upon object instantiation.
     */
    that.initialize = function() {};

    /**
     * Throws an error because the method should have been overridden.
     */
    that.subclassResponsibility = function() {
            throw new Error("Subclass responsibility");
    };

    return that;
}

/**
 * Return an array of direct subclasses.
 */
object.subclasses = [];

/**
 * Return an array of all subclasses.
 */
object.allSubclasses = function() {
    var allSubclasses = this.subclasses;
    this.subclasses.forEach(function(klass) {
            klass.allSubclasses().forEach(function(subclass) {
                    allSubclasses.push(subclass);
            });
    });
    return allSubclasses;
};

object.subclass = function(builder) {
    var that = this;

    function klass(spec, my) {
            spec = spec || {};
            my = my || {};

            var instance = that(spec, my);

            instance.class = klass;

            // access to super for public and protected properties.
            instance.super = Object.assign({}, instance);
            my.super = Object.assign({}, my);

            builder(instance, spec, my);
            instance.initialize();

            return instance;
    }

    // static inheritance
    Object.assign(klass, that);

    klass.subclasses = [];
    that.subclasses.push(klass);

    return klass;
};

Conclusion

This solution can be a great alternative for us. It fixes all of our issues:

  • spec & my initialized correctly, and no need to return that, which means that we can remove this lines from our codebase, and get rid of a lot of noise.
  • We have a proper way to initialize objects
  • We have instance-side as well as class-side inheritance
  • We can do super calls!
  • Instances have access to their classes (without breaking inheritance), through that.klass.
  • Best of all (for us), it's fully backward-compatible.

There are probably some optimizations to be made, like using the prototype chain behind the scene, but even without it I think I'm really happy with the result.

Update: For those interested, the code is on GitHub.

comments powered by Disqus

Footnotes:

1

This functions are not classes per-se, but I will refer to them as such, mostly because that's exactly how we think of them when writing code.