Prototypal Inheritance

In earlier chapters, we met JavaScript's classes. We mentioned that classes are syntactic sugar on top of JavaScript's built-in prototype system. In this chapter, we'll learn about prototypal inheritance and how ES6 classes map to the constructor/prototype pattern.

Prototypal inheritance is a core feature in JavaScript. Objects can inherit properties and methods directly from other objects, known as prototypes, rather than from classes. This approach differs from the classical inheritance model used in most other OO languages. It provides more flexible and dynamic object creation and modification, enabling objects to share behavior by referencing a prototype object.

Prototype objects may contain both methods and properties. However, the vast majority of prototypes only contain methods. To simplify our language, we won't mention the possibility of ordinary properties in prototype objects. Nevertheless, you should try to remember that prototypes may contain ordinary properties.

Note that most JavaScript developers routinely use classes in new code. Nevertheless, you should still understand how classes work "under the hood" with JavaScript's prototype system.

Prototype Objects

The key to understanding prototypal inheritance begins with understanding prototypes (or, sometimes, prototype objects), of which there are two kinds: function prototypes and object prototypes. They are closely related but are different in how they are used.

Function Prototypes

Most JavaScript functions and classes have a property named prototype. This property usually references an object that is called the function prototype. For example, consider the following code:

class Cat {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }

  whoAmI() {
    console.log(`My name is ${this.name}.`,
                `\nI am a ${this.color} cat.`);
  }
}

When JavaScript encounters the class definition in this code, it creates a Cat function called the constructor function which, confusingly, is not the same as the constructor method. It then creates an object that contains the constructor and whoAmI methods abd assigns the object to the Cat.prototype property. This object is referred to as the function prototype for the Cat class.

Object Prototypes

All JavaScript objects have an object prototype. The object prototype for an object is an object that contains the methods that an object inherits. For example, consider the following code:

class Cat {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }

  whoAmI() {
    console.log(`My name is ${this.name}.`,
                `\nI am a ${this.color} cat.`);
  }
}

//highlight
let cheddar = new Cat('Cheddar', 'ginger');
let cheddarProto = Object.getPrototypeOf(cheddar);

console.log(Object.getOwnPropertyNames(cheddarProto));
// ['constructor', 'whoAmI'];

cheddar.whoAmI();  // My name is Cheddar.
                   // I am a ginger cat.
//endhighlight

In this code, we've defined a Cat class, just as we did in the previous subsection. This time, we've also created an instance named cheddar.

Then, on line 15, we've called Object.getPrototypeOf to retrieve cheddar's object prototype. On line 17, we print a list of the methods defined by cheddar's object prototype; the list contains the names of the two methods defined by the Cat class: constructor and whoAmI.

Object.getPrototypeOf returns the object prototype of an object.

Object.getOwnPropertyNames returns an array with the names of all properties defined by the object given as an argument.

When we call a constructor function with the new keyword, JavaScript takes the function prototype of the constructor (Cat.prototype here) and assigns it as the object prototype of the object created by the constructor. Thus, an object prototype is, by default, identical to the function prototype of the object's constructor function.

Interestingly, function prototypes also have object prototypes. By default, the object prototype of a function prototype is the function prototype of the superclass.

class Animal {
  constructor(type) {
    this.type = type;
  }

  eat() {
    console.log("I am eating.");
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super();
    this.name = name;
    this.color = color;
  }

  whoAmI() {
    console.log(`My name is ${this.name}.`,
                `\nI am a ${this.color} cat.`);
  }
}

let cheddar = new Cat('Cheddar', 'ginger');
let cheddarProto = Object.getPrototypeOf(cheddar);
let cheddarProto2 = Object.getPrototypeOf(cheddarProto);

console.log(Object.getOwnPropertyNames(cheddarProto));
// ['constructor', 'whoAmI'];

console.log(Object.getOwnPropertyNames(cheddarProto2));
// ['constructor', 'eat'];

console.log(cheddarProto2 === Animal.prototype); // true

cheddar.whoAmI();  // My name is Cheddar.
                   // I am a ginger cat.
cheddar.eat();     // I am eating.

With almost all objects, you can follow this "chain" of object prototypes all the way back to a prototype object called Object.prototype. This chain is called the prototype chain; we'll learn more about it soon. Since Object.prototype's object prototype is null, every JavaScript prototype chain ultimately ends with null.

The following diagram depicts the structure of the above code. In particular, it shows that cheddar's object prototype is the same as the Cat constructor's function prototype. Likewise, the object prototype of Cat.prototype is the same object as Animal.prototype, and the object prototype of Animal.prototype is the object given by Object.prototype. Finally, the end of the chain is null.

                            +----------------+
                            |     cheddar    |
                            |--------*-------+
                                     | getPrototypeOf
+---------*--------+                 |
|        Cat       |                 |
+------------------+        +--------*-------+
|   Cat.prototype  | -----> |   constructor  |
+------------------+        |   whoAmI       |
                            +--------*-------+
                                     | getPrototypeOf
+------------------+                 |
|      Animal      |                 |
+------------------+        +--------*-------+
| Animal.prototype | -----> |   constructor  |
+------------------+        |   eat          |
                            +--------*-------+
                                     | getPrototypeOf
+------------------+                 |
|      Object      |                 |
+------------------+        +--------*-------+
| Object.prototype | -----> | hasOwnProperty |
+--------*---------+        | isPrototypeOf  |
                            |     etc...     |
                            +--------*-------+
                                     | getPrototypeOf
                                     |
                                  +--*---+
                                  | null |
                                  +------+

When Objects Inherit Directly From Object

All objects created from an object literal are instances of the Object class.Hence, their object prototype is the function prototype of Object, e.g., Object.prototype:

let obj = { // Object literal
  foo: 42,
};

let objProto = Object.getPrototypeOf(obj);
console.log(objProto === Object.prototype); // true

Since object factories use object literals, they also create objects that have Object.prototype as their object prototype:

function createObj() {
  return {
    foo: 42,
  };
}

let obj = createObj();
let objProto = Object.getPrototypeOf(obj);
console.log(objProto === Object.prototype); // true

It's also worth noting that objects created by new Object() also have Object.prototype as their prototype:

let obj = new Object();
let objProto = Object.getPrototypeOf(obj);
console.log(objProto === Object.prototype); // true

Accessing Object Prototypes

In JavaScript, every object has a hidden property called [[Prototype]] that points to its object prototype or null.

While you can't refer to [[Prototype]] directly, you can access the object prototype with the Object.getPrototypeOf method, as shown several times above. For example:

class Animal {}
class Cat extends Animal {}

let cat = new Cat();
console.log(Object.getPrototypeOf(cat));   // Animal {}

Reassigning Object Prototypes

In addition to accessing the object prototype, you can give an object an entirely new prototype with the Object.setPrototypeOf method.

class Animal {}
class Cat extends Animal {}

let cat = new Cat();
let catProto = Object.getPrototypeOf(cat);
console.log(catProto === Cat.prototype);   // true

//highlight
let myProto = {
  meow() {
    console.log("Meow!");
  }
};

Object.setPrototypeOf(cat, myProto);
catProto = Object.getPrototypeOf(cat);

cat.meow();                                // Meow!
console.log(catProto === Cat.prototype);   // false
console.log(catProto === myProto);         // true
//endhighlight

Replacing an object's prototype with a new prototype may result in code that runs more slowly than it would otherwise. Changing object prototypes may also result in code that is difficult to understand or maintain.

Object Prototypes for Functions and Classes

Functions and classes are also objects. That means they have object prototypes in addition to the function prototypes we met earlier.

As we've already discussed, function prototypes are used when creating new objects from the function or class; the function prototype gets assigned as the object prototype for the newly created object. Object prototypes are used by objects to define where JavaScript should look for instance methods.

With function and class objects, the object prototypes define where JavaScript should look for static methods. For instance, consider the following code:

class Foo {
  static identity() {
    console.log('This is Foo.identity()');
  }
}

class Bar {
  static whoAmI() {
    console.log('This is Bar.whoAmI()');
  }
}

// Foo and Bar initially have Function.prototype as their
// object prototypes.

// The names `FooProto` and `BarProto` are not consistent
// with JS conventions. We've done this intentionally for
// clarity.

let FooProto = Object.getPrototypeOf(Foo);
console.log(FooProto === Function.prototype); // true

let BarProto = Object.getPrototypeOf(Bar);
console.log(BarProto === Function.prototype); // true

// Let's change Bar's object prototype to Foo, which lets
// Bar inherit Foo's static methods. However, Bar does
// *not* inherit Foo's instance methods.
Object.setPrototypeOf(Bar, Foo);

// Bar can access both Bar.whoAmI and Foo.identity
Bar.whoAmI();       // This is Bar.whoAmI()
Bar.identity();     // This is Foo.identity()

The object prototypes for functions and classes ultimately chain to Function.prototype, which has some useful static methods such as apply, call, and bind. We discuss these in the Core curriculum.

Arrow Functions and Function Prototypes

Arrow functions do not have function prototypes. However, they do have object prototypes.

let foo = () => console.log('foo');
let fooProto = Object.getPrototypeOf(foo);

console.log(foo.prototype);                   // undefined
console.log(fooProto);                        // {}
console.log(fooProto === Function.prototype); // true

As we've seen, Object.getPrototypeOf returns the object prototype of an object. In this case, the object is a function, and the function's object prototype is Function.prototype. That happens to be a function prototype, but it is not the function prototype for foo. The function prototype, which would be referenced by foo.prototype if it existed, is undefined since foo doesn't have a prototype property.

The __proto__ Property

You will probably encounter code that uses the __proto__ property to access and reassign the object prototype:

class Animal {}
class Cat extends Animal {}

let cat = new Cat();
console.log(cat.__proto__);   // Animal {}

class Foo {}
cat.__proto__ = Foo;
console.log(cat.__proto__);   // [class Foo]

While this code works, __proto__ is officially deprecated. You should not use it in production code. While it's reasonably safe to use when testing or debugging, you should use Object.getPrototypeOf and Object.setPrototypeOf instead.

Accessing Function Prototypes

A function prototype can be accessed or changed by using the prototype property on the function or class:

class Animal {}

Animal.prototype.foo = function() {
  console.log('this is foo');
};

Animal.prototype.bar = function() {
  console.log('this is bar');
};

console.log(Animal.prototype);
// { foo: [Function (anonymous)],
//   bar: [Function (anonymous)] }

function Vehicle() {}

Vehicle.prototype.drive = function() {
  console.log('off we go');
};

console.log(Vehicle.prototype);
// { drive: [Function (anonymous)] }

The Prototype Chain

When JavaScript looks for a method associated with an instance object, JavaScript first looks for it in the object itself. If it doesn't find the method, it looks at the object prototype next. Thus, we can call the foo, bar, and drive methods above as follows:

let animal = new Animal();
let vehicle = new Vehicle();

animal.foo();     // this is foo
animal.bar();     // this is bar
vehicle.drive();  // off we go

As we saw earlier, when you attempt to access a method, JavaScript first looks at the calling object. If it doesn't find the method, it searches the object prototype of the calling object. If JavaScript finds the method in either location, JavaScript will use that method.

However, JavaScript won't always find the method in one of these locations. When it doesn't find it, JavaScript searches the remainder of the object's prototype chain. It stops searching when it sees the method or reaches the end of the chain (null).

The prototype chain begins by looking for the requested method in the calling object. If JavaScript finds the method there, it stops searching. If it doesn't find the method, JavaScript next looks in the object prototype for the calling object. If JavaScript finds the method in the prototype, it stops searching.

If the method still isn't found, JavaScript searches the object prototype of the inherited object, if any. JavaScript will continue to search inherited object prototypes until it finds the method or an object prototype of null.

For example, consider the following classes:

class Mammal {
  foo() { console.log("Mammal.foo"); }
}

class Whale extends Mammal {
  foo() { console.log("Whale.foo"); }
}

class Beluga extends Whale {}

let beluga = new Beluga();
beluga.foo(); // Whale.foo
beluga.bar(); // TypeError: beluga.bar is not a function

In this code, the lookup process for foo works like so:

  • JavaScript first looks in the beluga object for foo. It does not find it.
  • JavaScript next searches the object prototype of the beluga object (Beluga.prototype). JavaScript does not find foo in Beluga.prototype, so the search continues.
  • Finally, JavaScript searches the function prototype of the Whale class (Whale.prototype). Since JavaScript finds foo in Whale.prototype, it calls that method and prints Whale.foo. Mammal.foo never gets called.

The lookup process for bar works like this:

  • JavaScript first looks in the beluga object for bar but does not find it.
  • JavaScript next searches the object prototype of the beluga object (Beluga.prototype). Again, it doesn't find bar.
  • JavaScript then searches the function prototype of the Whale class (Whale.prototype). Again, it doesn't find bar.
  • Next, JavaScript searches the function prototype for the inherited class, Mammal. It finds Mammal.prototype but can't find bar.
  • When JavaScript tries to find the next function prototype, it finds Object.prototype. Since Object.prototype is null, it does not have a bar function. JavaScript then raises an error.

Prototypal Inheritance vs Classical Inheritance

In traditional OO languages, classes are blueprints for objects. Classes can inherit from other classes, which enables code reuse without copying methods all over the place. A class defines the structure and behaviors (methods) objects instantiated from it should have. A subclass inherits from a superclass and can add or override methods. The superclass's methods are available to instances of the subclass unless overridden.

JavaScript has a different way of doing things. In particular, objects inherit directly from other objects. There are no actual classes in pure prototypal inheritance. In fact, JavaScript doesn't have proper classes; the class keyword is syntactic sugar that maps to a constructor function and a function prototype.

Prototypal inheritance, as implemented in JavaScript, allows any object to inherit from any other. It is dynamic, meaning objects can extend or change their prototype at runtime. Since the prototypes are all linked via pointers, prototypal inheritance can also use memory more efficiently.

In classical OO languages, inheritance is more structured and strict. It is achieved through a class hierarchy; subclasses inherit from superclasses. Class-based inheritance is static; relationships between classes and instances are established at compile time and cannot usually be altered dynamically. The lack of flexibility, however, makes classical OO more intuitive to most developers. There's no need to understand prototypes.

Constructor/Prototype Pattern

It's time to see what prototypal inheritance looks like in all its convoluted glory. Consider the following class-based code:

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

  eat() {
    console.log(`${this.name} is eating.`);
  }
}

class Mammal extends Animal {
  constructor(name, hasFur) {
    super(name);
    this.hasFur = hasFur;
  }

  sleep() {
    console.log(`${this.name} is sleeping.`);
  }
}

class Dog extends Mammal {
  constructor(name, hasFur, breed) {
    super(name, hasFur);
    this.breed = breed;
  }

  bark() {
    console.log(`${this.name} the ${this.breed} ` +
                'is barking.');
  }
}

let myDog = new Dog('Rex', true, 'German Shepherd');
myDog.eat();    // Rex is eating.
myDog.sleep();  // Rex is sleeping.
myDog.bark();   // Rex the German Shepherd is barking.

Let's rewrite this code using the constructor/prototype pattern. To begin, we'll need constructor functions to replace each of the three classes (for now, we'll ignore the inheritance aspect of this code for now). We'll also need to add each method (other than constructor) to the function prototype for each type.

//highlight
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

function Mammal(name, hasFur) { // extends Animal
  // super(name) - we meed to simulate super()
  this.hasFur = hasFur;
}

Mammal.prototype.sleep = function() {
  console.log(`${this.name} is sleeping.`);
};

function Dog(name, hasFur, breed) { // extends Mammal
  // super(name, hasFur) - we need to simulate super()
  this.breed = breed;
}

Dog.prototype.bark = function() {
  console.log(`${this.name} the ${this.breed} is barking.`);
}
//endhighlight

let myDog = new Dog('Rex', true, 'German Shepherd');
myDog.eat();    // Rex is eating.
myDog.sleep();  // Rex is sleeping.
myDog.bark();   // Rex the German Shepherd is barking.

This code has two problems right now. First, we need to tell JavaScript that Dog inherits the prototype of Mammal, and Mammal inherits the prototype of Animal. Second, we must tell the constructor functions how to call the inherited constructor function. Neither problem has an intuitive solution. There is no extends keyword we can use, nor is there a super() function we can call.

The solution for not having an extends keyword is to use Object.create. It creates a new object that uses another object as its object prototype. For instance:

let obj = Object.create({
  foo() {},
  bar() {},
});

console.log(Object.getPrototypeOf(obj));
// { foo: [Function: foo], bar: [Function: bar] }

In the above code, we create a new object obj whose object prototype is given by the object passed as the argument for Object.create. Thus, when we log the object prototype of obj, we can see both the foo and bar methods.

This pattern is most useful with constructor functions. For instance, to have the Mammal constructor inherit the function prototype of Animal, we just use Object.create to create a function prototype for Mammal:

// Code omitted for brevity

function Mammal(name, hasFur) { // extends Animal
  // super(name) - we need to simulate super()
  this.hasFur = hasFur;
}

//highlight
Mammal.prototype = Object.create(Animal.prototype);

let myMammal = new Mammal('Flipper', false);
console.log(myMammal instanceof Mammal);  // true
console.log(myMammal instanceof Animal);  // true
//endhighlight

// Code omitted for brevity

Using this pattern, we can now have the Dog type inherit from the Mammal type:

// Code omitted for brevity

function Dog(name, hasFur, breed) { // extends Mammal
  // super(name, hasFur) - we need to simulate super()
  this.breed = breed;
}

Dog.prototype = Object.create(Mammal.prototype);

let myDog = new Dog('Rex', true, 'German Shepherd');
//highlight
console.log(myDog instanceof Dog);     // true
console.log(myDog instanceof Mammal);  // true
console.log(myDog instanceof Animal);  // true
//endhighlight

// Code omitted for brevity

At this point, our inheritance hierarchy is almost working, but not quite:

// Code omitted for brevity

let myDog = new Dog('Rex', true, 'German Shepherd');
console.log(myDog instanceof Dog);     // true
console.log(myDog instanceof Mammal);  // true
console.log(myDog instanceof Animal);  // true

myDog.eat();    // undefined is eating.
myDog.sleep();  // undefined is sleeping.
myDog.bark();
// undefined the German Shepherd is barking.

Since we haven't used super (and can't in this code), the methods in the myDog object are unable to determine the dog's name. To fix that, we must explicitly call the Animal and Mammal constructors using Function.prototype.call with this as the first argument.

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

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

function Mammal(name, hasFur) { // extends Animal
  //highlight
  Animal.call(this, name);
  //endhighlight
  this.hasFur = hasFur;
}

Mammal.prototype = Object.create(Animal.prototype);

Mammal.prototype.sleep = function() {
  console.log(`${this.name} is sleeping.`);
};

function Dog(name, hasFur, breed) { // extends Mammal
  //highlight
  Mammal.call(this, name, hasFur);
  //endhighlight
  this.breed = breed;
}

Dog.prototype = Object.create(Mammal.prototype);

Dog.prototype.bark = function() {
  console.log(`${this.name} the ${this.breed} ` +
              'is barking.');
}

let myDog = new Dog('Rex', true, 'German Shepherd');
myDog.eat();    // Rex is eating.
myDog.sleep();  // Rex is sleeping.
myDog.bark();   // Rex the German Shepherd is barking.

One item of note here is that the first argument to the call method should almost always be this. If you're creating a Dog instance, then the this argument will reference the Dog instance. Thus, all of the constructors will see the Dog instance as the calling object.

Everything is working now. However, there's one more step. By default, a constructor function's prototype.constructor property is a reference back to the constructor function. When you reassign the prototype property, the constructor subproperty's value no longer references the correct constructor. Some JavaScript libraries may break if prototype.constructor is not set correctly. To ensure it doesn't become a problem, you should assign the prototype.constructor property for each subtype to refer to the subtype's constructor function:

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

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating.`);
};

function Mammal(name, hasFur) { // extends Animal
  Animal.call(this, name);      // emulating `super`
  this.hasFur = hasFur;
}

Mammal.prototype = Object.create(Animal.prototype);
//highlight
Mammal.prototype.constructor = Mammal;
//endhighlight

Mammal.prototype.sleep = function() {
  console.log(`${this.name} is sleeping.`);
};

function Dog(name, hasFur, breed) { // extends Mammal
  Mammal.call(this, name, hasFur); // emulating super
  this.breed = breed;
}

Dog.prototype = Object.create(Mammal.prototype);
//highlight
Dog.prototype.constructor = Dog;
//endhighlight

Dog.prototype.bark = function() {
  console.log(`${this.name} the ${this.breed} ` +
              'is barking.');
}

let myDog = new Dog('Rex', true, 'German Shepherd');
myDog.eat();    // Rex is eating.
myDog.sleep();  // Rex is sleeping.
myDog.bark();   // Rex the German Shepherd is barking.

In most cases, it doesn't matter whether you reset the prototype.constructor property. However, you should do so, just in case.

And there it is in all its convoluted glory: the constructor/prototype pattern. Before ES6, JavaScript developers had to write OO code using this pattern. The complexity of inheritance with this pattern is a good reason to prefer the class syntax.

To recap, there are three significant steps here to have a subtype inherit from a supertype when using the constructor/prototype pattern:

  1. Replace the function prototype for each subtype's constructor. To do that, use Object.create with an argument referencing the supertype's function prototype, and assign it to the subtype constructor's prototype property.
  2. Set the subtype's prototype.constructor property to the subtype's constructor function.
  3. In the subtype's constructor, call the supertype's constructor using Function.prototype.call. You should pass this as the first argument to call followed by the arguments for the supertype's constructor.

When Should You Use the Constructor/Prototype Pattern?

  • When you need to support pre-ES6 versions of JavaScript and can't use a transpiler (not discussed in this book).
  • When your application already uses the constructor/prototype pattern.
  • When your team prefers to use the constructor/prototype pattern.
  • When you need many objects with methods.
  • When the types of your objects are essential to your application.

Summary

This chapter delves into prototypal inheritance, a key JavaScript feature that allows objects to inherit methods and other properties from prototype objects. Unlike classical inheritance, where classes inherit from other classes, JavaScript objects inherit directly from other objects through prototype chains. If JavaScript fails to find a method on an object, it searches the prototype chain until it finds the method or reaches a null prototype. The chapter explains this using examples and highlights differences with classical inheritance. It also introduces the constructor/prototype pattern, showing how classes simplify the traditional prototypal setup by acting as syntactic sugar, making code easier to write and understand.

Exercises

  1. This exercise re-examines exercise 1 from the previous chapter. In that exercise, you wrote a class to instantiate smartphone objects. In this exercise, we'll rewrite that solution using the constructor/prototype pattern.

    Using the constructor/prototype pattern, create a type that represents smartphones. Each smartphone should have a brand, model, and release year. Add methods that display the smartphone's information and check its battery level. Create objects that represent the following two smartphones:

    Brand Model Release Year
    Apple iPhone 12 2020
    Samsung Galaxy S21 2021

    Solution

    function Smartphone(brand, model, releaseYear) {
      this.brand = brand;
      this.model = model;
      this.releaseYear = releaseYear;
    }
    
    Smartphone.prototype.checkBatteryLevel = function() {
      return `${this.brand} ${this.model} has 75% ` +
             'battery remaining.';
    };
    
    Smartphone.prototype.displayInfo = function() {
      return `${this.releaseYear} ${this.brand} ` +
             `${this.model}`;
    };
    
    let iphone12 = new Smartphone('Apple',
                                  'iPhone 12',
                                  2020);
    let galaxyS21 = new Smartphone('Samsung',
                                   'Galaxy S21',
                                   2021);
    
    console.log(iphone12.checkBatteryLevel());
    // Apple iPhone 12 has 75% battery remaining.
    
    console.log(iphone12.displayInfo());
    // 2020 Apple iPhone 12
    
    console.log(galaxyS21.checkBatteryLevel());
    // Samsung Galaxy S21 has 75% battery remaining.
    
    console.log(galaxyS21.displayInfo());
    // 2021 Samsung Galaxy S21
    
  2. This exercise re-examines exercise 3 from the previous chapter. In that exercise, you wrote a class hierarchy to represent vehicles of various types. In this exercise, we'll rewrite that solution using the constructor/prototype pattern.

    Using the constructor/prototype pattern, create some types that represent vehicles, including cars, boats, and planes as specific kinds of vehicles. All vehicles should be able to accelerate and decelerate. Cars should be able to honk, boats should be able to drop anchor, and planes should be able to take off and land. Test your code.

    Solution

    function Vehicle(color, weight) {
      this.color = color;
      this.weight = weight;
    }
    
    Vehicle.prototype.accelerate = function() {
      console.log("Accelerate");
    };
    
    Vehicle.prototype.decelerate = function() {
      console.log("Decelerate");
    };
    
    function Car(color, weight, licenseNumber) {
      Vehicle.call(this, color, weight);
      this.licenseNumber = licenseNumber;
    }
    
    Car.prototype = Object.create(Vehicle.prototype);
    Car.prototype.constructor = Car;
    Car.prototype.honk = function() {
      console.log("Honk");
    };
    
    function Boat(color, weight, homePort) {
      Vehicle.call(this, color, weight);
      this.homePort = homePort;
    }
    
    Boat.prototype = Object.create(Vehicle.prototype);
    Boat.prototype.constructor = Boat;
    Boat.prototype.dropAnchor = function() {
      console.log("Drop anchor");
    };
    
    function Plane(color, weight, airline) {
      Vehicle.call(this, color, weight);
      this.airline = airline;
    }
    
    Plane.prototype = Object.create(Vehicle.prototype);
    Plane.prototype.constructor = Plane;
    Plane.prototype.takeOff = function() {
      console.log("Take off");
    };
    
    Plane.prototype.land = function() {
      console.log("Land");
    };
    
    let car = new Car('red', 3300, 'BXY334');
    car.accelerate();             // Accelerate
    car.honk();                   // Honk
    car.decelerate();             // Decelerate
    console.log(car.color, car.weight, car.licenseNumber);
    // red 3300 BXY334
    
    let boat = new Boat('yellow', 12000, 'Bahamas');
    boat.accelerate();            // Accelerate
    boat.decelerate();            // Decelerate
    boat.dropAnchor();            // Drop anchor
    console.log(boat.color, boat.weight, boat.homePort);
    // yellow 12000 Bahamas
    
    let plane = new Plane('blue', 83000, 'Southwest');
    plane.accelerate();           // Accelerate
    plane.takeOff();              // Take off
    plane.land();                 // Land
    plane.decelerate();           // Decelerate
    console.log(plane.color, plane.weight, plane.airline);
    // blue 83000 Southwest