The ultimate guide to understanding JavaScript class pattern

Consider an empty object. Print the object using toString method.

const obj = {};
console.log(obj.toString()); // [object Object]

We get the result [object Object] in the console. How does it work? (Interview question). Developers who have a strong background in C++ or C# will blindly attribute it to OOPs. All objects inherit from Object.

JavaScript is to Java what Cartoon is to Car.

JavaScript works as designed. But it does not work the same way as C# or Java. Consider the following code.

Object.getOwnPropertyNames(obj); // []

To inspect the properties within an object, JavaScript has getOwnPropertyNames method. For the empty object, there is no property defined. If obj were to inherit from Object, it should have toString as one of the properties. In JavaScript, objects have only properties. Any function defined within an object is assigned to a property. (I will prove that in a minute). Our empty object has no property of its own. So, where does it get the toString method? (Can we define method as a property which points to a function?)

Prototype

Every object has a prototype. Prototype is another object. Our empty object has the Object prototype. When you get a property on an object, JavaScript first checks the own properties of an object. If it is not found there, it looks up to the own properties of the object’s prototype.

To get the object’s prototype, use:

console.log(Object.getPrototypeOf(obj));
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(obj)));

The first statement in any browser console is misleading. It does not say much about the prototype object. But the second statement clearly shows that the prototype object belongs to Object.

As you can see from the above screenshot, toString is found in the prototype object. We can use obj.toString() and it works as expected.

Function

Object in JavaScript is a function.

console.log(typeof Object); // function
console.log(typeof obj); // object

Functions have a prototype property which points to the prototype object. (Objects do not have prototype property. To get the prototype of an object, use Object.getPrototypeOf or use __proto__ property).

console.log(Object.getOwnPropertyNames(Object.prototype));

The above statement gives all the properties of the Object’s prototype which we have seen already.

The following three lines of code are equivalent. (Almost, We will leave out the nuances for now.)

const obj = {};
const obj = new Object(); 
const obj = Object.create(Object.prototype);

An object has properties and linked to a prototype object. Object.create creates an empty object whose prototype object is specified as the argument. new is special keyword in JavaScript. It creates a new object whose prototype object is the function’s prototype. So, all three statements do the same thing.

An empty object has a prototype object which is same as the prototype object of Object function

It is possible to create an object with a null prototype as follows.

const reallyEmptyObject = Object.create(null);
console.log(reallyEmptyObject.toString); // undefined

As expected, our empty object with null prototype does not have the toString method defined.

Prototype chain

An object has own properties. But, it is usually linked to a prototype object. A prototype object is also an object. So, the prototype object also has a prototype object and so on. Ultimately, one of the prototype objects will point to a null prototype. To retrieve a property value of an object, JavaScript will check the entire prototype chain till a null prototype is reached.

console.log(Object.getPrototypeOf(Object.prototype)); // null

Armed with the knowledge of object, property access and prototype chain, we are ready to understand the class pattern in JavaScript.

Prototype chain using prototype objects

The first pattern that we will look is quite simple but least used (for various reasons).

We define two prototype objects, Foo and Bar. Link them together using Object.setPrototypeOf. After the linking, Foo is the prototype object for Bar. Create a new object with Bar as the prototype object. The newly created object works as you would expect.

const Foo = {
  speak: function() {
    console.log('speak');
  }
}

const Bar = {
  speakMore: function() {
    console.log('speak more');
  }
}

Object.setPrototypeOf(Bar, Foo);

const bar = Object.create(Bar);
bar.speakMore(); // speak more
bar.speak(); // speak

To verify, let us print out the own properties of each of the objects.

console.log(Object.getOwnPropertyNames(bar));
console.log(Object.getOwnPropertyNames(Bar));
console.log(Object.getOwnPropertyNames(Foo));
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(Foo)));

The result is shown below (and works as expected).

Prototype chain via object prototypes

There is a CodePen if you want to try it out.

Prototype chain using function constructor

In this pattern, we use the new keyword before a function. The function call is also called a constructor call in JavaScript when we use new keyword. We create two functions, Foo and Bar. We define function properties (or methods) on the prototype object of the functions. And we chain all the prototype objects appropriately as follows.

function Foo() {}
Foo.prototype.speak = function() {
  console.log("speak");
};

function Bar() {}
Bar.prototype = new Foo();
Bar.prototype.speakMore = function() {
  console.log("speak more");
};

const bar = new Bar();
bar.speak(); // speak
bar.speakMore(); // speak more

As shown above, the newly created object (bar) has a prototype object which is nothing but the prototype object of the Bar function. The prototype object of Bar function also has a prototype object which is nothing but the prototype object of Foo function. We can verify by printing the own properties of each of the object prototypes.

console.log(Object.getOwnPropertyNames(bar));
console.log(Object.getOwnPropertyNames(Bar.prototype));
console.log(Object.getOwnPropertyNames(Foo.prototype));
console.log(Object.getOwnPropertyNames(Object.prototype));

The screenshot below shows the result.

Prototype chain using Function constructors

It is slightly different from the previous result (Pattern 1). Foo.prototype has an additional constructor property. By default, a function prototype has constructor property. We redefined a new prototype object for Bar function with Bar.prototype = new Foo(). So, the original prototype of Bar function is replaced with an object whose prototype object is the same as the prototype object of Foo function. (CodePen)

Prototype chain using ES6 class

ES6 class is mostly syntactic sugar for building prototype chains. Consider the following class.

class Foo {}
console.log(typeof Foo); // function

Foo is a class. But typeof Foo returns a function. As a function, Foo has a prototype.

Let us add a method to our class. And print out the prototype properties.

class Foo {
  speak() {
    console.log('speak');
  }
}

console.log(Object.getOwnPropertyNames(Foo.prototype)); // ['constructor', 'speak']

We get constructor and speak as own property names for Foo’s prototype. So, the class keyword in JavaScript helps us to define a prototype for the function named after the class.

We can extend the prototype chain using the extend keyword. Define a new class Bar as follows.

class Bar extends Foo {
  speakMore() {
    console.log('speak more');
  }
}

console.log(Object.getPrototypeOf(Bar.prototype) === Foo.prototype); // true

As we now know, Bar class defines a prototype object for Bar function. The extends keyword makes Foo`s prototype object the prototype for Bar`s prototype object. In short, we chain the prototypes.

Finally, our example can be rewritten using the class pattern as follows.

class Foo {
  speak() {
    console.log('speak');
  }
}

class Bar extends Foo {
  speakMore() {
    console.log('speak more');
  }
}

const bar = new Bar();
bar.speak(); // speak
bar.speakMore(); // speak more

We can verify if the prototypes are chained as follows.

console.log(Object.getOwnPropertyNames(bar));
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(bar)));
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(bar))));
console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(bar)))));

The result is shown in the screenshot below.

Prototype chain using class

No magic here. CodePen is available.

Summary

I hope some of JavaScript magic is demystified in the article. Objects have properties. JavaScript looks up the own properties of an object while getting a property value. If it does not find in the own property of an object, it walks up the prototype chain till the property is found. An object defines behaviour using function properties which I also call methods. Function properties behave the same way as regular properties. JavaScript retrieves the first matching function name in the own property or prototype chain.

All objects have a prototype object. But prototype object can be null.

A function has a prototype object. Newing a function or calling the function constructor creates a new object whose prototype is the function’s prototype object.

Prototype chaining has three patterns:

  1. Chaining prototypes by defining prototype objects.
  2. Chaining prototypes using function constructor.
  3. Chaining prototypes using class keyword.

ES6 class is a syntactical sugar over functions. Whatever is defined within a class is the prototype of a function that goes by the same class name. So, newing a class is equivalent to calling a function constructor. And as explained, a new object is created with the prototype object being the function’s prototype (or the class). The extends keyword also helps in chaining two prototypes (as defined by the respective classes).

Related Posts

Leave a Reply

Your email address will not be published.