In the early nineties, a thing called object-oriented programming stirred up the software industry. Most of the ideas behind it were not really new at the time, but they had finally gained enough momentum to start rolling, to become fashionable. Books were being written, courses given, programming languages developed. All of a sudden, everybody was extolling the virtues of object-orientation, enthusiastically applying it to every problem, convincing themselves they had finally found the right way to write programs.
These things happen a lot. When a process is hard and confusing, people are always on the lookout for a magic solution. When something looking like such a solution presents itself, they are prepared to become devoted followers. For many programmers, even today, object-orientation (or their view of it) is the gospel. When a program is not 'truly object-oriented', whatever that means, it is considered decidedly inferior.
Few fads have managed to stay popular for as long as this one, though. Object-orientation's longevity can largely be explained by the fact that the ideas at its core are very solid and useful. In this chapter, we will discuss these ideas, along with JavaScript's (rather eccentric) take on them. The above paragraphs are by no means meant to discredit these ideas. What I want to do is warn the reader against developing an unhealthy attachment to them.
As the name suggests, object-oriented programming is related to
objects. So far, we have used objects as loose aggregations of values,
adding and altering their properties whenever we saw fit. In an
object-oriented approach, objects are viewed as little worlds of their
own, and the outside world may touch them only through a limited and
well-defined interface, a number of specific methods and properties.
The 'reached list' we used at the end of chapter 7 is an example of
this: We used only three functions, makeReachedList
, storeReached
,
and findReached
to interact with it. These three functions form an
interface for such objects.
The Date
, Error
, and BinaryHeap
objects we have seen also work
like this. Instead of providing regular functions for working with the
objects, they provide a way to create such objects, using the new
keyword, and a number of methods and properties that provide the rest
of the interface.
One way to give an object methods is to simply attach function values to it.
var rabbit = {}; rabbit.speak = function(line) { print("The rabbit says '", line, "'"); }; rabbit.speak("Well, now you're asking me.");
In most cases, the method will need to know who it should act on.
For example, if there are different rabbits, the speak
method must
indicate which rabbit is speaking. For this purpose, there is a
special variable called this
, which is always present when a
function is called, and which points at the relevant object when the
function is called as a method. A function is called as a method when
it is looked up as a property, and immediately called, as in
object.method()
.
function speak(line) { print("The ", this.adjective, " rabbit says '", line, "'"); } var whiteRabbit = {adjective: "white", speak: speak}; var fatRabbit = {adjective: "fat", speak: speak}; whiteRabbit.speak("Oh my ears and whiskers, how late it's getting!"); fatRabbit.speak("I could sure use a carrot right now.");
I can now clarify the mysterious first argument to the apply
method, for which we always used null
in chapter 6. This argument can be
used to specify the object that the function must be applied to. For
non-method functions, this is irrelevant, hence the null
.
speak.apply(fatRabbit, ["Yum."]);
Functions also have a call
method, which is similar to apply
,
but you can give the arguments for the function separately instead of
as an array:
speak.call(fatRabbit, "Burp.");
The new
keyword provides a convenient way of creating new objects.
When a function is called with the word new
in front of it, its
this
variable will point at a new object, which it will
automatically return (unless it explicitly returns something else).
Functions used to create new objects like this are called
constructors. Here is a constructor for rabbits:
function Rabbit(adjective) { this.adjective = adjective; this.speak = function(line) { print("The ", this.adjective, " rabbit says '", line, "'"); }; } var killerRabbit = new Rabbit("killer"); killerRabbit.speak("GRAAAAAAAAAH!");
It is a convention, among JavaScript programmers, to start the names of constructors with a capital letter. This makes it easy to distinguish them from other functions.
Why is the new
keyword even necessary? After all, we could have
simply written this:
function makeRabbit(adjective) { return { adjective: adjective, speak: function(line) {/*etc*/} }; } var blackRabbit = makeRabbit("black");
But that is not entirely the same. new
does a few things behind the
scenes. For one thing, our killerRabbit
has a property called
constructor
, which points at the Rabbit
function that created
it. blackRabbit
also has such a property, but it points at the
Object
function.
show(killerRabbit.constructor); show(blackRabbit.constructor);
Where did the constructor
property come from? It is part of the
prototype of a rabbit. Prototypes are a powerful, if somewhat
confusing, part of the way JavaScript objects work. Every object is
based on a prototype, which gives it a set of inherent properties. The
simple objects we have used so far are based on the most basic
prototype, which is associated with the Object
constructor. In fact,
typing {}
is equivalent to typing new Object()
.
var simpleObject = {}; show(simpleObject.constructor); show(simpleObject.toString);
toString
is a method that is part of the Object
prototype. This
means that all simple objects have a toString
method, which converts
them to a string. Our rabbit objects are based on the prototype
associated with the Rabbit
constructor. You can use a constructor's
prototype
property to get access to, well, their prototype:
show(Rabbit.prototype); show(Rabbit.prototype.constructor);
Every function automatically gets a prototype
property, whose
constructor
property points back at the function. Because the rabbit
prototype is itself an object, it is based on the Object
prototype,
and shares its toString
method.
show(killerRabbit.toString == simpleObject.toString);
Even though objects seem to share the properties of their prototype, this sharing is one-way. The properties of the prototype influence the object based on it, but the properties of this object never change the prototype.
The precise rules are this: When looking up the value of a property,
JavaScript first looks at the properties that the object itself has.
If there is a property that has the name we are looking for, that is
the value we get. If there is no such property, it continues searching
the prototype of the object, and then the prototype of the prototype,
and so on. If no property is found, the value undefined
is given. On
the other hand, when setting the value of a property, JavaScript
never goes to the prototype, but always sets the property in the
object itself.
Rabbit.prototype.teeth = "small"; show(killerRabbit.teeth); killerRabbit.teeth = "long, sharp, and bloody"; show(killerRabbit.teeth); show(Rabbit.prototype.teeth);
This does mean that the prototype can be used at any time to add new properties and methods to all objects based on it. For example, it might become necessary for our rabbits to dance.
Rabbit.prototype.dance = function() { print("The ", this.adjective, " rabbit dances a jig."); }; killerRabbit.dance();
And, as you might have guessed, the prototypical rabbit is the perfect
place for values that all rabbits have in common, such as the speak
method. Here is a new approach to the Rabbit
constructor:
function Rabbit(adjective) { this.adjective = adjective; } Rabbit.prototype.speak = function(line) { print("The ", this.adjective, " rabbit says '", line, "'"); }; var hazelRabbit = new Rabbit("hazel"); hazelRabbit.speak("Good Frith!");
The fact that all objects have a prototype and receive some properties
from this prototype can be tricky. It means that using an object to
store a set of things, such as the cats from chapter 4, can go wrong.
If, for example, we wondered whether there is a cat called
"constructor"
, we would have checked it like this:
var noCatsAtAll = {}; if ("constructor" in noCatsAtAll) print("Yes, there definitely is a cat called 'constructor'.");
This is problematic. A related problem is that it can often be
practical to extend the prototypes of standard constructors such as
Object
and Array
with new useful functions. For example, we could
give all objects a method called properties
, which returns an array
with the names of the (non-hidden) properties that the object has:
Object.prototype.properties = function() { var result = []; for (var property in this) result.push(property); return result; }; var test = {x: 10, y: 3}; show(test.properties());
And that immediately shows the problem. Now that the Object
prototype has a property called properties
, looping over the
properties of any object, using for
and in
, will also give us
that shared property, which is generally not what we want. We are
interested only in the properties that the object itself has.
Fortunately, there is a way to find out whether a property belongs to
the object itself or to one of its prototypes. Unfortunately, it does
make looping over the properties of an object a bit clumsier. Every
object has a method called hasOwnProperty
, which tells us whether
the object has a property with a given name. Using this, we could
rewrite our properties
method like this:
Object.prototype.properties = function() { var result = []; for (var property in this) { if (this.hasOwnProperty(property)) result.push(property); } return result; }; var test = {"Fat Igor": true, "Fireball": true}; show(test.properties());
And of course, we can abstract that into a higher-order
function. Note that the action
function is called with both the name
of the property and the value it has in the object.
function forEachIn(object, action) { for (var property in object) { if (object.hasOwnProperty(property)) action(property, object[property]); } } var chimera = {head: "lion", body: "goat", tail: "snake"}; forEachIn(chimera, function(name, value) { print("The ", name, " of a ", value, "."); });
But, what if we find a cat named hasOwnProperty
? (You never know.)
It will be stored in the object, and the next time we want to go over
the collection of cats, calling object.hasOwnProperty
will fail,
because that property no longer points at a function value. This can
be solved by doing something even uglier:
function forEachIn(object, action) { for (var property in object) { if (Object.prototype.hasOwnProperty.call(object, property)) action(property, object[property]); } } var test = {name: "Mordecai", hasOwnProperty: "Uh-oh"}; forEachIn(test, function(name, value) { print("Property ", name, " = ", value); });
(Note: This example does not currently work correctly in Internet Explorer 8, which apparently has some problems with overriding built-in prototype properties.)
Here, instead of using the method found in the object itself, we get
the method from the Object
prototype, and then use call
to apply
it to the right object. Unless someone actually messes with the method
in Object.prototype
(don't do that), this should work correctly.
hasOwnProperty
can also be used in those situations where we have
been using the in
operator to see whether an object has a specific
property. There is one more catch, however. We saw in chapter 4 that
some properties, such as toString
, are 'hidden', and do not show up
when going over properties with for
/in
. It turns out that browsers
in the Gecko family (Firefox, most importantly) give every object a
hidden property named __proto__
, which points to the prototype of
that object. hasOwnProperty
will return true
for this one, even
though the program did not explicitly add it. Having access to the
prototype of an object can be very convenient, but making it a
property like that was not a very good idea. Still, Firefox is a
widely used browser, so when you write a program for the web you have
to be careful with this. There is a method propertyIsEnumerable
,
which returns false
for hidden properties, and which can be used to
filter out strange things like __proto__
. An expression such as this
one can be used to reliably work around this:
var object = {foo: "bar"}; show(Object.prototype.hasOwnProperty.call(object, "foo") && Object.prototype.propertyIsEnumerable.call(object, "foo"));
Nice and simple, no? This is one of the not-so-well-designed aspects of JavaScript. Objects play both the role of 'values with methods', for which prototypes work great, and 'sets of properties', for which prototypes only get in the way.
Writing the above expression every time you need to check whether a
property is present in an object is unworkable. We could put it into a
function, but an even better approach is to write a constructor and a
prototype specifically for situations like this, where we want to
approach an object as just a set of properties. Because you can use it
to look things up by name, we will call it a Dictionary
.
function Dictionary(startValues) { this.values = startValues || {}; } Dictionary.prototype.store = function(name, value) { this.values[name] = value; }; Dictionary.prototype.lookup = function(name) { return this.values[name]; }; Dictionary.prototype.contains = function(name) { return Object.prototype.hasOwnProperty.call(this.values, name) && Object.prototype.propertyIsEnumerable.call(this.values, name); }; Dictionary.prototype.each = function(action) { forEachIn(this.values, action); }; var colours = new Dictionary({Grover: "blue", Elmo: "orange", Bert: "yellow"}); show(colours.contains("Grover")); show(colours.contains("constructor")); colours.each(function(name, colour) { print(name, " is ", colour); });
Now the whole mess related to approaching objects as plain sets of
properties has been 'encapsulated' in a convenient interface: one
constructor and four methods. Note that the values
property of a
Dictionary
object is not part of this interface, it is an internal
detail, and when you are using Dictionary
objects you do not need to
directly use it.
Whenever you write an interface, it is a good idea to add a comment with a quick sketch of what it does and how it should be used. This way, when someone, possibly yourself three months after you wrote it, wants to work with the interface, they can quickly see how to use it, and do not have to study the whole program.
Most of the time, when you are designing an interface, you will soon find some limitations and problems in whatever you came up with, and change it. To prevent wasting your time, it is advisable to document your interfaces only after they have been used in a few real situations and proven themselves to be practical. ― Of course, this might make it tempting to forget about documentation altogether. Personally, I treat writing documentation as a 'finishing touch' to add to a system. When it feels ready, it is time to write something about it, and to see if it sounds as good in English (or whatever language) as it does in JavaScript (or whatever programming language).
The distinction between the external interface of an object and its internal details is important for two reasons. Firstly, having a small, clearly described interface makes an object easier to use. You only have to keep the interface in mind, and do not have to worry about the rest unless you are changing the object itself.
Secondly, it often turns out to be necessary or practical to change something about the internal implementation of an object type1, to make it more efficient, for example, or to fix some problem. When outside code is accessing every single property and detail in the object, you can not change any of them without also updating a lot of other code. If outside code only uses a small interface, you can do what you want, as long as you do not change the interface.
Some people go very far in this. They will, for example, never include
properties in the interface of object, only methods ― if their object
type has a length, it will be accessible with the getLength
method,
not the length
property. This way, if they ever want to change their
object in such a way that it no longer has a length
property, for
example because it now has some internal array whose length it must
return, they can update the function without changing the interface.
My own take is that in most cases this is not worth it. Adding a
getLength
method which only contains return this.length;
mostly
just adds meaningless code, and, in most situations, I consider
meaningless code a bigger problem than the risk of having to
occasionally change the interface to my objects.
Adding new methods to existing prototypes can be very convenient.
Especially the Array
and String
prototypes in JavaScript could use
a few more basic methods. We could, for example, replace forEach
and
map
with methods on arrays, and make the startsWith
function we
wrote in chapter 4 a method on strings.
However, if your program has to run on the same web-page as another
program (either written by you or by someone else) which uses
for
/in
naively ― the way we have been using it so far ― then
adding things to prototypes, especially the Object
and Array
prototype, will definitely break something, because these loops will
suddenly start seeing those new properties. For this reason, some
people prefer not to touch these prototypes at all. Of course, if you
are careful, and you do not expect your code to have to coexist with
badly-written code, adding methods to standard prototypes is a
perfectly good technique.
In this chapter we are going to build a virtual terrarium, a tank with insects moving around in it. There will be some objects involved (this is, after all, the chapter on object-oriented programming). We will take a rather simple approach, and make the terrarium a two-dimensional grid, like the second map in chapter 7. On this grid there are a number of bugs. When the terrarium is active, all the bugs get a chance to take an action, such as moving, every half second.
Thus, we chop both time and space into units with a fixed size ― squares for space, half seconds for time. This usually makes things easier to model in a program, but of course has the drawback of being wildly inaccurate. Fortunately, this terrarium-simulator is not required to be accurate in any way, so we can get away with it.
A terrarium can be defined with a 'plan', which is an array of strings. We could have used a single string, but because JavaScript strings must stay on a single line it would have been a lot harder to type.
var thePlan = ["############################", "# # # o ##", "# #", "# ##### #", "## # # ## #", "### ## # #", "# ### # #", "# #### #", "# ## o #", "# o # o ### #", "# # #", "############################"];
The "#"
characters are used to represent the walls of the terrarium
(and the ornamental rocks lying in it), the "o"
s represent bugs, and
the spaces are, as you might have guessed, empty space.
Such a plan-array can be used to create a terrarium-object. This
object keeps track of the shape and content of the terrarium, and lets
the bugs inside move. It has four methods: Firstly toString
, which
converts the terrarium back to a string similar to the plan it was
based on, so that you can see what is going on inside it. Then there
is step
, which allows all the bugs in the terrarium to move one
step, if they so desire. And finally, there are start
and stop
,
which control whether the terrarium is 'running'. When it is running,
step
is automatically called every half second, so the bugs keep
moving.
The points on the grid will be represented by objects again.
In chapter 7 we used three functions, point
, addPoints
, and
samePoint
to work with points. This time, we will use a constructor
and two methods. Write the constructor Point
, which takes two
arguments, the x and y coordinates of the point, and produces an
object with x
and y
properties. Give the prototype of this
constructor a method add
, which takes another point as argument and
returns a new point whose x
and y
are the sum of the x
and y
of the two given points. Also add a method isEqualTo
, which takes a
point and returns a boolean indicating whether the this
point refers
to the same coordinates as the given point.
Apart from the two methods, the x
and y
properties are also part
of the interface of this type of objects: Code which uses point
objects may freely retrieve and modify x
and y
.
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.add = function(other) { return new Point(this.x + other.x, this.y + other.y); }; Point.prototype.isEqualTo = function(other) { return this.x == other.x && this.y == other.y; }; show((new Point(3, 1)).add(new Point(2, 4)));
Make sure your version of add
leaves the this
point intact and
produces a new point object. A method which changes the current point
instead would be similar to the +=
operator, whereas this one is
like the +
operator.
When writing objects to implement a certain program, it is not always very clear which functionality goes where. Some things are best written as methods of your objects, other things are better expressed as separate functions, and some things are best implemented by adding a new type of object. To keep things clear and organised, it is important to keep the amount of methods and responsibilities that an object type has as small as possible. When an object does too much, it becomes a big mess of functionality, and a formidable source of confusion.
I said above that the terrarium object will be responsible for storing its contents and for letting the bugs inside it move. Firstly, note that it lets them move, it doesn't make them move. The bugs themselves will also be objects, and these objects are responsible for deciding what they want to do. The terrarium merely provides the infrastructure that asks them what to do every half second, and if they decide to move, it makes sure this happens.
Storing the grid on which the content of the terrarium is kept can get
quite complex. It has to define some kind of representation, ways to
access this representation, a way to initialise the grid from a 'plan'
array, a way to write the content of the grid to a string for the
toString
method, and the movement of the bugs on the grid. It would
be nice if part of this could be moved into another object, so that
the terrarium object itself doesn't get too big and complex.
Whenever you find yourself about to mix data representation and
problem-specific code in one object, it is a good idea to try and put
the data representation code into a separate type of object. In this
case, we need to represent a grid of values, so I wrote a Grid
type,
which supports the operations that the terrarium will need.
To store the values on the grid, there are two options. One can use an array of arrays, like this:
var grid = [["0,0", "1,0", "2,0"], ["0,1", "1,1", "2,1"]]; show(grid[1][2]);
Or the values can all be put into a single array. In this case, the
element at x
,y
can be found by getting the element at position x
+ y * width
in the array, where width
is the width of the grid.
var grid = ["0,0", "1,0", "2,0", "0,1", "1,1", "2,1"]; show(grid[2 + 1 * 3]);
I chose the second representation, because it makes it much
easier to initialise the array. new Array(x)
produces a new array of
length x
, filled with undefined
values.
function Grid(width, height) { this.width = width; this.height = height; this.cells = new Array(width * height); } Grid.prototype.valueAt = function(point) { return this.cells[point.y * this.width + point.x]; }; Grid.prototype.setValueAt = function(point, value) { this.cells[point.y * this.width + point.x] = value; }; Grid.prototype.isInside = function(point) { return point.x >= 0 && point.y >= 0 && point.x < this.width && point.y < this.height; }; Grid.prototype.moveValue = function(from, to) { this.setValueAt(to, this.valueAt(from)); this.setValueAt(from, undefined); };
We will also need to go over all the elements of the grid, to find the
bugs we need to move, or to convert the whole thing to a string. To
make this easy, we can use a higher-order function that takes an
action as its argument. Add the method each
to the prototype of
Grid
, which takes a function of two arguments as its argument. It
calls this function for every point on the grid, giving it the point
object for that point as its first argument, and the value that is on
the grid at that point as second argument.
Go over the points starting at 0
,0
, one row at a time, so that
1
,0
is handled before 0
,1
. This will make it easier to write
the toString
function of the terrarium later. (Hint: Put a for
loop for the x
coordinate inside a loop for the y
coordinate.)
It is advisable not to muck about in the cells
property of the grid
object directly, but use valueAt
to get at the values. This way, if
we decide (for some reason) to use a different method for storing the
values, we only have to rewrite valueAt
and setValueAt
, and the
other methods can stay untouched.
Grid.prototype.each = function(action) { for (var y = 0; y < this.height; y++) { for (var x = 0; x < this.width; x++) { var point = new Point(x,