January 23, 2009

Easy object-oriented Javascript the Python way

Posted in Software at 00:47 by graham

Javascript is not an opinionated language. At it’s heart it is a hash map. You can layer pretty much any idiom you want on top of it. I’d like to make it look like Python, and it’s pretty easy to do. They both are dynamically typed, have functions as first class objects, and can treat most types as hash maps.

Let’s assume code for a whimsical Moose Observation Project, and translate it from Python to Javascript.

The Python

Here is mop.py:

ANSWER = 42

class Moose:

    BULL = 1
    COW = 2

    def __init__(self, sex):
        self.legs = 4
        self.sex = sex

    def speak(self):
        if self.sex == Moose.BULL:
            print "a heavy grunt-like noise that can be heard up to half a kilometer away"
        elif self.sex == Moose.COW:
            print "a wail-like bawl"
        else:
            print "Sexless moose say "+ self._default_sound()

    def _default_sound(self):
        return str(ANSWER)

The Javascript

And here is mop.js:

if (typeof MOP == "undefined" || !MOP) {
    var MOP = {};
}

MOP.ANSWER = 42;

MOP.Moose = function(sex) {
    // In case we forget the 'new' keyword
    if ( !(this instanceof MOP.Moose) ) {
        return new MOP.Moose(sex);
    }
    this.legs = 4;
    this.sex = sex
}
MOP.Moose.BULL = 1;
MOP.Moose.COW = 2;

MOP.Moose.prototype = {

    speak:
        function() {
            if (this.sex == MOP.Moose.BULL) {
                console.log("a heavy grunt-like noise that can be heard up to half a kilometer away");
            }
            else if (this.sex == MOP.Moose.COW) {
                console.log("a wail-like bawl");
            }
            else {
                console.log("Sexless moose say "+ this._default_sound());
            }
        },

    _default_sound:
        function() {
            return MOP.ANSWER;
        }
}

Using it

In Javascript:

var bennyTheMoose = new MOP.Moose(MOP.Moose.BULL);
bennyTheMoose.speak();

The details

Let’s look at the interesting parts in more detail. Note that below when I say property I mean attribute or method.

The module is just a namespace

Because you don’t know what other scripts might be included on a page that uses your script, you need to use the smallest amount possible of the global namespace. Create an object, and put everything else you define in that object. Yahoo, for example, puts everything they write in the YAHOO object / namespace. Here our namespace is called MOP.

new: The class is a function which returns an object

The two parts of Javascript that make object oriented programming possible are the new operator applied to a function, and the prototype attribute of that function.

When a function call is preceded by the new operator, it gets given a new object called this, and it returns it. The Moose function is your constructor It is good practice to define your object’s data in there, same as it is in Python. It returns your new instance.

If you forget the new operator, you get no warning. The function is called as a function instead of as a constructor. this is bound to the global window object, and the function returns undefined. Your call is perfectly valid, yet it does something very different to what you wanted. A lot of Javascript is like that! To prevent this we added a check at the top of the constructor to use new if you forget it.

prototype: Instances can have methods – but ignore that

A significant difference between Javascript and most other languages is that in Javascript object instances can have properties. In most other languages, only the class has properties, and all instances share them. We’re trying to be Pythonic, so let’s not use this feature, but knowing about it helps to understand the prototype attribute.

When you call a method on an instance, if that method is not found, it will try it’s prototype attribute. The prototype attribute is shared amongst all instances constructed with the same constructor (because prototype is an attribute of the constructor function). If we put all our methods on the prototype, we get the same behavior as in a more traditional object-oriented language.

Object literals keep things tidy

I’m using an object literal to list the methods of the Moose class. Here is the equivalent code without using an object literal:

MOP.Moose.prototype.speak =
    function() {
        if (this.sex == MOP.Moose.BULL) {
            console.log("a heavy grunt-like noise that can be heard up to half a kilometer away");
        }
        else if (this.sex == MOP.Moose.COW) {
            console.log("a wail-like bawl");
        }
        else {
            console.log("Sexless moose say "+ this._default_sound());
        }
    };
MOP.Moose.prototype._default_sound =
    function() {
        return MOP.ANSWER;
    };

I think an object literal wraps the functions up more neatly and saves some typing.

Private methods

Javascript, like Python, doesn’t give you anything built in to declare an access level – everything is public. Using closures, you can have private methods in Javascript – examples here and item 6 here – but that’s not how Python does it.

In Python the convention is to prefix private methods with a single underscore. They can still be called, but you are warning the user that they are now on their own, all bets are off. Let’s simply carry that convention over to Javascript: _default_sound is a private method.

Conclusion

  • Put everything in a namespace.
  • Your class is a function.
  • That function is your constructor. You declare your attributes in that constructor.
  • You call that function with the new operator.
  • The methods of your class go on the prototype attribute of that function.
  • Use the underscore-prefix convention to mark a private method.

Javascript and Python have a lot in common, and applying some of Python’s Zen to Javascript helps me impose some order on the chimera that is Javascript. I hope it helps you too.

Leave a Comment

Note: Your comment will only appear on the site once I approve it manually. This can take a day or two. Thanks for taking the time to comment.