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:
- Set the prototype, so that it can inherit the properties (data and
methods) of
Shape
, and - 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.
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.