Inheriting from a Class

In the past, to implement prototypal inheritance you had to literally change the prototype on the constructor function. This was rather inconvenient, error prone, and not intuitive. Also, there was no clear way to override methods. This was so hard that most programmers either got it wrong or relied on libraries to do it for them.

The updated syntax for inheritance greatly simplifies the task of creating prototypal inheritance. There is also a clear and elegant way to override methods. The syntax for inheritance is much like the one used by other mainstream languages, especially Java—that’s good news and bad news.

The good news is that programming inheritance is now very easy and approachable, and the code is easier to understand and maintain.

The bad news, however, is that the syntax is so much like class-based inheritance. That may lead you to believe that JavaScript supports class-based inheritance. But although the syntax looks like class-based inheritance, semantically, under the hood, JavaScript still uses prototypal inheritance, as we discussed in the previous section. Be warned.

To understand how the new inheritance syntax relates to prototypal inheritance, let’s look at some examples.

Extending a Class

In the past we created constructor functions to represent classes and modified their prototype to implement prototypal inheritance. Now, in modern JavaScript, we create classes using the class keyword. Likewise, we implement prototypal inheritance using a different keyword as well.

We need a base class to serve as the prototype and a derived class to reuse the prototype. In short, we need a simple inheritance hierarchy. For that, let’s first create a class that will serve as the base. Let’s start with a class, Person:

 class​ Person {
 constructor​(firstName, lastName) {
  console.log(​'initializing Person fields'​);
 this​.firstName = firstName;
 this​.lastName = lastName;
  }
 
  toString() {
 return​ ​`Name: ​${​this​.firstName}​ ​${​this​.lastName}​`​;
  }
 
 get​ fullName() { ​return​ ​`​${​this​.firstName}​ ​${​this​.lastName}​`​; }
 
 get​ surname() { ​return​ ​this​.lastName; }
 }

Now let’s inherit from the Person class a ReputablePerson class.

 class​ ReputablePerson ​extends​ Person {
 constructor​(firstName, lastName, rating) {
  console.log(​'creating a ReputablePerson'​);
 super​(firstName, lastName);
 this​.rating = rating;
  }
 }

Use extends to inherit a class from another class. In the constructor, a call to super() is required and should appear before accessing this. The call to super() will invoke the constructor of the base class, and JavaScript insists that the state of the base part of the instance is initialized before the state of the derived part. Swapping the last two lines in the constructor of ReputablePerson will result in a runtime error.

Overriding Methods

Although we can reuse the properties and methods of a base class, from the extensibility point of view, in the derived class you may want to provide an alternative implementation for a method or a property that is in the base. To override a method of the base class, write a method in the derived class with the same method name as in the base class. Likewise, to override a property in the derived, define a property with the same name as in the base class.

Let’s override the toString() method and the firstName property of Person in the ReputablePerson class:

 toString() {
 return​ ​`​${​super​.toString()}​ Rating: ​${​this​.rating}​`​;
 }
 
 get​ fullName() {
 return​ ​`Reputed ​${​this​.surname}​, ​${​super​.fullName}​ `​;
 }

Since a field, method, or a property may be in the base class, in the derived class, or in the instance itself, we have to be careful to use the proper syntax to access the appropriate thing. Here are the rules you need to follow:

  • To access the member in the instance or in the derived class instead of the one in the base class, use this—remember that this is dynamically scoped.

  • If a member does not exist in the instance or in the derived class but exists in the base class, use this. If in the future you override this member in the derived class, then it will take precedence—this is most likely what you’d want.

  • To bypass the member of the derived class and access the one in the base class, use super().

In the overriding toString() method, we called the toString() method of the base class using the super. notation. Likewise, in the overriding property getter we access the fullName property of the base class by using the super. prefix. However, to access the surname getter, we did not use the super. notation. That’s because there is no member with that name in the derived class that shadows a member in the base class. It is safe to use this. here for two reasons:

  • When a field or property is not present in an object, JavaScript will search for it in the prototype chain—that is, it will automatically look it up in the base class.

  • If we override the field or property in the derived class, then we will correctly use the overridden field or property in the instance and not the one in the base class.

In general, the only time we should use the super. prefix is

  • to access a member of the base class from within an overriding member of the derived class with the same name

  • from any other member when we intentionally want to bypass a member in the derived class and access the one in the base class

Let’s use the derived class we created to see the overriding method and property in action.

 const​ alan = ​new​ ReputablePerson(​'Alan'​, ​'Turing'​, 5);
 console.log(alan.toString());
 console.log(alan.fullName);

The output from the code shows the constructor call sequence. The overriding method and property was called, and the overriding members collaborated with the base members:

 creating a ReputablePerson
 initializing Person fields
 Name: Alan Turing Rating: 5
 Reputed Turing, Alan Turing

extends Implies Prototypal Inheritance

Even though JavaScript uses the extends keyword, which is popular for inheritance in Java, it’s important to remember that the inheritance is prototype based and not class based. Let’s verify this key design for inheritance in JavaScript with an example.

We’ll continue with the Person and ReputablePerson classes we’ve been using. We can obtain the prototype of an object using the Reflect.getPrototypeOf() method. Since prototypes form a chain, we can repeatedly or recursively use this method to walk the inheritance hierarchy—that is, the prototype chain.

Let’s write a function that, given an object, will print it and then print the object’s prototype chain.

 const​ printPrototypeHierarchy = ​function​(instance) {
 if​(instance !== ​null​) {
  console.log(instance);
  printPrototypeHierarchy(Reflect.getPrototypeOf(instance));
  }
 };

The method prints the given object and recursively calls itself, passing the prototype of the object at hand, until it reaches the end of the prototype chain. Let’s call it on an instance of ReputablePerson:

 const​ alan = ​new​ ReputablePerson(​'Alan'​, ​'Turing'​, 5);
 
 printPrototypeHierarchy(alan);

The excerpt of the output from this call to printPrototypeHierarchy is shown next:

 ReputablePerson { firstName: 'Alan', lastName: 'Turing', rating: 5 }
 ReputablePerson {}
 Person {}
 {}

The first line of the output shows the details of the first instance passed to printPrototypeHierarchy; the second line shows the prototype of that instance. The third line shows the prototype of the prototype, and the fourth line shows the terminal prototype.

Changing the Prototype Chain

Unlike class-based inheritance hierarchy, the prototype chain is not frozen in time. We can change the prototype chain—cautiously—if we desire. We’ll see many reasons to change the prototype in Chapter 11, Exploring Metaprogramming.

Let’s alter the inheritance hierarchy we saw in the previous output to include a new object—for example, the prototype of a ComputerWiz class—in the chain. Here’s the code for that:

 class​ ComputerWiz {}
 
 Reflect.setPrototypeOf(Reflect.getPrototypeOf(alan), ComputerWiz.prototype);
 
 console.log(​'...after change of prototype...'​);
 
 printPrototypeHierarchy(alan);

Instead of altering the prototype of the instance alan, we’re altering the prototype of the prototype of alan. Let’s take a look at the excerpt from the output after this change:

 ...after change of prototype...
 ReputablePerson { firstName: 'Alan', lastName: 'Turing', rating: 5 }
 ReputablePerson {}
 ComputerWiz {}
 {}

Compare this output with the one we saw before the change to the prototype hierarchy. From the prototype chain, everything below and including Person has been replaced with a new chain starting with ComputerWiz.

We modified the prototype of prototype of alan—remember that instances of a class share prototypes. Thus, if we create another object of ReputablePerson, then its prototype chain will be the same as the modified prototype chain of alan. Let’s verify that:

 const​ ada = ​new​ ReputablePerson(​'Ada'​, ​'Lovelace'​, 5);
 printPrototypeHierarchy(ada);

Here’s the output for this part of the code:

 ReputablePerson { firstName: 'Ada', lastName: 'Lovelace', rating: 5 }
 ReputablePerson {}
 ComputerWiz {}
 {}

Not only does ada, which was created after alan, share the prototype chain with alan, any object of ReputablePerson that may have been created before alan was created also shares the same prototype chain.

We have to be very careful when changing the prototype chains—we’ll see use cases for this in Chapter 11, Exploring Metaprogramming—if the change is drastic and unintended, it may result in unexpected, hard-to-debug behavior.

Using Default Constructors

Recall that if you do not write a constructor, JavaScript provides a default constructor for the class. The same rule applies to derived classes, with one bonus—the default constructor automatically calls super to pass any arguments to the base class. This feature is nice and removes the need to write silly constructors that do not do anything except pass data to the base class. Once you get used to it, you may wish languages like Java and C# had this feature.

Let’s extend an AwesomePerson class from the Person class we wrote earlier. Our AwesomePerson class is not going to introduce any new fields; it will only override a property of the base class. If we were forced to write a constructor, that would be pure noise to merely forward the parameters to the base class. The default constructor thankfully acts as a nice quiet passthrough constructor, as we see here:

 class​ AwesomePerson ​extends​ Person {
 get​ fullName() {
 return​ ​`Awesome ​${​super​.fullName}​`​;
  }
 }

Let’s create an object of this new class to see how awesome the default constructor is:

 const​ ball = ​new​ AwesomePerson(​'Lucille'​, ​'Ball'​);
 console.log(ball.fullName);

We passed the first and last names to the constructor when creating the instance ball, even though we did not write a constructor for the AwesomePerson class. Let’s take a look at the output to see how this decision turned out:

 initializing Person fields
 Awesome Lucille Ball

Quite well, as we can see; the constructor of the Person base class was called when the instance of AwesomePerson was created and the fields on the base class were properly initialized.

Extending Legacy Classes

In the examples so far, we’ve inherited from a class created using the class keyword. However, we can inherit from a class created using the old function keyword as well—this is necessary for backward compatibility—classes defined using the old syntax can be used as a base class with the new extends keyword.

Here’s a short example of a class created using the legacy approach and a derived class created using the newer syntax:

 function​ LegacyClass(value) {
 this​.value = value;
 }
 
 class​ NewClass ​extends​ LegacyClass {}
 
 console.log(​new​ NewClass(1));

Even though the base class is created using the legacy approach, the derived class plays by the new rules—the default constructor performs the automatic passthrough of the arguments.

You’ve learned how to extend from classes and to build a class hierarchy—that is, how to implement prototypal inheritance using the updated syntax for classes and inheritance. There’s one gotcha, however, when working with a hierarchy of classes—deciding the type of instance to create from within methods. Next, we’ll see what JavaScript provides to address that question.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset