\( \newcommand{\vecIII}[3]{\left[\begin{array}{c} #1\\#2\\#3 \end{array}\right]} \newcommand{\vecIV}[4]{\left[\begin{array}{c} #1\\#2\\#3\\#4 \end{array}\right]} \newcommand{\Choose}[2]{ { { #1 }\choose{ #2 } } } \newcommand{\vecII}[2]{\left[\begin{array}{c} #1\\#2 \end{array}\right]} \newcommand{\vecIII}[3]{\left[\begin{array}{c} #1\\#2\\#3 \end{array}\right]} \newcommand{\vecIV}[4]{\left[\begin{array}{c} #1\\#2\\#3\\#4 \end{array}\right]} \newcommand{\matIIxII}[4]{\left[ \begin{array}{cc} #1 & #2 \\ #3 & #4 \end{array}\right]} \newcommand{\matIIIxIII}[9]{\left[ \begin{array}{ccc} #1 & #2 & #3 \\ #4 & #5 & #6 \\ #7 & #8 & #9 \end{array}\right]} \)

CS307: Object-Oriented Programming in JavaScript

This reading is about how Java-style OOP is done in JavaScript. It's not meant to cover all aspects of OOP in JavaScript nor to be a tutorial on OOP from the ground up.

Review and Terminology

You already know a lot about Object-Oriented Programming, from your experience with Java and Python. Let's just recap a few of these ideas, and some of the associated terminology. I'm not going to be very formal here.

  • An object is a representation of some data and the methods that operate on that data. A web browser might have an object that represents a hyperlink and another object that represents a div.
  • The objects are often organized into types or kinds, called classes. For example, there might be classes called Hyperlink and Div.
  • An object belonging to a particular class is called an instance of that class. There could be many instances of the Div class on a web page and many instances of the Hyperlink class as well.
  • The particular data of an instance is held in variables that are often called instance variables. For example, two instances of the Div class might have different widths and heights, and each stores its own width and height in instance variables.
  • An object will usually have functions, called methods, for doing stuff to the instance. For example, a Div object might have a method to resize the div, or hide it, or some such.
  • A class will usually have a function for making an instance, and that function is sometimes called a constructor.
  • A class can sometimes inherit instance variables and methods from another classes.

The JavaScript Object Data Structure

The JavaScript language has a simple data structure for heterogeneous data, that is, treating a collection of data (properties and values) as a single thing. For example, if we have a rectangle that has a width of 3 and a height of 5, we could store it like this:

var r1 = {width: 3, height: 5};

(You can start a JavaScript console and look at r1 if you'd like, as you can with all the examples in this reading, except the ones with "execute this" buttons.)

We're going to call this data structure an object, even though we don't have any methods, classes, constructors or any of that other OOP stuff. In a few minutes, we'll build all of that. The object does, however, have two properties, which play the role of our instance variables.

Constructor

Let's start with a constructor. Let's define a simple function to make a rectangle object:

function makeRect1(w,h) {
    return {width: w, height: h};
}

Every call to makeRect1 will return a new instance of a rectangle. So, there's implicitly a class.

Methods

You can, of course, use the JavaScript syntax to read and update the contents of these rectangles. There are two syntaxes in JavaScript to reference a property of an object. If the property's name is simple and follows the rules for variables (no embedded spaces or other special characters), you can just give a dot (period) and the name of the property:

var r = makeRect1(3,5);
alert("its width is "+r.width);  // 3
r.width = 2*r.width;
alert("doubled to "+r.width); // 6

(The other syntax uses square brackets, but we won't be using that syntax in these notes.)

While we can use the JavaScript syntax, we'd rather have methods to get and set the data. To make the setter function just slightly non-trivial, we'll avoid negative lengths by storing the absolute value. Here's how:

function makeRect2(w,h) {
    return {width: w,
            height: h,
            getWidth: function () { return this.width; },
            setWidth: function (w) { this.width = Math.abs(w); }
           }; // end of object
} // end of makeRect2

(Note that we haven't used absolute value with the arguments to the constructor, so it's still possible to construct bogus instances of a rectangle. We'll leave that refinement as an exercise to the reader.)

The use of the this variable in methods should remind you a lot of OOP in Java.

You can try it out as follows:

var r = makeRect2(3,5);
alert("width of r: "+r.getWidth());  // 3
r.setWidth(2*r.getWidth());
alert("doubled to "+r.getWidth());  // 6

How does the code work? We've already seen that we can use a dot to access a property of an object, and here we have two functions that are properties of the object, so the syntax r.getWidth() reaches into the object stored in r, accesses the property getWidth, and invokes that function. The tricky part is that JavaScript binds the special variable this to the object while the method is executing, so the method can refer to this in its body. In this case, the getWidth method reaches into this to get and return the current width.

Wait. What?

JavaScript supports OOP by having a special method-calling syntax, where anytime the code looks like obj.meth(args), the value of this is bound to obj while meth is executing.

The new operator

There's also additional support for creating instances. Rather than create our own object, as we did in makeRect2, we can use the new operator along with a constructor function. Here's an example:

function Rect3(w,h) {
    this.width = w;
    this.height = h;
    this.getWidth = function () { return this.width; };
    this.setWidth = function (w) { this.width = Math.abs(w); };
}    

You use such a constructor function along with the new operator as follows:

var r = new Rect3(4,6);
alert("r's width is "+r.getWidth());  // 4
r.setWidth(2*r.getWidth());
alert("its width is now "+r.getWidth());  // 8

The new operator creates a new, empty object, and binds it to this while the constructor function is running. The object is returned (regardless of what the function returns).

Thus, we would say that we are creating an instance of the Rect3 class, and so this special constructor function gives us classes.

Note that a convention of the JavaScript language is that constructor functions like Rect3 are spelled with an initial capital letter, as a reminder to invoke them with new. Invoking them with new is not enforced though (that is, there's no error message), and if you forget to do so, all the assignments are to global variables instead of instance variables. The result is not pretty.

Summary So Far

We've seen how to define classes and methods, create instances, and generally do Object-Oriented programming in JavaScript. You almost know enough to do the OOP that you'll need to use in this course, and to understand more of the Three.js source code when you look at it. However, Three.js uses inheritance, so we'll turn to that now.

Prototypes

One flaw with the Rect3 class is that every instance has two methods (and would have four in a more complete implementation), and these methods are identical copies of each other, instead of being the exact same function.

Hunh? What does that mean? JavaScript implementations create functions as chunks of compiled code taking up space in memory. If you have two identical functions, they will have identical code, but taking up two chunks of memory. If you just have a few copies of the function, it's not a big deal, but if you have many copies, it can clearly get out of hand, wasting space in the browser's memory and causing your code to run slower or even crash the browser. Let's take an example:

var add1 = function (x,y) { return x+y; };
var add2 = function (x,y) { return x+y; };
var add3 = add1;
alert("are add1 and add2 the same? "+(add1===add2));  // false
alert("are add1 and add3 the same? "+(add1===add3));  // true

We can observe the same thing about the methods in our rectangle instances. In the following example, we want the two instances to be different objects, even though they happen to have the same values at the moment, because we should be able to change their widths independently.

var r1 = new Rect3(4,5);
var r2 = new Rect3(4,5);
alert("are they the same? "+(r1===r2));  // false
r1.setWidth(6);
alert("are their methods the same? "+(r1.getWidth===r2.getWidth));  // false

The two instances can share their methods by using a feature of JavaScript's objects called prototypes. Every object has a prototype that is another object, forming a chain. If a property is looked up in an object and that property isn't found, the next object in the prototype chain is checked. This allows for a kind of inheritance (which we'll get to soon), but also for the sharing of common code, such as these methods.

Here's how we can share methods among our different rectangle objects by adding properties to the function's prototype:

function Rect3sharing(w,h) {
    this.width = w;
    this.height = h;
}    

Rect3sharing.prototype.getWidth = function () { return this.width; };

Rect3sharing.prototype.setWidth = function (w) { this.width = Math.abs(w); }

Now, let's see this in action:

var r1 = new Rect3sharing(4,5);
var r2 = new Rect3sharing(4,5);
alert("are they the same? "+(r1===r2));  // false
r1.setWidth(6);
alert("are their methods the same? "+(r1.getWidth===r2.getWidth));  // true!

Inheritance

Three.js uses inheritance to define core behavior of objects/classes on a parent, and then extending them with child classes. For example, you can create a box in your scene using the THREE.BoxGeometry class, whose constructor allow you to specify the length of each face and the number of segments along each face. Or, you can add a cylinder to your scene using THREE.CylinderGeometry whose constructor allows you to specify the radius of the top and bottom and so forth.

Both of these classes inherit from THREE.Geometry, which has properties like vertices, faces, colors, and even much more. This is a sensible and useful inheritance hierarchy, sharing a lot of code. (The Geometry.js implementation is nearly 800 lines of code. The BoxGeometry.js implementation is only an additional 124 lines of code, and CylinderGeometry.js implementation is only 165 lines of code more. So the 800 lines of code is shared among all of Geometry's 18+ children.)

Rather than delve into all of that Three.js code, let's define a Shape class with subclasses like Rectangle, Circle and Triangle. Since those shapes are all quite different, it's not clear what useful stuff the subclasses could inherit, but we will implement some instance variables (isShape and typeOfShape) and some methods (area and description). The description method is very like a toString method.

Defining the Shape class is pretty straightforward.

function Shape () {
    this.isShape = true;
    this.typeOfShape = "unknown shape";
}

Shape.prototype.area = function () { return "unknown"; };

Shape.prototype.description =
    function () {
        return("[a "+this.typeOfShape
                    +" of area "
                    +this.area()
                    +"]");
};

Although we would typically not make an instance of this class, we could. In the following, I use the JSON.stringify() function to produce a string representation of an object that, with braces and colons and such, like we saw in our first code example.

var s = new Shape();
alert("object is "+JSON.stringify(s));  // {isShape:true,typeOfShape:"unknown"}
alert("area is "+s.area());             // unknown
alert("description is "+s.description());  // [a unknown shape of area unknown]

Now the Rectangle subclass needs to do two things:

  1. Set the prototype, so that it can inherit the properties (data and methods) of Shape, and
  2. Call the Shape constructor, so that all the correct instance variables get set.

Both of these are a bit tricky, and will involve some esoteric methods of the Object and Function classes.

The prototype for Rectangle should be set to an instance of a Shape but we don't want to call the Shape constructor for reasons that we don't need to go into here, but you can read about later. Instead, we'll use the create method of Object to create an new object with the specified prototype object and properties. (The Three.js code uses this technique, so I want to mention it to you, in case you look at their source code.)

In our Rectangle constructor, we also want to call the Shape constructor in a way that allows us to ensure that this has the correct value, namely the same value it has in the Rectangle constructor. The call method of a function allows us to invoke it and also specify the value of this.

Here we go:

function Rect3inheriting(w,h) {
    // Invoke Shape's constructor
    Shape.call(this);

    // override defaults from Shape:
    this.typeOfShape = "rectangle";

    // finish our constructor
    this.width = w;
    this.height = h;
}    

// Create a Shape to be the prototype for Rectangles
Rect3inheriting.prototype = Object.create( Shape.prototype );

// Methods for this subclass

Rect3inheriting.prototype.getWidth = function () { return this.width; };

Rect3inheriting.prototype.setWidth = function (w) { this.width = Math.abs(w); }

Rect3inheriting.prototype.area = function () { return this.width*this.height; };

Let's test this out, seeing if we get all the instance variables and methods we want:

var r = new Rect3inheriting(5,6);
alert("object is "+JSON.stringify(r));  // {isShape:true,typeOfShape:"rectangle",width:5,height:6}
alert("area method returns "+r.area()); // 30
alert("the inherited description method returns "+r.description()); // [a rectangle of area 30]

Polymorphism

The Shape subclasses could implement common methods such as area and perimeter. Thus, the knowledge of how to compute the area of a shape has been distributed among the different subclasses, instead of having to write one master area method. This also allows us to write code that can take a heterogeneous list of objects and compute, say, the total area, without having to figure out the type of each object and treat them as different cases.

For example, here is a function that takes a list of shapes and computes their total area:

  function total_area(shape_list) {
      var total = 0;
      var i;
      var len = shape_list.length;
      for( i=0; i < len; i++ ) {
          total += shape_list[i].area();
      }
      return total;                 
 }

The elements of that list could be rectangles, circles or any thing else, as long as the object has an area method. The code is short and sweet, thanks to this polymorphism.

Let's quickly write a Circle class and show the polymorphism idea in action.

  function Circle(radius) {
    // Invoke Shape's constructor
    Shape.call(this);

    // override defaults from Shape:
    this.typeOfShape = "circle";

    // finish our constructor
    this.radius = radius;
}    

// Create a Shape to be the prototype for Circles
Circle.prototype = Object.create( Shape.prototype );

// Circle methods:

Circle.prototype.getRadius = function () { return this.radius; };

Circle.prototype.setRadius = function (r) { this.radius = Math.abs(r); }

Circle.prototype.area = function () { return 2*Math.PI*this.radius; };

Now, let's create a list of circles and rectangles and compute their total area:

var shapes = [];
shapes.push(new Rect3inheriting(3,4));
shapes.push(new Circle(1));
shapes.push(new Rect3inheriting(5,6));
shapes.push(new Circle(2));

alert("our list: \n"
+shapes[0].description()+"\n" // [a rectangle of area 12]
+shapes[1].description()+"\n" // [a circle of area 6.28...]
+shapes[2].description()+"\n" // [a rectangle of area 30]
+shapes[3].description());    // [a circle of area 12.56...]

alert("total area: "+total_area(shapes));  // total area of 60.849...

This kind of polymorphic programming makes coding much more clear and concise, and is often used in graphics programming (and is in Three.js).

Stop Here

Information Hiding

Captain Abstraction is sitting over my shoulder, insisting that I explain how you can make your instance variables private, so that callers can't violate your abstraction barriers. For example, we decided that our rectangles can't have negative widths, so our setWidth method enforces that. However, there's nothing to prevent someone from doing the following:

var r = new Rect3(4,6);
r.width = -5;
alert("its width is "+r.getWidth());  // -5

We can achieve the information hiding we want by using closures. The instance variables will all be closure variables, and the object will only contain the public methods. Here's an example, including a private method called noNeg.

function Rect4(w,h) {
    // private method
    var noNeg = function (x) {
                    if(x<0) {
                       throw new Error("No negative lengths: "+x); 
                    } else {
                       return x;
                    }
                };

    // private instance variables, initialized using private method
    var width = noNeg(w);
    var height = noNeg(h);

    this.getWidth = function () { return width; };
    this.setWidth = function (w) {
                       width = noNeg(w);
                     }; // end of setWidth
                               
    this.area = function () { return width*height; };
}    

If you create an instance of Rect4, you'll see that you can get and set the width, but you can't directly change either width or height. You know that both exist because the area method works correctly.

Let's try it. Note that the following example uses the Object.keys() method because the values are all functions, and JSON.stringify() omits properties whose values are functions.

var r = new Rect4(8.5,11);
alert("its properties (keys) are: "+Object.keys(r)); // getWidth,setWidth,area
alert("its width is "+r.getWidth());                 // 8.5
r.setWidth(2*r.getWidth());
alert("its width is now "+r.getWidth());             // 17
alert("its area is "+r.area());                      // 187

Note that we can't share the methods by putting them in the prototype, because they actually are different functions now, since they are closing over different occurrences of the closure variables (width and height).

If there's a way to do both information hiding and method sharing in JavaScript, I don't know it.

Error: Omitting Setting the Prototype

One necessary step to setting up inheritance is to set the prototype of the child to be the an instance of the parent. The prototype will have all the properties (methods and instance variables) that we want to inherit, thereby enabling all that behavior. If we forget that step, our child class won't inherit behavior from the parent, even if we remember to call the parent's constructor.

Here's an example, where the parent is a grocery item, with instance variables name and price and methods to get them.

function GroceryItem(name,price) {
    this.name = name;
    this.price = price;
}

GroceryItem.prototype.getName = function () { return this.name; };
GroceryItem.prototype.getPrice = function () { return this.price; };

The child is a food item, which also has an expiration date. Notice that we call the parent's constructor, which ensures that the instance variables are properly initialized.

function FoodItem(name,price,expiration) {
    GroceryItem.call(this,name,price);
    this.expiration = expiration;
}

FoodItem.prototype.getExpiration = function () { return this.expiration; };

Now to test it out. When you run the following, you'll see that the twinkie has all the right instance variables with all the right values, and has the getExpiration method, but not the getName and getPrice methods.

var twinkie = new FoodItem("twinkie",2.19,"1/1/2032");
alert("object is: "+JSON.stringify(twinkie));  // {name:"twinkie",price:2.19,expiration:"1/1/2032"}
alert("getExpiration is "+twinkie.getExpiration); // function () { return this.expiration }
alert("getName is "+twinkie.getName);             // undefined!

The first two alerts display what we expect, but the third shows that getName is undefined, because we didn't set up the inheritance properly.

Error: Omitting Calling the Parent's Constructor

The other step to setting up inheritance is to call the Parent's constructor function. If you omit that, but remember to set up the prototype, you'll get the methods, but the instance variables won't be set correctly. Let's create another kind of grocery item, namely an "limited" product, where you can only buy so many of them.

function LimitedItem(name,price,limit) {
    // GroceryItem.call(this,name,price); forgot this
    this.limit = limit;
}

// Inherit from GroceryItem
LimitedItem.prototype = Object.create( GroceryItem.prototype );

// Our own methods
LimitedItem.prototype.getLimit = function () { return this.limit; };

Now, we'll see that the getName method exists, but it doesn't return the right value:

// at 99 cents/can, only 10 per customer
var soup = new LimitedItem("soup",0.99,10);  
alert("object is: "+JSON.stringify(soup));   // {limit: 10}
alert("getLimit is "+soup.getLimit());       // 10
alert("getName is "+soup.getName);           // function () { return this.name; }
alert("getName evaluates to "+soup.getName());  // undefined!

The value is "undefined," because it should have been set in the parent's constructor, but the LimitedItem constructor didn't invoke that.

Why Avoid invoking the Constructor?

We know that we need to set the prototype of a child class to an instance of the parent class in order to inherit the parent's properties. Most of the time, you can just do:

  Child.prototype = new Parent();

But in some circumstances, it's better to do the following, which means almost the same as the preceding.

  Child.prototype = Object.create( Parent.prototype );

Why almost? The difference is that the latter makes the right kind of prototype object, without actually invoking the parent constructor, in case that constructor function has some side-effect that you want to avoid (or just consumes a lot of memory).

Here's an example where every time we create an object, we assign it an id, and put it on a list, so that if you know the id of the object, you can easily get it back. We'll call these NumberedObjects. For clarity of the code, let's make the list global; in real life, we would probably make it a property of the NumberedObject class.

var ObjectList = [];

function NumberedObject() {
    ObjectList.push(this);          // put it on the end
    this.id = ObjectList.length-1;  // so its index is known
    console.log("made object "+this.id);
}

function ShowAllNumberedObjects() {
    var i, len = ObjectList.length, result = "";
    for( i=0; i < len; i++ ) {
        result += JSON.stringify(ObjectList[i])+"\n";
    }
    alert(result);
}         

Before we make any child classes, let's see this in action. You can keep clicking the button to make as many objects as you want. But keep track of how many you create, because the issue we'll look at later is having extra numbered objects. (Or, you can just reload this page, to make the list empty again.)

var o = new NumberedObject();
alert("object has id "+o.id);
var p = ObjectList[o.id];
if(p != o) { alert("Ooops! They aren't the same! "); }

Now, let's make two child classes, Strange Objects and Pretty Things. The Strange Objects will invoke the parent's constructor when setting its prototype, but the Pretty Things will use the esoteric Object.create() method.

function StrangeObject(strangeness) {
    NumberedObject.call(this);
    this.strangeness = strangeness;
}

// Invoke the parent's constructor
StrangeObject.prototype = new NumberedObject();

function PrettyThing(prettiness) {
    NumberedObject.call(this);
    this.prettiness = prettiness;
}

// Make an instance of the parent
PrettyThing.prototype = Object.create( NumberedObject.prototype );

Now, let's see how this works:

var so = new StrangeObject("weird");
alert(JSON.stringify(so));
var pt = new PrettyThing("handsome");
alert(JSON.stringify(pt));

No trouble at all, right? But now let's look at all our NumberedObjects:

ShowAllNumberedObjects();

There is an extra, unwanted numbered object with an ID of zero cluttering up our list, because we invoked the NumberedObject constructor to set up the inheritance for Strange Objects. If we'd done that for Pretty Things, we'd have another unwanted object, this time with an ID of one. (We know that they would have IDs of 0 and 1, because they would have been created when the page loads, which would be before any objects you created by clicking.)

All in all, this is an unlikely scenario, so if you want to call the Parents' constructor instead of using Object.create(), by all means, go ahead. Furthermore, many tutorials by many well-respected JavaScript authorities (Douglas Crockford, John Resig and others) do this. However, as I mentioned, you'll see that the Three.js sources use Object.create(), so I wanted to introduce you to this.

A Diabolical Inheritance Bug

I discovered the issue covered in the last section in the context of debugging a particularly insidious bug that I introduced into a Three.js program. What happened was the interaction of three things:

  • The parent class had a non-atomic data structure stored in an instance variable. In the example below, I'll use a list data structure.
  • I invoked the parent's constructor to set the prototype for the child class. That meant that the prototype has a property with a list as its value.
  • I forgot to invoke the parent's constructor in the child's constructor, so that the instances didn't have their own lists.

The first two things are not errors; the third one is the only error, but the result was harder to debug thanks to the second thing.

The simplified example I've created uses lists of nicknames. Here's the parent class, which is just a person and his or her nicknames, including a shared method to report the names.

function Person(name) {
    this.name = name;
    this.nicknames = [];
}

Person.prototype.addNickname =
    function (nickname) {
        this.nicknames.push(nickname);
    };

Person.prototype.allNames =
    function () {
        var result = this.name;
        var i,len = this.nicknames.length;
        for( i=0; i < len; i++ ) {
            result += " AKA "+this.nicknames[i];
        }
        return result;
   };

This is pretty straightforward, and we can see that it works:

var p = new Person("Robert");
p.addNickname("Bobby");
alert("person is "+p.allNames());

And now, I'll define the child class to represent wizards from the world of Harry Potter:

function Wizard(name,house) {
    this.name = name;
    this.house = house;
}

Wizard.prototype = new Person();

Let's create Harry Potter with his nickname and Lord Voldemort with his:

var h = new Wizard("Harry Potter","Gryffindor");
h.addNickname("the boy who lived");

var v = new Wizard("Lord Voldemort","Slytherin");
v.addNickname("You-know-who");
v.addNickname("He-who-must-not-be-named");
v.addNickname("the Dark Lord");

Now, let's look at what we have:

alert(JSON.stringify(h));
alert(JSON.stringify(v));

All perfectly normal so far. Let's look at the nicknames:

alert(h.allNames());
alert(v.allNames());

What?? How is it that they have same nicknames? The answer is that the two lists are, in fact, the very same data structure:

alert("same list? "+(h.nicknames === v.nicknames));
alert("same list in prototype? "+(h.__proto__.nicknames === v.__proto__.nicknames));

The reason for this behavior is that

  • Because I didn't call the parent's constructor, the instances don't have a nicknames property, but
  • because I invoked the parent's constructor in setting the prototype, the Wizard prototype does have that property, but
  • all instances of Wizards share that property, instead of each wizard having his own separate list of nicknames, stored in the instance.

Here's a picture illustrating the idea, where the boxes are prototypes and the ovals (boxes with rounded corners) are instances. Note that Robert has his own list-valued property, while Harry and Voldemort lack that property, so they share the one in the prototype.

harry and lord voldemort share the nickname list stored in the wizard prototype, instead of having their own separate lists

This was made harder to debug because the Wizard prototype had the nicknames property. If I had set the prototype using Object.create(), the prototype would not have had the nicknames property and the addNickname method would have thrown an error, saying that the nicknames property didn't exist.

I imagine one could use this idea for good instead of evil, but for me and my students it produced an evil amount of difficult debugging. Hopefully, this explanation will save you from this mistake.

Classical versus Prototypical

Note that the paradigm above is Class-based OOP, and JavaScript can handle it. However, JavaScript also does OOP using prototypes, where each object can inherit stuff from another object in a prototype chain. Douglas Crockford's excellent book, JavaScript: the Good Parts, describes this difference.