JavaScript object-orientated and prototype-based programming: ES5, ES6, ES2016 (ES7) and TypeScript

As you learned in the previous chapter on JavaScript data types, JavaScript relies heavily on the concept of objects. While being familiar with other object-orientated programming(OOP) languages (e.g. C#, Java) helps, JavaScript works differently than most mainstream OOP languages.

JavaScript was conceived as a prototype-based programming language, which is a type of OOP language. Although the differences are subtle, exploring prototype-based concepts is critical to better understanding JavaScript in general, not to mention knowing such differences also makes it easier to grasp JavaScript's object-orientated evolution.

ES5: Prototype-based programming and the prototype property, plus everything is an Object, the new keyword, static properties/methods and getters/setters

A prototype, according to the dictionary is: an original model on which something is patterned. By extension, it means prototype-based programming languages use prototypes as their building blocks. But re-read the prototype definition again, doesn't it sound pretty similar to the concept of classes in OOP ? After all, in OOP languages, classes are used as the building blocks to describe models.

It turns out OOP and prototype-based programming are behaviorally similar, however, prototype-based design is a little different than class-based design. To make your first encounter with prototype-based design in JavaScript as simple as possible, I'll start with a scenario based on the String data type presented in the previous chapter.

The JavaScript String data type has its own set of properties and methods provided out-of-the-box as described in table 2-2 (most common object data types). Now, suppose you're faced with the problem of constantly formatting the strings in your application with a variation that isn't provided by the String data type (e.g. adding exclamation marks or wrapping it as HTML bold statements). In an OOP language, to simplify this repetitive process you would create a String sub-class -- to inherit the out-of-the-box String properties and methods -- and be able to add custom properties or methods to perform these repetitive operations. In a prototype-based programming language, specifically JavaScript, all you need to do is alter the String data type itself via its prototype property to make new properties accesible to all String instances, a process that's illustrated in listing 3-1.

Listing 3-1. ES5: String data type with prototype property.
String.prototype.wildExclamation = function() { 
  return `${this.toString()}!#?!#?`;
};

String.prototype.boldHtml = function() { 
  return `<b>${this.toString()}</b>`;
};



var test = "Hello";
console.log(test.toUpperCase()); // Standard String method
console.log(test.wildExclamation()); // Custom String method added via prototype
console.log(test.boldHtml()); // Custom String method added via prototype


console.log(String("World").toUpperCase()); // Standard String method
console.log(String("World").wildExclamation()); // Custom String method added via prototype
console.log(String("World").boldHtml()); // Custom String method added via prototype

A method is a function statement assigned to an object property

In JavaScript all statements containing a function() are functions, however, when an object property is assigned a function() statement the function is referred to as a method.

The first two snippets in listing 3-1 assign the wildExclamation and boldHtml properties to the built-in String data type using the prototype property. In both cases, the additional prototype properties are methods that return the string's value -- leveraging the String.toString() method -- using JavaScript template literals to produce a unique format.

Once a data type's prototype is updated with additional properties, all instances of the data type gain access to these properties. You can see in listing 3-1 the test string is able to access the standard String.toUpperCase() method, but also the custom wildExclamation() and boldHtml() methods. Similarly, you can see in listing 3-1 the constructor based string statements are also able to access the custom wildExclamation() and boldHtml() methods, in addition to the standard String.toUpperCase() method.

Now that you have an initial understanding of the JavaScript prototype property with the String data type, lets take a look at a more elaborate example that illustrates prototype-based programming in JavaScript. Listing 3-2 illustrates the use of constructor functions for objects.

Listing 3-2. ES5: Constructor functions for objects
// Constructor function
var Language = function(name,version) { 
 this.name = name;
 this.version = version;
 this.hello = function() { 
   return "Hello from " + this.name;
 }
};

// Create object instances from the prototype
var javascript = new Language("JavaScript","5");
var python = new Language("Python","2.7");


// Verify property value access
console.log(javascript.name);
console.log(javascript.version);
console.log(javascript.hello());
console.log(python.name);
console.log(python.version);
console.log(python.hello());


// You can add a property to an instance
python.typed = 'Dynamically';
console.log(python.typed);
// But the javscript instance won't have a 'typed' property
console.log(javascript.typed); // undefined

// You can use 'prototype' to add a property to the prototype (i.e. to all object instances)
// add 'typed' property
Language.prototype.typed = "Dynamically";
// Now the javascript instance has a 'typed' property, because it was added to its prototype
console.log(javascript.typed);

At first sight, the Language statement in listing 3-2 can appear to be a function expression -- like the ones presented in function declarations, function expressions and immediately-invoked function expressions (IIFE) in the JavaScript key concepts chapter -- but notice this function lacks a return statement, on top of which there are multiple assignments with the this keyword.

The Language statement in listing 3-2 is a constructor function, designed to build a custom object data type named Language with three properties: name, version and hello. In this sense, a constructor function in JavaScript serves the same purpose as a constructor in OOP languages -- it defines how to build objects -- and the this keyword in JavaScript also behaves in a similar way to other object-orientated languages, as a way to reference an instance of a given object and assign it values.

Next in listing 3-2 are the new Language("JavaScript","5"); and new Language("Python","2.7"); statements, which create two different instances of the Language custom object data type. In this case, the new keyword in JavaScript also behaves like other object-orientated languages, as a way to create object instances.

So thus far, you can conclude JavaScript ES5 also uses the this and new keywords like other object-orientated languages, however, it still leaves a couple of looming questions about listing 3-2. Why is there no explicit class definition ? (e.g. a class keyword) and how is it possible that a function -- technically a constructor function -- behaves like an object-orientated class ? (e.g. it makes use of the this keyword).

First, there's no explicit class definition in listing 3-2 because JavaScript ES5 doesn't have a class keyword. Although JavaScript gained the class keyword in ES6, in ES5 it was sufficient to use a regular function to act as a constructor in the creation of custom object data types.

Second, the reason a JavaScript function like the one in listing 3-2 is capable of behaving like an object-orientated class, is because a JavaScript function is a JavaScript object by means of inheritance, specifically prototypal inheritance. In the case of listing 3-2, it's said the Language function has a a prototype chain that includes the Object data type (i.e. it inherits its behaviors from the data type(s) in the chain). So similar to how object-orientated classes are said to have an inheritance hierarchy, prototype-based programming language objects are said to have a prototype chain.

In addition, recall that in the JavaScript data types chapter, I had already mentioned how all JavaScript data types -- which included the function data type -- are descendants of the all prevalent Object data type. So due to prototypal inheritance, a JavaScript function can use the this reference to assign itself a property (e.g. this.name, this.version) like any other Object data type, this works in a similar way to how the JavaScript's global object assigns itself global properties.

Now, unlike object-orientated languages that use classes to define inheritance hierarchies by extending other classes or use other constructs like abstract classes and interfaces to build secondary classes, in prototype-based programming languages, the inheritance process is much simpler and achieved through the prototype property, as you already learned in the first exercise in this chapter, listing 3-1.

If you turn your attention back to listing 3-2, after the property values for the javascript and python instances are output, the typed property is added to the python instance. However, this dot notation added to an instance reference (e.g. python.typed = 'Dynamically';) means you're only adding the property to the instance itself.

Toward the end of listing 3-2, you can see the Language.prototype.typed = "Dynamically"; statement adds the typed property through the prototype property. By leveraging the prototype prototype of an object, you're effectively altering all object instances that are based on the object data type -- in this case Language -- so both the python javascript references in listing 3-2 gain access to the typed property thanks to prototypal inheritance.

As you can see in listing 3-2, the process of altering and creating object properties is much simpler than that of altering and adding new classes in classical OOP languages -- even though the former may initially be confusing (e.g. a method behaving like a class).

Next, let's take a look at an even more elaborate example in listing 3-3 consisting of two constructor functions, where one constructor function inherits its behavior from the other constructor function, similar to how classes inherit their behavior from one another in object-orienteated programming languages.

Listing 3-3. ES5: Object inheritance
var Letter = function(value)  {
  this.value = value;
  this.iam = function() { 
   return `I am the ${this.constructor.name} ${this.value}`;
  };
  this.alphabet = function() { 
   return this.value + " is letter No." + '0abcdefghijklmnopqrstuvwxyz'.indexOf(this.value) + " in the alphabet";
  };
};

var test = new Letter("a");
console.log(test.iam());
console.log(test.alphabet());


var Vowel = function(value) { 
  if (["a","e","i","o","u"].indexOf(value) === -1)
        throw new SyntaxError("Invalid vowel");
  // Call parent constructor
  Letter.call(this,value);
};

// Assign child prototype same prototype as parent 
Vowel.prototype = Object.create(Letter.prototype);
// Reassign correct constructor to child
Vowel.prototype.constructor = Vowel;


var test2 = new Vowel("i");
console.log(test2.iam());
console.log(test2.alphabet());

The first statement in listing 3-3 is a constructor function that's designed to build a custom object data type named Letter with three properties: value, iam and alphabet. The only novelty of the Letter constructor function vs. the one from listing 3-2, is that the iam method uses the this.constructor.name reference to output the name of the constructor used to build the object -- the reason why it's used will be clearer in a moment. Next, you can see an instance of the Letter object is created and calls are made to its iam() and alphabet() methods.

The second constructor function in listing 3-3 Vowel also accepts an input value like Letter, however, notice the constructor function logic lacks any properties. The first step in Vowel is to verify the input value is a vowel, if it isn't the function immediately throws an error. If the input value is a vowel, then a call is made to the Letter function with an instance of Vowel (i.e.this) and the input value, this last step constructs a Letter instance in a similar way to how object-orientated languages invoke parent class constructors.

At this juncture, the Vowel constructor is only equipped to construct a Letter instance, however, it's still unaware of any properties present in the Letter instance, which is the purpose of the next two lines in listing 3-3. The Vowel.prototype = Object.create(Letter.prototype); statement works similarly to the prototype statements presented in listing 3-1 and 3-2, except it assigns all the object properties in one step, it says: create an object with all the properties of the Letter data type (i.e. value,iam,alphabet) and assign them to the properties of the Vowel data type, in other words, give the Vowel data type the same prototype chain as the Letter data type. Next, because this one step assignment of the prototype property also include a constructor function, the Vowel.prototype.constructor = Vowel; statement ensures the Vowel data type is reassigned the Vowel constructor.

Finally, an instance of Vowel is created and calls are made to its iam() and alphabet() methods. In this case, notice that Vowel is capable of calling the iam() and alphabet() methods inherited from the prototype chain of the Letter data type. In addition, notice that when a call is made to the iam() method, the output for the this.constructor.name statement adjusts depending on the object instance type (i.e. Letter or Vowel).

To new or not to new and static properties/methods

Now that you have a firm understanding of JavaScript's constructor functions, the prototype property and how the prototype chain works, it's time to readdress a topic first mentioned in the JavaScript data types chapter: the use of the new keyword to create object instances.

The new keyword as you learned in the previous chapter and this chapter, is used to create object instances. However, as early as listing 2-2 in the previous chapter, I mentioned how the new keyword should be avoided on simple object data types that map directly to primitive data types, since an object created with the new keyword is not the same as one created without it, going on to cite JavaScript strict equality and loose equality comparisons to illustrate the differences.

The rules of thumb you should follow to use -- or not -- the JavaScript new keyword:

Listing 3-4 illustrates the various JavaScript literal representations that don't require using the new keyword and still behave like objects.

Listing 3-4. ES5: Literal values and constructor objects
var letter = 'a';
var vowel = "E";
var number = 1; 
var dict = {};
var arr = [];
var bool = true;
var testing = function() { 
    return "testing"; 
};

// Leverage object methods thanks to constructor objects
console.log(letter.toUpperCase());
console.log(vowel.toLowerCase());
console.log(number.toExponential());

dict.language = "JavaScript";
console.log(dict);
console.log(dict.language);
console.log(Object.keys(dict));

arr.push("Python");
console.log(arr);
console.log(bool);
console.log(testing());

As you can see in listing 3-4, you can create a literal representations of a string with '' or "", a number with a number value, an object with {}, an Array with [] and a boolean with a boolean value, as well as a function with a function statement. More importantly, notice how listing 3-4 also illustrates how it's possible to use a literal representation's associated full-fledeged object properties and methods.

This last behavior is made possible because literal representations support constructor objects, which as you learned in the previous chapter, means a statement like var letter = 'a'; is equivalent to var letter = String('a'); and a statement like var number = 1; is equivalent to var number = Number(1);.

Now let's take a look at another example in listing 3-5 which showcases the use of the new keyword and demonstrates the second rule of thumb for using the new keyword with built-in object data types.

Listing 3-5. ES5: Built-in object data types with new and without new
// Date object instance
var today = new Date();
//today is a full-fledged object that can leverage methods from the Date data type
console.log(today);
console.log(today.getFullYear());
console.log(today.getDate());
console.log(today.toLocaleDateString("en-US"));


// Date type also has the static now() method which can be called without new  
var todayStatic = Date.now();

// todayStatic is a plain timestamp (static) value, with no access to other methods
console.log(todayStatic);

// Math data type is composed of all static properties and methods 
// new is never used since values don't change based on an object instance 
console.log(Math.PI);
console.log(Math.pow(2,2));

var square = Math.pow(2, 3);
console.log(square);

Listing 3-5 starts by creating an instance of the built-in Date data type with the new keyword and assiging it to the today reference. Immediatly after, various methods from the Date prototype are called on the today reference (e.g. getFullYear(), getDate(), toLocaleDateString()). Next, is the Date.now() statement which executes a Date operation without the need of a new keyword. This last operation without the new keyword is possible because now() is a static method of the Date data type which always returns the current Epoch time -- the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC -- irrespective of Date instance. Finally, the last examples in listing 3-5 illustrate various Math data type static properties and methods, which due to their static nature are called directly through the data type without the need of the new keyword.

As you can see, the new keyword allows you to create a full-fledged object to leverage all the prototype functionalities in a data type and its prototype chain, where as forgoing the new keyword is only allowed on data type properties and methods that are static in nature (i.e. values that don't change based on an object instance).

Now let's take a look at listing 3-6 which showcases the use of the new keyword on custom object data types.

Listing 3-6. ES5: Custom objects with new and without new
var Language = function(name,version) { 
 this.name = name;
 this.version = version;
 this.hello = function() { 
   return "Hello from " + this.name;
 }
};

var instanceLanguage = new Language("Python","2.7");


var literalLanguage = {"name":"Python","version":"2.7","hello":function() { return "Hello from " + this.name}};

// Compare instance and literal object values
console.log(literalLanguage.name);
console.log(instanceLanguage.name);
console.log(literalLanguage.version);
console.log(instanceLanguage.version);
console.log(literalLanguage.hello());
console.log(instanceLanguage.hello());
console.log(typeof(literalLanguage));
console.log(typeof(instanceLanguage));
console.log(Object.prototype.toString.call(literalLanguage));
console.log(Object.prototype.toString.call(instanceLanguage));


// However, even though the objects are identical in content, they're actually different 
if (literalLanguage == instanceLanguage) { 
  console.log("literalLanguage == instanceLanguage");
} else { 
   console.log("literalLanguage != instanceLanguage");
}

if (literalLanguage === instanceLanguage) { 
  console.log("literalLanguage === instanceLanguage");
} else { 
  console.log("literalLanguage !== instanceLanguage");
}

// In fact, not even two literal (or constructor) objects are ever equal
if ({} == {}) { 
  console.log("{} == {}");
} else { 
  console.log("{} != {}");
}

if ({} === {}) { 
  console.log("{} === {}");
} else { 
  console.log("{} !== {}");
}

// The Object.toString() compares the actual contents of an object 
if (literalLanguage.toString() == instanceLanguage.toString()) { 
  console.log("literalLanguage.toString() == instanceLanguage.toString()");
} else { 
   console.log("literalLanguage.toString() != instanceLanguage.toString()");
}

if (literalLanguage.toString() === instanceLanguage.toString()) { 
  console.log("literalLanguage.toString() === instanceLanguage.toString()");
} else { 
  console.log("literalLanguage.toString() !== instanceLanguage.toString()");
}


// With a constructor function, you get prototype inheritance
Language.prototype.bye = function()  { 
   return "Bye from " + this.name  + " " + this.version;
 };
 
// Literal objects can only append properties directly 
 literalLanguage.bye = function()  { 
   return "Bye from " + this.name + " " + this.version;
 };
 
// Same results, but constructor functions favor reusability
console.log(literalLanguage.bye());
console.log(instanceLanguage.bye());

// Create a new Language instance 
var otherInstanceLanguage = new Language("Python","3.7");
// You get access to bye because the Language prototype was modified
console.log(otherInstanceLanguage.bye());

// Create a new literal language
var otherLiteralLanguage = {"name":"Python","version":"2.7","hello":function() { return "Hello from " + this.name}}; 
// You won't get access to bye since you need to explicitly define it in a literal
//console.log(otherLiteralLanguage.bye()); // ERROR bye is not a function

Listing 3-6 starts by declaring a constructor function for a custom object data type named Language like the one in listing 3-2. Next, a Language instance is created using new and assigned to the instanceLanguage reference. Immediatly after, an identical literal object version of instanceLanguage is created and assigned to the literalLanguage reference.

Next, you can see the properties and data types of the literalLanguage and instanceLanguage references are identical. However, if you look at the equivalency tests that follow -- both strict equality and loose equality -- you can confirm the literalLanguage and instanceLanguage objects are different. This is not only because one object is built as a literal and the other with a constructor function, it's also because no two JavaScript objects will ever have the same in-memory representation, a fact you can confirm in the other equivalency tests for {} == {} and {} === {} which are always false. However, you can see in the final set of equivalency tests that the literalLanguage and instanceLanguage objects are in fact equal, when the objects are converted to their string representations using the Object.toString() method.

Right after the equivalency tests in listing 3-6, you can see the bye property is added to both the literalLanguage and instanceLanguage references. The version in literalLanguage is added by assigning a property directly to the object literal, where as the version in instanceLanguage is added by leveraging the prototype of the custom Language data type. Next, you can see the behavior of the bye property is identical in both versions, since the implementations are also identical.

However, the final snippets in listing 3-6 illustrate a major difference between creating literal objects (i.e. without new) and constructor based objects (i.e. with new). Because the bye property of the instanceLanguage reference is added through the object's prototype -- thanks to new -- it's automatically available on all other instances of the Language data type, a fact you can confirm in the otherInstanceLanguage object which is capable of calling bye right after the instance is created. On the other hand, because the bye property of the literalLanguage reference is added directly as an object property, it's contained to the object and effectively acts as a static value. In others words, literal objects are limited to individual objects which have static like behavior vs. constructor based objects which can propagate functionality to multiple objects by altering a custom data type's constructor function or leveraging its prototype.

Object getters and setters with get and set

Encapsulation is one of the primary features in OOP and consists primarily of restricting access to certain object values so they're administered through a dedicated set of methods. In OOP parlance, objects are said to have these methods known as getters and setters to achieve orderly access to their values. ES5 introduced the get and set keywords to provide JavaScript objects getter and setter methods like the ones used in other OOP languages.

Listing 3-7 illustrates the use of the JavaScript get and set keywords in the context of both literal and instance objects.

Listing 3-7. ES5: Getters and setters for objects
var literalLanguage = {
 "name":"Python",
 "version":"2.7",
 "hello":function() { return "Hello from " + this.name},
 get language() { 
   return this.name + " " + this.version;
 },
 set language(value) { 
  [this.name, this.version] = value.split(" ");
 },
};

// Verify property value access
console.log(literalLanguage.name);
console.log(literalLanguage.version);
console.log(literalLanguage.hello());
// Access language that uses get 
console.log(literalLanguage.language);

// Update language with setter 
literalLanguage.language = "JavaScript 5";

// Verify property values
// Access updated language that uses get
console.log(literalLanguage.language);
// language set updated inividual object properties  
console.log(literalLanguage.name);
console.log(literalLanguage.version);
console.log(literalLanguage.hello());

var Language = function(name,version) { 
 this.name = name;
 this.version = version;
 this.hello = function() { 
   return "Hello from " + this.name;
 }
};

var instanceLanguage = new Language("Python","2.7");

Object.defineProperties(Language.prototype, {
        language: {
             get: function()    { return this.name + " " + this.version; }
            ,set: function(value) { [this.name, this.version] = value.split(" ");   }
        }
});

// Verify property value access
console.log(instanceLanguage.name);
console.log(instanceLanguage.version);
console.log(instanceLanguage.hello());
// Access language that uses get 
console.log(instanceLanguage.language);

// Update language with setter 
instanceLanguage.language = "JavaScript 5";

// Verify property values
// Access updated language that uses get
console.log(instanceLanguage.language);
// language set updated inividual object properties  
console.log(instanceLanguage.name);
console.log(instanceLanguage.version);
console.log(instanceLanguage.hello());

Listing 3-7 starts by declaring a literal object similar to the one in listing 3-6, but unlike it, notice this one has get and set statements. The get language() statement indicates that when the language property on the object is accessed (e.g. literalLanguage.language) it call the assigned method. The set language(value) statement indicates that when a value is assigned to the language property (e.g. literalLanguage.language = "JavaScript") it call the assigned method. In the case of the get statement, the logic consists of returning a composite string made up of the object's name and version properties, where as in the case of the set statement, the logic consists of taking the value input and using it to reassign values to the object's name and version properties.

As you can see in listing 3-7, the literalLanguage object makes various calls to its properties and methods, including getting the language value -- supported by get -- and setting the language -- supported by set. In each case, you can see how easy it's to access and modify object values through these getter and setters methods, but more importantly, how the data remains encapsulated in the object.

The Language constructor function in listing 3-7 is like the one in listing 3-6, but unlike the literalLanguage literal object that directly declares get and set statements, a constructor function cannot use the same syntax. For this reason, you can see that after the Language constructor function is declared and the instanceLanguage object is created out of it, the Object.defineProperties is used to add the get and set statements to the object.

The Object.defineProperties method is part of the all prevalent Object data type so it's available on all data types to programmatically add object properties. If you look closely, the first argument to Object.defineProperties is Language.prototype, which indicates to add a property on the Language's prototype, meaning that all Language object instances will get said property. Next, is the language property statement which has identical get and set statements to the literalLanguage literal object from the beginning of the listing. Finally, the same series of get and set operations are performed on the instanceLanguage object instance, illustrating how it's possible to leverage get and set statements on custom data types built from constructor functions.

Does JavaScript have other encapsulation/accesibility constructs besides the get and set keywords ?

In most OOP languages, the use of getters and setters is closely used in conjuction with other accesibility constructs like private properties or protected methods. In JavaScript there is no such thing.

As a mere convention, what is often used in JavaScript ES5 and later ECMAScript versions to indicate a private property/method (i.e. one that shoudn't be accessed directly) is to prefix a reference with an underscore _ (e.g. _dontaccess = "OK"). Of course, this doesn't preclude someone from accesing it directly (e.g. obj._dontaccess), it's simply a matter of the leading underscore making it more obvious a property isn't meant to be called directly.

If you're looking for more formal support of private properties or protected methods in JavaScript, one option is to use TypeScript. TypeScript does support constructs like the private and protected keywords, which in combination with getters and setters provides a similar feel to encapsulation/accesibility behaviors present in other OOP languages (e.g. Java, C#).

ES6: The class, constructor, extends, super, get, set and static keywords

If you skim through the examples in the previous ES5 section once again, you'll realize that with the exception of the subtle prototype-based programming behavior, JavaScript ES5 isn't too far away from most OOP behaviors. For example, JavaScript data types are very much like OOP classes, just as constructor functions are pretty similar to OOP constructors. In a similar way, listing 3-3 illustrates OOP like inheritance between objects, as well as the ability to call parent constructors like it's done in OOP inheritance. And let's not forget, ES5 data types can also make use of the this keyword methods to reference instances, can implement getter/setter methods with the get and set keywords like other OOP language and also have the ability to define static properties and methods.

However, for all the OOP like goodies ES5 has to offer, it lacks one thing: explicit syntax or 'widely used' OOP syntax as it's used in other programming languages. Someone with a background in a mainstream OOP language (e.g. Java, C#, C++) would be at a loss trying to interpret most of the OOP behavior written in ES5 syntax in the past section. Therefore, ES6 aligned itself more closely to OOP syntax used in other languages to make JavaScript object-orientated and prototype-based programming more amenable.

And when I say amenable I mean that literally, as there's nothing in ES6 that forces anyone to use this new syntax to attain JavaScript object-orientated or prototype-based behaviors. In fact, most of the OOP focused keywords introduced in ES6 are what's known as "syntactic sugar" -- a concept introduced in the previous chapter discussing ES6 syntax -- which represent constructs to make things easier to read and express. In this sense, the following JavaScript ES6 keywords are really 'crutches' to make JavaScript OOP syntax easier to analyze, if and when they provide something novel that wasn't possible in ES5 it's pointed out.

ES6: First level classes with class and constructor

As you learned in the previous ES5 section, what is often referred to as a class in other OOP languages, in JavaScript ES5 is an amalgamation of a data type with a powerful function that has the ability to create object instances and which has the prototype property to extend its functionality, among other things. In order to call a spade a spade, ES6 designers decided to introduce the class and constructor keywords which are illustrated in listing 3-8.

Listing 3-8. ES6: Classes with class and constructor syntax
class Language  {
   constructor(name,version) {
     this.name = name;
     this.version = version;
   }
   hello() {
     return "Hello from " + this.name;
   }
}

// Create object instances from class
let javascript = new Language("JavaScript","5");
let python = new Language("Python","2.7");


// Verify property value access
console.log(javascript.name);
console.log(javascript.version);
console.log(javascript.hello());
console.log(python.name);
console.log(python.version);
console.log(python.hello());

// Properties can be added to an object instance just like in ES5 
// You can add a property to an instance
python.typed = 'Dynamically';
console.log(python.typed);
// But the javscript instance won't have a 'typed' property
console.log(javascript.typed); // undefined

// The 'prototype' is also accesible like in ES5 data types
Language.prototype.typed = "Dynamically";
// Now the javascript instance has a 'typed' property, because it was added to its prototype
console.log(javascript.typed);

First off, I invite you to compare the examples in listing 3-8 with those in listing 3-2, because they both achieve the same end result but with different syntax.

The first statement in listing 3-8 relies on the class keyword to create a class named Languge. Next, inside the Language class statement are two methods: constructor and hello. If it wasn't obvious by its name, the constructor method is designed to be called every time an object instance of a class is created (i.e. with the new keyword), a concept which is almost universal across all OOP languages. In this case, the constructor method accepts two arguments which means all Language object instances must be created with two parameters, in addition, the constructor uses the two arguments to create the name and version object properties relying on the this keyword to reference the object instance. Finally, the hello class method returns a text message accompanied by the current value of an object's name property.

Next, two instances of the Language class are created with the new keyword and the statements that follow output the instance's properties and call its hello method, a process which is identical to the sequence presented in listing 3-2 but which relies on a constructor function and a function assigned to a property.

So what's the difference between creating JavaScript object instances like it's done in listing 3-8 and listing 3-2 ? Functionally none, the only difference is the syntax in listing 3-8 is much more obvious, particularlly to those with OOP experience. To further confirm both approaches are functionally equivalent, notice the second part of listing 3-8 illustrates how it's possible to add a property to an individual object instance, as well as alter the behavior of a class through its prototype, just like it's done in listing 3-2.

ES6: Class inheritance with extends and super

Inheritance is one of the major features of OOP since it allows classes to retain behaviors from other classes, a process which favors code reusability and the creation of object hierarchies. For example, with inheritance it's possible to have a parent class (e.g.Builiding) and reuse it to create more granular classes (e.g. ApartmentComplex, Hospital, Firehouse) without the need to reimplement the logic in the parent class.

Although ES5 supports object inheritance -- as illustrated in listing 3-3 -- its implementation syntax is not very obvious, especially if you compare it to other OOP languages which have dedicated keywords for inheritance scenarios. ES6 introduced the extends and super keywords with the intent to simplify JavaScript class inheritance. Listing 3-9 illustrates the use of the extends and super keywords in conjunction with the class and constructor keywords presented in the previous section.

Listing 3-9. ES6: Class inheritance
class Letter {
   constructor(value) { 
        this.value = value;
    }
    
   iam() { 
   return `I am the ${this.constructor.name} ${this.value}`;
   }
   
   alphabet() { 
   return this.value + " is letter No." + '0abcdefghijklmnopqrstuvwxyz'.indexOf(this.value) + " in the alphabet";
  }
}

let test = new Letter("a");
console.log(test.iam());
console.log(test.alphabet());

class Vowel extends Letter {
    constructor(value) { 
    super(value);
    if (["a","e","i","o","u"].indexOf(value) === -1)
        throw new SyntaxError("Invalid vowel");
   }
}


let test2 = new Vowel("i");
console.log(test2.iam());
console.log(test2.alphabet());


let test3 = new Vowel("d"); // Raises syntax error in constructor

First off, I invite you to compare the examples in listing 3-9 with those in listing 3-3, because they both achieve the same end result but with different syntax.

The first declaration in listing 3-9 defines the Letter class that makes use of the class and constructor keywords, in addition to declaring the iam() and alphabet() methods. Next, an instance of the Letter class is created and calls are made to its various methods. Up to this point, it's a standard ES6 class and object creation sequence.

The second declaration in listing 3-9 defines the Vowel class which makes use of the extends keyword with the Letter class. This syntax allows the Vowel class to inherit the same behaviors (i.e. properties and methods) declared in the Letter class. Next, inside the Vowel class you can see it only contains a constructor method which uses the super keyword and generates an error if a Vowel object is created with something other than vowel (i.e. a, e, i, o, u). So why doesn't the Vowel class have any properties and methods ? It could, but in this case it inherits the value property and iam() and alphabet() methods from the Letter class.

When a Vowel object instance is created with var test2 = new Vowel("i");, the Vowel constructor method is called and the super(value) tells JavaScript to call the parent class's constructor (i.e. Letter) which creates the value property and gives it access to the iam() and alphabet() methods. Next, you can see how it's possible to call the iam() and alphabet() methods on a Vowel object instance. Finally, the last line in listing 3-9 attempts to create a Vowel object instance with the d value, but because the constructor raises an error in case the input is not a vowel the object instance creation fails.

ES6: Class properties with get, set and static

Remember back in listing 3-7 how get and set statements were limited to literal objects or required a programmatic approach to be added to data types ? With the introduction of ES6 classes, get and set statements can be part of a class just like they're used in other OOP languages. Listing 3-10 illustrates the use of getters and setters in ES6 classes.

Listing 3-10. ES6: Getters and setters in classes
class Language  {
   constructor(name,version) {
     this._name = name;
     this._version = version;
   }
   get name() {
     return this._name;
   }
   set name(value) {
     this._name = value;
   }
   get version() {
     return this._version;
   }
   set version(value) {
     this._version = value;
   }
   hello() {
     return "Hello from " + this.name;
   }
}

// Create object instances from class
let javascript = new Language("JavaScript","5");

// Verify property value access through getters 
console.log(javascript.name);
console.log(javascript.version);
console.log(javascript.hello());

// Reassign property values through setters
javascript.name = "ECMAScript";
javascript.version = "ES2015";

// Verify property value updates through getters
console.log(javascript.name);
console.log(javascript.version);
console.log(javascript.hello());

The Language constructor in listing 3-10 starts by creating the _name and _version object properties -- notice the leading underscore in both properties, which as mentioned previously is a common convention to name private JavaScript properties. Next, a couple of getters and setters are declared for the name and version properties, both of which leverage the _name and _version properties, respectively.

Next, a Languge instance is created and assigned to the javascript reference. Immediatly after, the instance's name and version properties are accessed, both of which are supported through the class's getter methods. Next, the instance's name and version properties are reassigned -- both of which are supported through the class's setter methods -- and later output to confirm the reassignment setter logic.

Besides the get and set keywords, another adddition to JavaScript ES6 classes is the static keyword. Remember back in listing 3-5 and listing 3-6 you learned about static properties ? Static properties and methods are those that don't change between object instances and can be accesed without creating an object instance with new. With support for the static keyword, ES6 classes support static methods, as illustrated in listing 3-11.

Listing 3-11. ES6: Static properties in classes
class Alphabet {

    static vowels() {
      return ['a','e','i','o','u'];
    }

    static first() { 
      return "a";
    }

}

console.log(Alphabet.vowels());
console.log(Alphabet.first());

Alphabet.last = "z";

console.log(Alphabet.last);

The Alphabet class in listing 3-10 contains the vowels() and first() static methods. More importantly, notice how it's possible to invoke the vowels() and first() methods without creating an object instance with new -- just like it was done for built-in data types in listing 3-5.

So what about static class properties ? The static keyword is only intended to define static methods. However, it's equally possible to declare static class properties outside the class structure by adding them as regular object properties, as it's shown toward the end of listing 3-10. Notice the last property is added to the Alphabet class and is later accessed like a regular object property without requiring an object instance, just like it's done in ES5 and was shown in listing 3-6.