15 min read

In this article by Narayan Prusty, author of Learning ECMAScript 6, you will learn how ES6 introduces classes that provide a much simpler and clearer syntax to creating constructors and dealing with inheritance. JavaScript never had the concept of classes, although it’s an object-oriented programming language. Programmers from the other programming language background often found it difficult to understand JavaScript’s object-oriented model and inheritance due to lack of classes. In this article, we will learn about the object-oriented JavaScript using the ES6 classes:

  • Creating objects the classical way
  • What are classes in ES6
  • Creating objects using classes
  • The inheritance in classes
  • The features of classes

(For more resources related to this topic, see here.)

Understanding the Object-oriented JavaScript

Before we proceed with the ES6 classes, let’s refresh our knowledge on the JavaScript data types, constructors, and inheritance. While learning classes, we will be comparing the syntax of the constructors and prototype-based inheritance with the syntax of the classes. Therefore, it is important to have a good grip on these topics.

Creating objects

There are two ways of creating an object in JavaScript, that is, using the object literal, or using a constructor. The object literal is used when we want to create fixed objects, whereas constructor is used when we want to create the objects dynamically on runtime.

Let’s consider a case where we may need to use the constructors instead of the object literal. Here is a code example:

var student = {
name: "Eden",
printName: function(){
   console.log(this.name);
}
}
student.printName(); //Output "Eden"

Here, we created a student object using the object literal, that is, the {} notation. This works well when you just want to create a single student object.

But the problem arises when you want to create multiple student objects. Obviously, you don’t want to write the previous code multiple times to create multiple student objects. This is where constructors come into use.

A function acts like a constructor when invoked using the new keyword. A constructor creates and returns an object. The this keyword, inside a function, when invoked as a constructor, points to the new object instance, and once the constructor execution is finished, the new object is automatically returned. Consider this example:

function Student(name)
{
this.name = name;
}

Student.prototype.printName = function(){
console.log(this.name);
}

var student1 = new Student("Eden");
var student2 = new Student("John");

student1.printName(); //Output "Eden"
student2.printName(); //Output "John"

Here, to create multiple student objects, we invoked the constructor multiple times instead of creating multiple student objects using the object literals.

To add methods to the instances of the constructor, we didn’t use the this keyword, instead we used the prototype property of constructor. We will learn more on why we did it this way, and what the prototype property is, in the next section.

Actually, every object must belong to a constructor. Every object has an inherited property named constructor, pointing to the object’s constructor. When we create objects using the object literal, the constructor property points to the global Object constructor. Consider this example to understand this behavior:

var student = {}

console.log(student.constructor == Object); //Output "true"

Understanding inheritance

Each JavaScript object has an internal [[prototype]] property pointing to another object called as its prototype. This prototype object has a prototype of its own, and so on until an object is reached with null as its prototype. null has no prototype, and it acts as a final link in the prototype chain.

When trying to access a property of an object, and if the property is not found in the object, then the property is searched in the object’s prototype. If still not found, then it’s searched in the prototype of the prototype object. It keeps on going until null is encountered in the prototype chain. This is how inheritance works in JavaScript.

As a JavaScript object can have only one prototype, JavaScript supports only a single inheritance.

While creating objects using the object literal, we can use the special __proto__ property or the Object.setPrototypeOf() method to assign a prototype of an object. JavaScript also provides an Object.create() method, with which we can create a new object with a specified prototype as the __proto__ lacked browser support, and the Object.setPrototypeOf() method seemed a little odd. Here is code example that demonstrates different ways to set the prototype of an object while creating, using the object literal:

var object1 = {
name: "Eden",
__proto__: {age: 24}
}

var object2 = {name: "Eden"}
Object.setPrototypeOf(object2, {age: 24});

var object3 = Object.create({age: 24}, {name: {value: "Eden"}});

console.log(object1.name + " " + object1.age);
console.log(object2.name + " " + object2.age);
console.log(object3.name + " " + object3.age);

The output is as follows:

Eden 24
Eden 24
Eden 24

Here, the {age:24} object is referred as base object, superobject, or parent object as its being inherited. And the {name:”Eden”} object is referred as the derived object, subobject, or the child object, as it inherits another object.

If you don’t assign a prototype to an object while creating it using the object literal, then the prototype points to the Object.prototype property. The prototype of Object.prototype is null therefore, leading to the end of the prototype chain. Here is an example to demonstrate this:

var obj = {
name: "Eden"
}

console.log(obj.__proto__ == Object.prototype); //Output "true"

While creating objects using a constructor, the prototype of the new objects always points to a property named prototype of the function object. By default, the prototype property is an object with one property named as constructor. The constructor property points to the function itself. Consider this example to understand this model:

function Student()
{
this.name = "Eden";
}

var obj = new Student();

console.log(obj.__proto__.constructor == Student); //Output "true"
console.log(obj.__proto__ == Student.prototype); //Output "true"

To add new methods to the instances of a constructor, we should add them to the prototype property of the constructor, as we did earlier. We shouldn’t add methods using the this keyword in a constructor body, because every instance of the constructor will have a copy of the methods, and this isn’t very memory efficient. By attaching methods to the prototype property of a constructor, there is only one copy of each function that all the instances share. To understand this, consider this example:

function Student(name)
{
   this.name = name;
}

Student.prototype.printName = function(){
   console.log(this.name);
}

var s1 = new Student("Eden");
var s2 = new Student("John");

function School(name)
{
this.name = name;
this.printName = function(){
   console.log(this.name);
}
}

var s3 = new School("ABC");
var s4 = new School("XYZ");

console.log(s1.printName == s2.printName);
console.log(s3.printName == s4.printName);

The output is as follows:

true
false

Here, s1 and s2 share the same printName function that reduces the use of memory, whereas s3 and s4 contain two different functions with the name as printName that makes the program use more memory. This is unnecessary, as both the functions do the same thing. Therefore, we add methods for the instances to the prototype property of the constructor.

Implementing the inheritance hierarchy in the constructors is not as straightforward as we did for object literals. Because the child constructor needs to invoke the parent constructor for the parent constructor’s initialization logic to take place and we need to add the methods of the prototype property of the parent constructor to the prototype property of the child constructor, so that we can use them with the objects of child constructor. There is no predefined way to do all this. The developers and JavaScript libraries have their own ways of doing this. I will show you the most common way of doing it.

Here is an example to demonstrate how to implement the inheritance while creating the objects using the constructors:

function School(schoolName)
{
this.schoolName = schoolName;
}
School.prototype.printSchoolName = function(){
console.log(this.schoolName);
}

function Student(studentName, schoolName)
{
this.studentName = studentName;
 
School.call(this, schoolName);
}
Student.prototype = new School();
Student.prototype.printStudentName = function(){
console.log(this.studentName);
}

var s = new Student("Eden", "ABC School");
s.printStudentName();
s.printSchoolName();

The output is as follows:

Eden
ABC School

Here, we invoked the parent constructor using the call method of the function object. To inherit the methods, we created an instance of the parent constructor, and assigned it to the child constructor’s prototype property.

This is not a foolproof way of implementing inheritance in the constructors, as there are lots of potential problems. For example—in case the parent constructor does something else other than just initializing properties, such as DOM manipulation, then while assigning a new instance of the parent constructor, to the prototype property, of the child constructor, can cause problems.

Therefore, the ES6 classes provide a better and easier way to inherit the existing constructors and classes.

Using classes

We saw that JavaScript’s object-oriented model is based on the constructors and prototype-based inheritance. Well, the ES6 classes are just new a syntax for the existing model. Classes do not introduce a new object-oriented model to JavaScript.

The ES6 classes aim to provide a much simpler and clearer syntax for dealing with the constructors and inheritance.

In fact, classes are functions. Classes are just a new syntax for creating functions that are used as constructors. Creating functions using the classes that aren’t used as constructors doesn’t make any sense, and offer no benefits. Rather, it makes your code difficult to read, as it becomes confusing. Therefore, use classes only if you want to use it for constructing objects. Let’s have a look at classes in detail.

Defining a class

Just as there are two ways of defining functions, function declaration and function expression, there are two ways to define a class: using the class declaration and the class expression.

The class declaration

For defining a class using the class declaration, you need to use the class keyword, and a name for the class.

Here is a code example to demonstrate how to define a class using the class declaration:

class Student
{
constructor(name)
{
   this.name = name;
}
}

var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

Here, we created a class named Student. Then, we defined a constructor method in it. Finally, we created a new instance of the class—an object, and logged the name property of the object.

The body of a class is in the curly brackets, that is, {}. This is where we need
to define methods. Methods are defined without the function keyword, and a comma is not used in between the methods.

Classes are treated as functions, and internally the class name is treated as the function name, and the body of the constructor method is treated as the body of the function.

There can only be one constructor method in a class. Defining more than one constructor will throw the SyntaxError exception.

All the code inside a class body is executed in the strict mode, by default.

The previous code is the same as this code when written using function:

function Student(name)
{
this.name = name;
}

var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

To prove that a class is a function, consider this code:

class Student
{
constructor(name)
{
   this.name = name;
}
}

function School(name)
{
this.name = name;
}

console.log(typeof Student);
console.log(typeof School == typeof Student);

The output is as follows:

function
true

Here, we can see that a class is a function. It’s just a new syntax for creating a function.

The class expression

A class expression has a similar syntax to a class declaration. However, with class expressions, you are able to omit the class name. Class body and behavior remains the same in both the ways.

Here is a code example to demonstrate how to define a class using a class expression:

var Student = class {
constructor(name)
{
   this.name = name;
}
}

var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

Here, we stored a reference of the class in a variable, and used it to construct the objects.

The previous code is the same as this code when written using function:

var Student = function(name) {
this.name = name;
}

var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

The prototype methods

All the methods in the body of the class are added to the prototype property of the class. The prototype property is the prototype of the objects created using class.

Here is an example that shows how to add methods to the prototype property of a class:

class Person
{
constructor(name, age)
{
   this.name = name;
   this.age = age;
}
printProfile()
{
   console.log("Name is: " + this.name + " and Age is: " +
this.age);
}
}

var p = new Person("Eden", 12)
p.printProfile();

console.log("printProfile" in p.__proto__);
console.log("printProfile" in Person.prototype);

The output is as follows:

Name is: Eden and Age is: 12
true
true

Here, we can see that the printProfile method was added to the prototype property of the class.

The previous code is the same as this code when written using function:

function Person(name, age)
{
this.name = name;
this.age = age;
}

Person.prototype.printProfile = function()
{
console.log("Name is: " + this.name + " and Age is: " +
this.age);
}

var p = new Person("Eden", 12)
p.printProfile();

console.log("printProfile" in p.__proto__);
console.log("printProfile" in Person.prototype);

The output is as follows:

Name is: Eden and Age is: 12
true
true

The get and set methods

In ES5, to add accessor properties to the objects, we had to use the Object.defineProperty() method. ES6 introduced the get and set prefixes for methods. These methods can be added to the object literals and classes for defining the get and set attributes of the accessor properties.

When get and set methods are used in a class body, they are added to the prototype property of the class.

Here is an example to demonstrate how to define the get and set methods in a class:

class Person
{
constructor(name)
{
   this._name_ = name;
}

get name(){
   return this._name_;
}

set name(name){
   this._name_ = name;
}
}

var p = new Person("Eden");
console.log(p.name);
p.name = "John";
console.log(p.name);

console.log("name" in p.__proto__);
console.log("name" in Person.prototype);
console.log(Object.getOwnPropertyDescriptor(p.__proto__,
"name").set);
console.log(Object.getOwnPropertyDescriptor(Person.prototype,
"name").get);
console.log(Object.getOwnPropertyDescriptor(p, "_name_").value);

The output is as follows:

Eden
John
true
true
function name(name) { this._name_ = name; }
function name() { return this._name_; }
John

Here, we created an accessor property to encapsulate the _name_ property. We also logged some other information to prove that name is an accessor property, which is added to the prototype property of the class.

The generator method

To treat a concise method of an object literal as the generator method, or to treat a method of a class as the generator method, we can simply prefix it with the * character.

The generator method of a class is added to the prototype property of the class.

Here is an example to demonstrate how to define a generator method in class:

class myClass
{
* generator_function()
{
   yield 1;
   yield 2;
   yield 3;
   yield 4;
   yield 5;
}
} var obj = new myClass(); let generator = obj.generator_function(); console.log(generator.next().value); console.log(generator.next().value); console.log(generator.next().value); console.log(generator.next().value); console.log(generator.next().value); console.log(generator.next().done); console.log("generator_function" in myClass.prototype);

The output is as follows:

1
2
3
4
5
true
true

Implementing inheritance in classes

Earlier in this article, we saw how difficult it was to implement inheritance hierarchy in functions. Therefore, ES6 aims to make it easy by introducing the extends clause, and the super keyword for classes.

By using the extends clause, a class can inherit static and non-static properties of another constructor (which may or may not be defined using a class).

The super keyword is used in two ways:

  • It’s used in a class constructor method to call the parent constructor
  • When used inside methods of a class, it references the static and non-static methods of the parent constructor

Here is an example to demonstrate how to implement the inheritance hierarchy in the constructors using the extends clause, and the super keyword:

function A(a)
{
this.a = a;
}

A.prototype.printA = function(){
console.log(this.a);
}

class B extends A
{
constructor(a, b)
{
   super(a);
   this.b = b;
}

printB()
{
   console.log(this.b);
}

static sayHello()
{
   console.log("Hello");
}
}

class C extends B
{
constructor(a, b, c)
{
   super(a, b);
   this.c = c;
}

printC()
{
   console.log(this.c);
}

printAll()
{
   this.printC();
   super.printB();
   super.printA();
}
}

var obj = new C(1, 2, 3);
obj.printAll();

C.sayHello();

The output is as follows:

3
2
1
Hello

Here, A is a function constructor; B is a class that inherits A; C is a class that inherits B; and as B inherits A, therefore C also inherits A.

As a class can inherit a function constructor, we can also inherit the prebuilt function constructors, such as String and Array, and also the custom function constructors using the classes instead of other hacky ways that we used to use.

The previous example also shows how and where to use the super keyword. Remember that inside the constructor method, you need to use super before using the this keyword. Otherwise, an exception is thrown.

If a child class doesn’t have a constructor method, then the default behavior will invoke the constructor method of the parent class.

Summary

In this article, we have learned about the basics of the object-oriented programming using ES5. Then, we jumped into ES6 classes, and learned how it makes easy for us to read and write the object-oriented JavaScript code. We also learned miscellaneous features, such as the accessor methods.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here