10 December 2008

Objects in Javascript

As a consequence of building iconfu.com, I've used more javascript in the past four months than in all the previous ten years combined. Javascript is not difficult, but its superficial similarities with java can be misleading for someone, such as I, coming from that language. Javascript is also, surprisingly, a respectable language, tarnished only by browser incompatibilities and buggy implementations (I'm thinking of one browser implementation in particular).

Here are the three worst stumbling blocks I faced on the slow path of figuring out how objects work in javascript: (1) what does "this" refer to? (2) how to write a constructor, and (3) how to define functions on objects.

If you're just scripting mouse events on a web page, you won't need much of this, but it's handy if you're doing anything serious with javascript (like, writing an image editor, ahem).

1. "this" refers to the object on which the function was called, not necessarily the place where the function was defined

Unlike java, javascript lets you copy methods about from object to object. So, suppose you have

var Truck = {
  fuel : 100,

  drive: function(km) {
    this.fuel = this.fuel - km;
  }
}

You can legitimately write

var Car = {
  fuel: 100
};

Car.drive = Truck.drive;

And then,

Car.drive(10);
alert(Car.fuel); // alerts 90

The "this" reference in the drive function references the callee's object, in this case Car, even though "this" originally referred to Truck.

"this" can be a source of much grief when setting up event listeners on objects. Suppose you want to install an onclick handler on a DOM element that allows your user interact with your vehicle, thus:

<a id='drive_truck'>drive the truck!</a>

and

var Truck = {
  // same as before, plus

  init : function() {
    document.getElementById('drive_truck').onclick = function(event) {
      // this.drive();  // FAIL: "this" is the span element, not the Truck object
      Truck.drive(); // this works, but it's ugly!
      someOtherStuff();
    };
  }
}

Truck.init();

the moral of the story: this inside an event handler function refers to the DOM element on which the event was fired - NOT the place where the function was defined. Fortunately, there are better ways to declare objects so that this ugliness can be avoided.

2. Two ways of creating objects: direct declaration (hash), or via constructor

The Truck example above shows a simple, direct way of creating objects, and it's common to do this all over the place in javascript. Mostly, it's the right thing to do, if you want something that's similar to a HashMap in java.

But, javascript also has constructors, and the funny thing about a javascript constructor is that it looks just like an ordinary function.

function Truck() {
  this.fuel = 100;

  this.drive = function(km) {
    this.fuel = this.fuel - km;
  }

  var self = this;

  document.getElementById('drive_truck').onclick = function(event) {
    // this.drive();  // FAIL: "this" is the span element, not the Truck object
    self.drive(); // this works, but you need the "self" local variable
    someOtherStuff();
  };
}

var myTruck = new Truck();
myTruck.drive(60);
alert(myTruck.fuel); // alerts 40

The Truck() function is, in fact, the Truck constructor. When you call new Truck(), instead of just calling Truck(), the "this" keyword within the function references a new object, for which this instanceof Truck returns true.

The difference between this approach and the previous one is that now you have a typed object, and you can have multiple instances of the same type of object.

3. Two ways of defining functions on objects: in constructor, or via prototype

So there are a few ways of associating functions with objects: you can declare them in the constructor, as we did above, or you can just add them later, as we did with the "Car" example. A problem arises with the constructor method if you define functions on your objects within the constructor: each object instance you create will have its own unique instance of each function as well. This can potentially introduce memory issues if you're creating a lot of objects.

The prototype approach eliminates this issue. Every object has an associated "prototype" object, to which you can add properties. Truck.prototype is just another object, but one treated with special respect by all Truck objects.

function Truck() {
  this.fuel = 100;

  var self = this;

  document.getElementById('drive_truck').onclick = function(event) {
    // this.drive();  // FAIL: "this" is the span element, not the Truck object
    self.drive(); // this works, but you need the "self" local variable
    someOtherStuff();
  };
}

Truck.prototype.drive = function(km) {
  this.fuel = this.fuel - km;
}

var myTruck = new Truck();
myTruck.drive(60);
alert(myTruck.fuel); // alerts 40, same as before

When the javascript interpreter searches a Truck instance for a "drive" property and finds nothing, it will consult "Truck.prototype" and see if there's anything appropriate there. If there is, the interpreter behaves as if that property were originally defined on the Truck instance itself. So the "this" reference remains intact for all properties inherited from the object's prototype.

Don't confuse the "prototype" concept with the "prototype.js" library. Javascript is a "prototype-based" object-oriented language, unlike java, which is a "class-based" object-oriented language. The prototype.js library relies heavily on the prototype feature of javascript, that's all.

HTH, let me know how it works for you ...

4 comments:

  1. Very nice and succinct write up.

    ReplyDelete
  2. wait for more interesting topic on object of js...

    ReplyDelete
  3. @Joe McCann, thanks, glad you like it

    ReplyDelete
  4. Really begins to open up the true power of javascript. Thanks.

    -sud

    ReplyDelete

Note: Only a member of this blog may post a comment.