Object-Oriented Javascript

From Jonathan Gardner's Tech Wiki
Jump to: navigation, search

Introduction

Javascript's object-oriented system is based on prototypes. Prototypical systems tend to be simpler than class-based object-oriented systems.

You can build a class-based object-oriented system on top of the prototypical system, but it is not necessary.

How it Works

In any OO system, it is necessary to understand how the following mechanisms work:

  1. Instantiation
  2. Attribute access.
  3. Method binding.
  4. Inheritance.

Let's discuss each one.

Instantiation

Instantiation means "creating new objects". In Javascript, you use the 'new' syntax.

var new_object = new object(parameters);

Note the following:

  • The new keyword.
  • The object must be a function.
  • The parameters are passed to the function as-is.
  • Inside the function, this refers to the newly created object.
  • The function's return value is ignored.

If you forget the 'new' keyword, then new_object will be assigned to the return value of the function, which is typically 'undefined' since constructors shouldn't return values. If you try to use 'undefined' as an object, you are going to get a message saying that it is undefined.

There is a short-cut syntax to create objects that are derived directly from Object.

var new_object = { }

This will create a new object from Object. You can specify "attribute:value" pairs inside the curly braces, separated by commas.

Attribute Access

Objects are dicts (or hashmaps or hashes, depending on what you call them.) You can access attributes in one of two ways:

  • The dot syntax: 'object.attribute'. Note that the attribute must be a valid variable name, which includes symbols not typically allowed in other languages such as C or Python.
  • Index lookup syntax: 'object["attribute"]. This syntax allows you to have attributes which are not valid variable names. Note that the contents of the square braces are evaluated, so you can put arbitrary expressions in there.

What does access do?

First, it looks at the immediate object.

If object has not had those values set, then it will look in the creating function's 'prototype' attribute's object for the attribute. This is done ad infinitum until an object has a blank prototype. An example is demonstrated under Inheritance below.

If no parent object has the attribute set, then 'undefined' is returned.

To set an attribute, assign to it. Reference the attribute using the same syntax as accessing the attribute. Note that if the attribute was defined in a parent object, assigning to it in a child object doesn't affect the parent object. It simply overrides it for that particular instance.

To delete an attribute, you can either use the delete statement, or you can assign it to undefined. The two are equivalent. Note that deleting an attribute will not delete it from the parent.

function foo() {};
foo.prototype = {attr: 5};
bar = new foo();    // bar.attr is now 5
delete bar.attr;    // foo.prototype.attr is still 5, as is bar.attr.

Method Binding

When you access an attribute that is callable in Javascript, it will be bound with 'this' set to the object which owns the attribute. For example:

o = { }; // Curly-brace instantiation
o.method = function(a) {
    this.attribute = a;
};
o.method(5); // 'this' will be set to o.
o.attribute; // => 5

Note that you cannot pass around bound methods. This is different from Python. However, it is trivial to create a callable, bound method using anonymous functions. (See, for instance, MochiKit's 'bind' function.)

var bind_method = function(obj, method) {
    return function() {return method.apply(obj, arguments);};
}

Be very careful! For every function call, 'this' will be reset. That means that the 'this' inside anonymous functions is likely going to be set to 'window'. A common idiom to work around this is:

function() {
    var self = this;
    ...
    function() {
        self.foo(); // Calling the outer this' foo.
    }
}

Inheritance

We hinted at inheritance earlier under Attribute Access. Here, we will examine it in detail.

Creating Function

Every object created remembers the function that acted as its constructor. For instance, in the following code:

var instance = new my_object();

The variable 'instance' will refer to an object which remembers its creating function, which was 'my_object'. This should never change, unless you start mucking around with non-standard attributes.

However, the creating function is not really the prototype of the object! That is, the attributes of the creating function are not shared with objects created from that function.

var foo = function() { };
foo.bar = 5;
var baz = new foo();
// baz.bar is undefined!

prototype

If the creating function has an attribute called 'prototype', then those attributes will be available for all objects created from the creating function. Example:

function A() { };
A.prototype = { a: 5 }
var instance = new creator();
instance.a; // => 5
A.prototype.a = 7
instance.a; // => 7

What should you assign to the prototype? Why, an instance of the base type. If you see a lot of code that looks like this:

var X = function() {
    // constructor
};
X.prototype = new Y();

Then you are doing it right.

Typically, you want to extend the prototype of the derived object. So, you should use a function like MochiKit's update that will transfer attributes into the prototypical object from a list of attributes.

var X = function() {
    // constructor
};
X.prototype = MochiKit.base.update(
    new Y(),
    { ... additional attributes here ... }
);

This is so common that you should write a function to create objects derived from other objects. You'll need to specify:

  • The constructor
  • The parent object (the instance of it)
  • The additional attributes.

Note: Don't think that you can reassign the prototype after objects have already been created. Don't do this, it's not pretty and it may not work the way you expect it to. However, manipulating the prototype is OK.

instanceof

instanceof is an operator that checks to see what the creating function of the object was, or any of the object's prototypes were, created from that function.

var a = function() {};
var b = function() {};
var c = function() {};
c.prototype = new a();

// The following are all true
new a() instanceof a
new b() instanceof b
new c() instanceof c
new c() instanceof a

// The following are all false.
new a() instanceof b
new a() instanceof c
new b() instanceof a
new b() instanceof c
new c() instanceof b

How do you call super methods? This is rather easy.

obj.prototype.method.call(obj, ...);

or

obj.prototype.method.apply(obj, [...]);

(Of course, if the instance hasn't overridden that method, then you'd have to write prototype.prototype.)

Two Constructors

In order to make prototypical Javascript work for you, you're going to have to write two constructors.

The first is the creating function. This cannot assign any attributes that cannot be shared by many objects deriving from this. That is, anything that doesn't belong in prototype shouldn't be assigned here.

The second is the instance initializer. This is called on derived objects that are not going to be used as a prototype.

This means that there are really two ways to make a new object:

  • For a prototype of another creating function.
  • As an object that isn't going to be a prototype.

Major Differences Between Javascript and Java

  1. There is no such thing as a class.
  2. There are no interfaces, either.
  3. Objects may only derive from one object. (Through the prototype attribute of the creating function.)

Takeaway Tools

  1. A new class of objects is defined by a creating class with a prototype that describes the behavior of that class of objects.
  2. Prototypes are different than instances of an object since they need to be put together differently.

See Also