2-7 Defining class

A pitfall of object literals

If you only need a single object with simple behavior, making an object literal with methods is a quick solution. However, if you need to create and maintain several objects with similar structure (data properties and methods), making separate object literals is cumbersome and inefficient.

Consider the following example of person health records with a method BMI to calculate Body mass index. We’ll need to define each health record as object with its own version of the BMI method.

let p1 = { weight: 70, height: 1.7, 
  BMI() { return this.weight / this.height ** 2 }};
let p2 = { weight: 62, height: 1.4, 
  BMI() { return this.weight / this.height ** 2 }};
// ...

Some built-in class

On the other hand, the JavaScript language defines several built-in class, each with its own constructor function to create objects of the class. Objects thus created usually have built-in methods as defined in the class. (See the full list of built-in objects in mozilla online reference)

In OOP sense, a class defines a template for similar objects, and provides (at least) one constructor function to build new objects belonging to the class.

Date

One common built-in class is Date, which represents a single moment in time. You create a new Date object by calling the constructor new Date(...). All Date object thus created share the same rich list of built-in methods, see [(online reference)].moz-date

// call a constructor to create a Date object,
// which equals to this moment
let now = new Date();
// create a Date object for the Macau SAR establishment day
let sare = new Date("1999-12-20");
// create a Date object for the coming X'mas
let xmas = new Date(2016,11,25); // month: 0-11. Confusing!
// a Date object has some methods
let now = new Date();
let dow = now.getDay(); // 0-6, where 0 means Sunday
console.log(now.toLocaleString());

Array

Typically, you construct an array with the array literal syntax like let a = [1, 2, 3]. But because arrays are also JavaScript objects, you can create an Array object by calling the constructor new Array(1,2,3). An array object has many methods, and you can invoke them with the dot notation. Refer to online ref for a list of methods of array objects.

// same as let a = [1,2,3]
let a = new Array(1,2,3);
a.unshift(0);
a.push(4)
for (let odd of a.map(x=>x*2+1)) {
  console.log(odd);
}

Technical note: there is a trap in the design of the Array constructor function when there is only 1 parameter of number type. See reference for detail.

Map

A recent update of JavaScript defines a new kind of collections called Map. A map stores key/value pairs. You create a Map by submitting a list of key/value pairs to the constructor. Each pair is written as an array of two elements, namely key and value. You retrieve an element from a map using a key with the method .get(key). You add / replace an element in the map with the method .set(key,value). You can also delete an element from a map with .delete(key), and check the number of elements with the property .size.

// creates a new Map with two key/value pairs
let m = new Map([ ['one', 1], ['two', 2] ]);
// use .get to retrieve an element
console.log('one => ', m.get('one'));
// use .set to add / replace an element
m.set('three', 3);
// now, m is Map {"one" => 1, "two" => 2, "three" => 3}
console.log("Does the map contains the key 'four'? ", m.has('four'));
console.log('No, so .get returns ', m.get('four'));  // undefined

A map is iterable, and the for .. of loop returns a key/value pair in each step.

let m = new Map([ ['one', 1], ['two', 2], ['three', 3] ]);
for (let [k,v] of m) {
  console.log(`k = ${k}  v = ${v}`);
}

Defining class

JavaScript has a new syntax to define class. It is similar to the syntax in Java. A class is a template to create similar objects, with a constructor function (which builds new object of the class) and methods (which access properties of the object using this).

You typically call the constructor function with new. Inside the constructor, this refers to the new object being created. You should initialize all properties inside the constructor. Notice that in JavaScript, you don’t need to define the properties (as you would define attributes in a Java class).

In a method, this refers to the object that invokes the method. Methods defined in a class are available for all objects created using the constructor function.

class Point {
  // create a Point object with the given x-, y- coordinates
  constructor (x,y) {
    // this refers to the new object being built
    this.x = x; this.y = y;
  }
  // a method to convert the point into a string
  toString () {
    // this refers to the object that invokes the method
    return `(${this.x},${this.y})`;
  }
};

let p1 = new Point(3,4);
console.log(`p1 is ${p1}`);
let p2 = new Point(2,-1);
console.log(`p2 is ${p2.toString()}`);

The object created by a constructor is similar to object literal in common usage. (But there are some difference about prototype in implementation detail that we’ll not cover.) Use a class when you need to create many objects using a common template.

// assume you've run the code listing above
// examine the structure of the object created with constructor
dir(p1);
// and here is an object literal
let p3 = { x: 2, y: 5 };
p3.toString = function() { return `(${this.x},${this.y})` }
dir(p3);

All properties in JavaScript objects are public. So you can access the properties in a function.

function distanceBetween (p1,p2) {
  return Math.hypot(p1.x-p2.x, p1.y-p2.y);
}

function midpoint (p1, p2) {
  const midx = (p1.x+p2.x)/2;
  const midy = (p1.y+p2.y)/2;
  return new Point(midx, midy);
}

Exercise

  1. Define a class Circle that can be used in the following ways. Hints: the class contains a constructor, a method area to calculate the area of the circle, a method toString to convert the object into string, and a method move(x,y) to move the center of the circle to the given location.

    let c = new Circle(1,2,10);
    let a = c.area();
    console.log(`The area is ${a}.`);
    // this calls c.toString()
    console.log(`c is ${c}`);
    // c is a circle at (1,2) of radius 10
    c.move(-1,0);
    console.log(`c is ${c}`);
    // c is a circle at (-1,0) of radius 10
    
  2. Define a function contains(c1,c2) that takes two Circle objects as parameters and determine whether the first circle contains the second completely. Test your work with the following.

    let c1 = new Circle(0, 0, 10);
    let c2 = new Circle(3, 4, 4);
    let c3 = new Circle(3, 4, 8);
    console.log('c1 contains c2? ', contains(c1,c2) );  // true
    console.log('c1 contains c3? ', contains(c1,c3) );  // false
    

The sample answer is available.