OOP principles support

The second requirement that allows us to consider JavaScript as an Object-Oriented language involves the support of at least three principles—encapsulation, inheritance, and polymorphism. Let analyze how JavaScript supports each of these principles.

Encapsulation

Objects are central to the Object-Oriented Programming model, and they represent the typical expression of encapsulation, that is, the ability to concentrate in one entity both data (properties) and functions (methods), hiding the internal details.

In other words, the encapsulation principle allows an object to expose just what is needed to use it, hiding the complexity of its implementation. This is a very powerful principle, often found in the real world that allows us to use an object without knowing how it internally works. Consider for instance how we drive cars. We need just to know how to speed up, brake, and change direction. We do not need to know how the car works in detail, how its motor burns fuel or transmits movement to the wheels.

To understand the importance of this principle also in software development, consider the following code:

var company = { 
    name: "ACME Inc.", 
    employees: [], 
    sortEmployeesByName: function() {...} 
}; 

It creates a company object with a name, a list of employees and a method to sort the list of employees using their name property. If we need to get a sorted list of employees of the company, we simply need to know that the sortEmployeesByName() method accomplishes this task. We do not need to know how this method works, which algorithm it implements. That is an implementation detail that encapsulation hides from us.

Hiding internal details and complexity has two main reasons:

  • The first reason is to provide a simplified and understandable way to use an object without the need to understand the complexity inside. In our example, we just need to know that to sort employees, we have to call a specific method.
  • The second reason is to simplify change management. Changes to the internal sort algorithm do not affect our way to order employees by name. We always continue to call the same method. Maybe we will get a more efficient execution, but the expected result will not change.

We said that encapsulation hides internal details in order to simplify both the use of an object and the change of its internal implementation. However, when internal implementation depends on publicly accessible properties, we risk to frustrate the effort of hiding internal behavior. For example, what happens if you assign a string to the property employees of the object company?

company.employees = "this is a joke!"; 
 
company.sortEmployeesByName(); 

The assignment of a string to a property whose value is an array is perfectly legal in JavaScript, since it is a language with dynamic typing. But most probably, we will get an exception when calling the sort method after this assignment, since the sort algorithm expects an array.

In this case, the encapsulation principle has not been completely implemented. A general approach to prevent direct access to relevant properties is to replace them with methods. For example, we can redefine our company object as in the following:

function Company(name) { 
    var employees = []; 
 
    this.name = name; 
 
        this.getEmployees = function() {  
            return employees; 
            }; 
    this.addEmployee = function(employee) {  
            employees.push(employee); 
            }; 
    this.sortEmployeesByName = function() { 
            ... 
            }; 
} 
 
var company = new Company("ACME Inc."); 

With this approach, we cannot access directly the employees property, but we need to use the getEmployees() method to obtain the list of employees of the company and addEmployee() to add an employee to the list. This guarantees that the internal state remains really hidden and consistent. The way we created methods for the Company() constructor is not the best one. We will see why and how to use a better approach in Chapter 3, Working with Encapsulation and Information Hiding.

This is just one possible approach to enforce encapsulation by protecting the internal state of an object. This kind of data protection is usually called information hiding and, although often linked to encapsulation, it should be considered as an autonomous principle. Information hiding deals with the accessibility to an object's members, in particular to properties. While encapsulation concerns hiding details, the information hiding principle usually allows different access levels to the members of an object.

We will discuss more in deep encapsulation and information hiding in the next chapter, since different solutions can be applied in JavaScript in order to emulate the common access levels supported by other OOP languages.

Inheritance

OOP, inheritance enables new objects to acquire the properties of existing objects. This relationship between two objects is very common and can be found in many situations in real life. It usually refers to creating a specialized object starting from a more general one.

In OOP, inheritance enables new objects to acquire the properties of existing objects. This relationship between two objects is very common and can be found in many situations in real life. It usually refers to creating a specialized object starting from a more general one. Let's consider, for example, a person: he has some features such as name, surname, height, weight, and so on. This set of features describes a generic entity that represents a person. Using abstraction, we can select the features needed for our purpose and represent a person as an object:

If we need a special person who is able to program computers, that is a programmer, we need to create an object that has all the properties of a generic person plus some new properties that characterize the programmer object. For instance, the new programmer object can have a property describing which programming language they know.

Suppose we choose to create the new programmer object by duplicating the properties of the person object and adding to it the programming language knowledge as follows:

This approach is in contrast with the Object-Oriented Programming goals. In particular, it does not reuse existing code, since we are duplicating the properties of the person object. A more appropriate approach should reuse the code created to define the person object. This is where the inheritance principle can help us. It allows us to share common features between objects, avoiding code duplication:

Note

Inheritance is also called subclassing in languages that support classes. A class that inherits from another class is called a subclass, while the class from which it is derived is called a superclass. Apart from the naming, the inheritance concept is the same, although of course it does not seem suited to JavaScript.

We can implement inheritance in JavaScript in various ways. Consider, for example, the following constructor of person objects:

function Person() { 
this.name = ""; 
this.surname = ""; 
} 

In order to define a programmer as a person specialized in computer programming, we will add a new property describing its knowledge about a programming language: knownLanguage.

A simple approach to create the programmer object that inherits properties from person is based on prototype. Here is a possible implementation:

function Programmer() { 
    this.knownLanguage = ""; 
} 
 
Programmer.prototype = new Person(); 

We will create a programmer with the following code:

var programmer = new Programmer(); 

We will obtain an object that has the properties of the person object (name and surname) and the specific property of the programmer (knownLanguage), that is, the programmer object inherits the person properties.

This is a simple example to demonstrate that JavaScript supports the inheritance principle of Object-Oriented Programming at its basic level. Inheritance is a complex concept that has many facets and several variants in programming, many of them dependent on the used language.

In Chapter 4, Inheriting and Creating Mixins, we will explore more in depth how JavaScript supports inheritance, analyzing different approaches and more advanced topics such as overriding and multiple inheritance.

Polymorphism

In Object-Oriented Programming, polymorphism is understood in different ways, even if the basis is a common notion—the ability to handle multiple data types uniformly.

Support of polymorphism brings benefits in programming that go toward the overall goal of OOP. Mainly, it reduces coupling in our application, and in some cases, allows to create more compact code.

The most common ways to support polymorphism with a programming language include:

  • Methods that take parameters with different data types (overloading)
  • Management of generic types, not known in advance (parametric polymorphism)
  • Expressions whose type can be represented by a class and classes derived from it (subtype polymorphism or inclusionpolymorphism)

In most languages, overloading is what happens when you have two methods with the same name but different signatures. At compile time, the compiler works out which method to call based on matching between types of invocation arguments and method's parameters. The following is an example of method overloading in C#:

public int CountItems(int x) { 
    return x.ToString().Length; 
} 
 
public int CountItems(string x) { 
    return x.Length; 
} 

The CountItems()method has two signatures-one for integers and one for strings. This allows to count the number of digits in a number or the number of characters in a string in a uniform manner, just calling the same method.

Overloading can also be expressed through methods with different numbers of arguments, as shown in the following C# example:

public int Sum(int x, int y) { 
    return Sum(x, y, 0); 
} 
 
public int Sum(int x, int y, int z) { 
    return x+ y + z; 
} 

Here, we have the Sum()method that is able to sum two or three integers. The correct method definition will be detected on the basis of the number of arguments passed.

As JavaScript developers, we are able to replicate this behavior in our scripts. For example, the C# CountItems() method becomes in JavaScript as follows:

function countItems(x) { 
return x.toString().length; 
} 

While the Sum() example will be as follows:

function sum(x, y, z) { 
x = x?x:0; 
y = y?y:0; 
z = z?z:0; 
return x + y + z; 
} 

Or, using the more convenient ES6 syntax:

function sum(x = 0, y = 0, z = 0) { 
return x + y + z; 
} 

These examples demonstrate that JavaScript supports overloading in a more immediate way than strong-typed languages.

Note

In strong-typed languages, overloading is sometimes called static polymorphism, since the correct method to invoke is detected statically by the compiler at compile time. This is opposed to dynamic polymorphism that concerns method overriding, as we will see in a later chapter.

Parametric polymorphism allows a method to work on parameters of any type. Often it is also called generics and many languages support it in built-in methods. For example, in C#, we can define a list of items whose type is not defined in advance using the List<T> generic type. This allows us to create lists of integers, strings, or any other type.

We can also create our generic class as shown by the following C# code:

public class Stack<T> { 
    private T[] items; 
    private int count; 
    public void Push(T item) { ... } 
    public T Pop() { ... } 
} 

This code defines a typical stack implementation whose item's type is not defined. We will be able to create, for example, a stack of strings with the following code:

var stack = new Stack<String>(); 

Due to its dynamic data typing, JavaScript supports parametric polymorphism implicitly. In fact, the type of function's parameters is inherently generic, since its type is set when a value is assigned to it. The following is a possible implementation of a stack constructor in JavaScript:

function Stack() 
{ 
 this.stack = []; 
 this.pop = function(){ 
  return this.stack.pop(); 
 } 
 this.push = function(item){ 
  this.stack.push(item); 
 } 
} 

Subtype polymorphism allows the consideration of objects of different types, but with an inheritance relationship, to be handled consistently. This means that wherever I can use an object of a specific type, here I can use an object of a type derived from it.

Let's see a C# example to clarify this concept:

public class Person { 
 public string Name {get; set;} 
 public string SurName {get; set;} 
} 
 
public class Programmer:Person { 
    public String KnownLanguage {get; set;} 
} 
 
public void WriteFullName(Person p) { 
 Console.WriteLine(p.Name + " " + p.SurName);    
} 
 
var a = new Person(); 
a.Name = "John"; 
a.SurName = "Smith"; 
 
var b = new Programmer(); 
b.Name = "Mario"; 
b.SurName = "Rossi"; 
b.KnownLanguage = "C#"; 
 
WriteFullName(a);    //result: John Smith 
WriteFullName(b);    //result: Mario Rossi 

In this code, we again present the definition of the Person class and its derived class Programmer and define the method WriteFullName() that accepts argument of type Person. Thanks to subtype polymorphism, we can pass to WriteFullName() also objects of type Programmer, since it is derived from Person. In fact, from a conceptual point of view a programmer is also a person, so subtype polymorphism fits to a concrete representation of reality.

Of course, the C# example can be easily reproduced in JavaScript since we have no type constraint. Let's see the corresponding code:

function Person() { 
this.name = ""; 
this.surname = ""; 
} 
 
function Programmer() { 
    this.knownLanguage = ""; 
} 
 
Programmer.prototype = new Person(); 
 
function writeFullName(p) { 
    console.log(p.name + " " + p.surname); 
} 
 
var a = new Person(); 
a.name = "John"; 
a.surname = "Smith"; 
 
var b = new Programmer(); 
b.name = "Mario"; 
b.surname = "Rossi"; 
b.knownLanguage = "JavaScript"; 
 
writeFullName(a);    //result: John Smith 
writeFullName(b);    //result: Mario Rossi 

As we can see, the JavaScript code is quite similar to the C# code and the result is the same.