Objects with methods
In this section, we’re going to talk about a new aspect of objects: the fact that they can have methods. Methods are just like regular function, except that they have a built-in idea about which value they should act upon: the object to which they belong. Using methods can make your code cleaner and more concise.
Why methods?
The easiest way to demonstrate methods is by adapting a previous example.
Here’s an example from earlier in which clicking somewhere on the screen adds a
new “rectangle” object to an array. In draw()
, all of the rectangle objects
are drawn to the screen. The rectangles move as well:
let rectObjs = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectObjs.length; i++) { fill(rectObjs[i].fillColor); rect(rectObjs[i].xpos, rectObjs[i].ypos, 50, 25); rectObjs[i].ypos += 1; } } function mousePressed() { rectObjs.push({xpos: mouseX, ypos: mouseY, fillColor: random(255)}); }
One way to generalize this code would be to create a display()
function
and a update()
function, both of which take an object as a parameter, and
work with the properties of that object:
let rectObjs = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectObjs.length; i++) { display(rectObjs[i]); update(rectObjs[i]); } } function mousePressed() { rectObjs.push({xpos: mouseX, ypos: mouseY, fillColor: random(255)}); } function display(obj) { fill(obj.fillColor); rect(obj.xpos, obj.ypos, 50, 25); } function update(obj) { obj.ypos += 1; }
There are problems with this approach, however. Well, there’s one potential
problem: the display()
and update()
functions only know how to deal with one
kind of object. So if we added other kinds of objects (like circles or
triangles or faces or aardvarks or whatever), we’d have to adjust the
display()
and update()
functions to compensate. And even then we’d have to
write some code in both methods to guess what kind of object we’re working
with. It would be a mess.
In their infinite wisdom, the creators of Javascript included a way to get around this problem: instead of making one function that knows how to work with all kinds of objects, why not make it so each object can carry with it all of the functions it needs to operate? And so the method was born.
(Actually, this idea doesn’t originate with Javascript; many programming languages support object-oriented functionality like methods.)
Adding methods to objects
A method is just a function that is the value for a key in an object. For example, try running this code in an empty p5.js sketch:
let rectangle = {
width: 100,
height: 200,
area: function() {
return this.width * this.height;
},
perimeter: function() {
return 2*this.width + 2*this.height;
}
};
console.log(rectangle.area()); // 20000
console.log(rectangle.perimeter()); // 600
This code creates an object named rectangle
, which has four attributes:
the values for width
and height
are both numbers, and the values for area
and perimeter
are both function values. Because these functions are values
in an object, we call them methods.
There are two bits of syntax you need to know to get started using methods.
The first is how to call a method. To do this, take an expression that
evaluates to an object, follow it by a dot (.
), and then type the name of the
method, followed by parentheses. (Methods can also take parameters, in which
case the parentheses won’t be empty. See below.)
The second bit of new syntax is this
. Inside of the method,
there’s a special keyword this
which refers to the object that contains the
method. You can access properties, overwrite properties, add new properties,
and even call methods of the containing object.
Here’s a version of the example from above. This time, however, the
mousePressed()
function adds objects to the array that have methods of their
own for display()
and update()
.
let rectObjs = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectObjs.length; i++) { rectObjs[i].display(); rectObjs[i].update(); } } function mousePressed() { let newRect = { xpos: mouseX, ypos: mouseY, fillColor: random(255), display: function() { fill(this.fillColor); rect(this.xpos, this.ypos, 50, 25); }, update: function() { this.ypos += 1; } }; rectObjs.push(newRect); }
Object factories
As we’ve seen, Javascript has a set of conventions and bits of syntax for dealing with objects and methods. In addition to what we’ve seen so far, Javascript has a notion of what’s called “prototypal inheritance,” which allows certain objects to be the “template” for other objects, which then inherit all of the first object’s properties and methods. You can read more about inheritance here.
We’re not going to discuss inheritance in this class, but I do want to show
you how to create objects that are “inheritance-ready,” so that you don’t get
into coding habits that prevent you from using inheritance in the future.
The two bits of Javascript syntax we’re going to discuss in pursuit of this
goal are the new
keyword and the .prototype
attribute.
Constructor functions and the new
keyword
As we’ve seen above, it’s very easy to write Javascript code that creates a new object. For example, this function creates and returns a new object:
function createAsteroid(massVal, albedoVal) {
let asteroid = {
mass: massVal,
albedo: albedoVal,
population: 0
};
return asteroid;
}
let foo = createAsteroid(1000, 0.14);
console.log(foo.albedo); // prints 0.14
Usually in Javascript, functions whose sole purpose is to create new objects
are called constructor functions, and they’re called in a very particular
way, using the new
keyword.
A constructor function called with new
automatically has the keyword this
available inside of it. Additionally, the constructor function doesn’t need to
return anything explicitly; it only needs to assign the desired attributes of
the object to this
.
Here’s the same example as above, rewritten using new
:
function Asteroid(massVal, albedoVal) {
this.mass = massVal;
this.albedo = albedoVal;
this.population = 0;
}
let foo = new Asteroid(2000, 0.45);
console.log(foo.albedo); // prints 0.45
By convention, the names of constructor functions begin with a capital letter.
Adding methods to a constructor with .prototype
The purpose of creating a constructor function is that (someday, eventually)
we’ll want to be able to use inheritance. The other thing you need to do to
make your object factories “inheritance-ready” is to add methods not to the
object itself, but to the .prototype
attribute of the constructor function.
For example, if we wanted to add a colonize
method to the objects that the
Asteroid
function creates, we would do this:
function Asteroid(massVal, albedoVal) {
this.mass = massVal;
this.albedo = albedoVal;
this.population = 0;
}
Asteroid.prototype.colonize = function(settlerCount) {
this.population += settlerCount;
}
let foo = new Asteroid(2000, 0.45);
console.log(foo.population); // prints 0
foo.colonize(123);
console.log(foo.population); // prints 123
Assigning a function value to Asteroid.prototype.colonize
creates a new
method that you can call on objects returned from the Asteroid
constructor,
in exactly the same way that you can call methods that are defined inside of
the object body itself.
Back to rectangles
Here, finally, is a rewritten version of the rectangle example that uses a
constructor function, the new
keyword, and functions assigned to
.prototype
.
function Rect(xposVal, yposVal, fillColorVal) { this.xpos = xposVal; this.ypos = yposVal; this.fillColor = fillColorVal; } Rect.prototype.display = function() { fill(this.fillColor); rect(this.xpos, this.ypos, 50, 25); }; Rect.prototype.update = function() { this.ypos += 1; }; let rectObjs = []; // start with empty list function setup() { createCanvas(400, 400); } function draw() { background(50); noStroke(); rectMode(CENTER); fill(255); for (let i = 0; i < rectObjs.length; i++) { rectObjs[i].display(); rectObjs[i].update(); } } function mousePressed() { rectObjs.push( new Rect(mouseX, mouseY, random(255))); }
EXERCISE: Modify the sketch above so that there are multiple possible shapes, like circles and triangles. Create a constructor function and methods for each type of shape. (Do you need to modify the code in
draw()
to accommodate the new kinds of objects?)
Further reading and resources
- The Secret Life of Objects (from Eloquent Javascript)
- Introduction to Object-Oriented JavaScript (from the Mozilla Developers Network)