Objects with methods

← back to Creative Computing

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:

► run sketch ◼ stop sketch
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:

► run sketch ◼ stop sketch
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().

► run sketch ◼ stop sketch
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.

► run sketch ◼ stop sketch
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