Functions As Factories
Let's say, now that we've been introduced to <canvas>, that we'd like to make our own version of the classic arcade game Asteroids. We know how to create parts of the game: we can write an animation loop, we can move objects around the screen, and we can write functions to draw them. It would be possible to muddle our way through a version of Asteroids using only these techniques, but it would involve managing a lot of complexity for all the different objects and interactions onscreen. Is there a way to make this problem simpler and easier to grasp?
Consider the way Asteroids works: the player must clear the field of floating space rocks by shooting them, while avoiding collisions. Each asteroid that's struck by a missile, however, splits into multiple smaller asteroids, and each of those splits into even smaller asteroids when shot (which is, after all, where the challenge comes from). In order to make this game work, we're going to need to be able to create new asteroids on demand, very easily. We need a constructor function that can generate them for us.
We talk about constructors as though they are a separate kind of function, because in many programming language they are a part of a class definition structure. JavaScript doesn't have classes, so technically every function is a constructor function. All we have to do is add the new keyword in front of it, and we'll get a new object back.
//regular function invocation
var result = normalFunction();
//creating objects with a constructor
var newObject = new Thing();
By convention, functions that are meant to be used as constructors should start with a capital letter for easy visual identification, since using them without the new keyword causes unwelcome side effects.
When a function is called with the new keyword in front, a new object will be created and set as the this value for the function. We can use this inside the constructor to customize that object. For example, we might start creating our asteroids using the following constructor.
var Asteroid = function(size, x, y) {
this.x = x;
this.y = y;
this.size = size;
this.dx = Math.random() * 6 - 3;
this.dy = Math.random() * 6 - 3;
};
In this constructor, we use the arguments to initialize the x, y, and size properties. But we also generated dynamic dx and dy properties, so that the asteroid will have a random drift. We can also add defaults to the properties set by the arguments by using the OR operator--if the arguments weren't provided, they'll be undefined, and JavaScript will use the next value instead.
var Asteroid = function(size, x, y) {
this.x = x || 0;
this.y = y || 0;
this.size = size || 'large';
this.dx = Math.random() * 6 - 3;
this.dy = Math.random() * 6 - 3;
};
Now it's easy to create lots of asteroids, both at start-up and when each asteroid is shot and breaks apart, just by calling our constructor.
var rocks = [];
//let's make 100 asteroids!
for (var i = 0; i < 100; i++) {
var asteroid = new Asteroid();
//add each to our rocks array
rocks.push(asteroid);
}
Prototypes
Constructors like this are fine for properties that change between instances of an object, but what about properties that are the same on every new object? Instead of creating those manually in the constructor, we can use the prototype of the constructor function. The prototype is the object that's used to create this when a function is called as a constructor. For example:
var Thing = function() {
this.x = 1;
}
Thing.prototype = {
isThing: true
}
var t = new Thing();
console.log(t.x); // 1
console.log(t.isThing); // true
//Prototype properties are inherited by existing child objects in a "live" way
console.log(t.inherited); // undefined
Thing.prototype.inherited = true;
console.log(t.inherited); // true
The prototype is shared between all objects created from the same constructor function. If you change the prototype, all those objects will pick up those changes. Changing the individual objects, however, will not update the prototype. Assigning a new value to a property that's inherited from the prototype will "shadow" that property: the local object property is used instead of the prototype property. To really understand how this works, we should understand the prototype chain.
var Parent = function() {
this.type = "parent";
}
Parent.prototype = {
inherited: true
}
var Child = function() {
this.type = "child";
}
Child.prototype = new Parent();
//we can add to the prototype, but not Parent, like this
Child.prototype.notParent = true;
var kid = new Child();
In the code above, we create two constructors. One, the Parent, is the base object. The other, the Child, is based on Parent: its prototype is a "new" Parent, and we say that Child inherits from Parent. Because Child's prototype is an instance of Parent, and not Parent.prototype, we can extend it with new properties that make it distinct from its ancestor (such as notParent above). Finally, we've created an actual Child, named kid.
We never set the value of the inherited property on Child or on kid itself. If we ask for kid.inherited, it's technically undefined on the actual kid object, but before returning a value JavaScript will walk up the prototype chain, looking for it there. kid's prototype is the same as any Child, meaning that it's a Parent. Since Parent objects do have an inherited property, JavaScript returns its value there. If Parent had a prototype from another object, the chain would work up to that. Only if JavaScript reaches the end of the prototype chain and still hasn't found a matching property does it return undefined.
console.log(kid.notParent); // true
console.log(kid.neverDefined); // undefined
console.log(kid.inherited); // true
From the perspective of the prototype chain, "shadow" properties make more sense. When you set a property on a child object that has the same name as a property on the prototype, JavaScript stops looking at the object itself, and never bothers looking up the chain. To remove the property, and allow JavaScript to check the prototype, we can use the delete keyword.
//shadow the chain
kid.inherited = false;
console.log(kid.inherited); // false, because Child and Parent aren't checked
//remove the shadowing property
delete kid.inherited;
console.log(kid.inherited); // true, via the prototype chain
It may be easier to visualize the relationship between objects and their prototypes visually. Each of the square canvases below represents a prototype in the chain, from Object on down. If you click on each canvas, you'll see dots appear down the prototype chain. Drawing on a canvas lower in the chain, of course, will not go to the left (higher on the chain), just as assigning a property on a child does not set it on the parent prototype.
Asteroids, Continued
Keeping all this information about inheritance in mind, let's return to our Asteroids game. In the game's simplistic playing field, there are many kinds of objects: asteroids of three sizes, the player ship, and missiles. All of these objects are different in important ways, but they share many characteristics. They are all affected by inertia as they move, they are all onscreen at their x and y coordinates, and they must be tracked by the game engine for collisions and drawing. Perhaps we can simplify our game by making all these objects inherit from a single prototype that handles the way they're alike, but allows them to be different.
The following code defines a Sprite constructor that will serve as the ancestor for all of our onscreen objects. It contains properties for screen position and speed, defines an update() method that moves them based on momentum, and provides an empty draw() method that does nothing.
//since this is just used for its prototype,
//the constructor doesn't try to change it.
var Sprite = function() {};
Sprite.prototype = {
x: 0,
y: 0,
dx: 0, //horizontal speed
dy: 0, //vertical speed
update: function() {
//update with current "drift" momentum
this.x = this.x + this.dx;
this.y = this.y + this.dy;
},
draw: function() {}
}
Sprite is pretty simple, because it only serves as a way to share functionality between our real classes, such as our Asteroids.
var Asteroid = function(size, x, y) {
this.size = size || "large";
this.x = x || 0;
this.y = y || 0;
this.dx = Math.random() * 6 - 3;
this.dy = Math.random() * 6 - 3;
}
Asteroid.prototype = new Sprite();
//Now we'll add new properties where Asteroids are not like Sprites
//starting with its appearance.
Asteroid.prototype.draw = function() {
canvasContext.beginPath();
canvasContext.arc(this.x, this.y, 20, 0, Math.PI * 2);
canvasContext.stroke();
}
//Here's a player ship as well
var Ship = function(x, y) {
this.x = x || 0;
this.y = y || 0;
//the ship starts stationary, no need to set dx and dy
}
Ship.prototype = new Sprite();
//Ships also look different from asteroids, so we'll replace draw()
Ship.prototype.draw = function() {
canvasContext.beginPath();
canvasContext.moveTo(this.x, this.y - 10);
canvasContext.lineTo(this.x + 5, this.y + 10);
canvasContext.lineTo(this.x - 5, this.y + 10);
canvasContext.lineTo(this.x, this.y - 10);
canvasContext.stroke();
}
Now for the real gains: because our ships and our asteroids share a common set of functions and properties, we can treat them identically when it comes to animating our game.
//set up all objects
var sprites = [];
var player = new Ship();
sprites.push(player);
//create 100 asteroids in the same array
for (var i = 0; i < 100; i++) {
var asteroid = new Asteroid();
sprites.push(asteroid);
}
//drawing loop
var animate = function() {
//clear the screen
canvasContext.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < sprites.length; i++) {
//get each sprite
var s = sprites[i];
//update its position using Sprite's update() function
s.update();
//draw using the object type's unique draw() method
s.draw();
}
//set timer for next frame
setTimeout(animate, 10);
}
//start animation loop
animate();
This code has a long way to go before it's a real game--we haven't added any inputs, our asteroids all start at the same position, and we're not yet testing for collisions. But by using inheritance and constructor functions, we can share code between objects when possible. Now, if we want to change our physics for all objects, we can update anything that inherits from Sprite by changing code in just one place, instead of in each object's unique prototype.
JavaScript prototypes are very different from the way that other programming languages handle inheritance, and they take some time before they're really natural. In practice, you should try to avoid using very long prototype chains, and use constructors only when appropriate, but it can be a valuable tool for creating well-organized programs.
Practice Questions
- Games are a natural use case for constructors and inheritance, but applications for these techniques are everywhere you look. How would you use constructors if you were making a text editor? What about a paint application?
- Inheritance is actually used heavily in your browser. Think about the different types of elements on a page: they share many characteristics, such as height and width properties, but they also have differences (some are text, some are images, and some just tell the browser how to render the page). On a piece of paper, draft an inheritance tree showing which elements inherit from others. Think about your reasoning for each placement.