2-6 Objects with methods

Object literals

In previous sections, we’ve studied how to use JavaScript objects to model complicated data and how to declare functions that can be used as function objects, which can be stored in variable. By combining these knowledge, we can create an object in the sense of Object Oriented Programming: a object with state and behavior.

To recap, a JavaScript object is a group of properties. Each property has a name and a value. Previously we create a single object by listing the properties of the object in { }. This is known as object literal.

let circle = { 
  x: 2, y: 3,  // x, y-coordinate of center of the circle
  radius: 5,
  fillColor: 'red'
};

Adding methods

A property can take ‘anything’ as value, including number, string, boolean, array, object, and even function. When we add a property with a function value, we’ve added a method to the object.

let circle = { 
  x: 2, y: 3,
  radius: 5,
  fillColor: 'red',
  area: function () { 
    return Math.PI * this.radius ** 2 
  }
};

console.log(`Area of the circle is ${circle.area()}`)

You call a method of an object with the dot notation (similar to Java). circle.area refers to the function object we defined for the area property. You run this method by adding ( ) (possibly with parameters). In this example, it is circle.area().

In the body of a method, this refers to the object on which the method is invoked. (Think ‘area of this circle’)

There is a shorthand notation to define methods in an object literal. In this example, we also define another method resize.

let circle = { 
  x: 2, y: 3,
  radius: 5,
  fillColor: 'red',
  area() { 
    return Math.PI * this.radius ** 2; 
  },
  resize(factor) {
    this.radius = this.radius * factor;
  } 
};

circle.resize(2);
console.log(`Area of the circle is ${circle.area()}`);

A caveat on writing methods

Here is a reminder about writing methods. Never use arrow functions as methods.

let circle_with_incorrect_method = { 
  x: 2, y: 3,
  radius: 5,
  // arrow functions CANNOT work as methods
  area: () => Math.PI * this.radius ** 2 
};

circle_with_incorrect_method.area()  // returns NaN

The reason that the above example does not work is that when an arrow function is called, it does not set this to refer to the object on which the method is invoked. In this case, this does not point to circle, and this.radius does not exist (or, is undefined).

Technical note: In this example, this refers to the ‘global object’, which is window in browsers, or globalThis in general. this.radius would refer to a global variable named radius, if it exists.

So, how does an arrow function obtain this when invoked? It obtains from the surrounding (usually an outer function). Read this reference for an example.

Inherited methods

You might notice that object literals include some methods not defined by you. For example, point.toString() is called when JavaScript tries to concatenate a string with the object.

let point = { x: 2, y: 3 };
console.log('This point is ' + point);

The method .toString() is actually inherited from the prototype of the object. But this version of toString is not very informative. You can define a more useful version of toString by redefining it in point.

let point = {
  x: 2,
  y: 3,
  toString() { return `(${this.x},${this.y})` }
};
console.log(`This point is ${point}`);

JavaScript’s use of prototype to implement inheritance is different from that of Java and Python. You can learn more about that in this reference.

Exercise

  1. Add a method quadrant() to the object literal point. The method returns the quadrant in which the point resides. (sample answer)

    let point = {
      x: -2,
      y: 3,
      toString() { return `(${this.x},${this.y})` },
      quadrant() { /* your work */ }
    };
    // prints "The point (-2,3) is in quadrant II"
    console.log(`The point ${point} is in quadrant ${point.quadrant()}`);
    
  2. Write an object literal that implements the queue data structure. Use the following template to write your solution. (sample answer)

    let q = {
      // data properties to hold the content of the queue
      // ...
      enqueue(elem) {
        // add 'elem' at the end of queue
      },
      dequeue() {
        // remove the element at the head of queue, and return the element
      },
      count() {
        // returns the number of elements in the queue
      },
      toString() {
        // return the content of the queue as string
      }
    };
    q.enqueue('a'); q.enqueue('b'); q.enqueue('c');
    console.log( q.dequeue() ); // print 'a'
    console.log( q.count() );   // print 2
    q.enqueue('d');
    console.log(`The queue contains ${q}.`);