Prototypal Inheritance with Constructor Functions

Mom. Software Engineer.
20 min readJun 23, 2023

--

This article is the third in a comprehensive five part series on Prototypal Inheritance.

Part 1 — Understanding the Prototype Chain
Part 2 — Prototypal Inheritance with Object.create
Part 3 — Prototypal Inheritance with Constructor Functions
Part 4 — Coming soon!
Part 5 — Coming soon!

In part 2 of this series, we saw that while we could use object literals to construct prototype chains, it becomes impractical to do so when we need to create multiple objects with the same shape.

In this post, I’ll demonstrate how to build robust prototype chains using constructor functions. I highly recommend attempting the exercises at the very end of this post before moving on to part 4.

If you’re not familiar with what the prototype chain is, how JavaScript traverses the prototype chain, or how Object.create works, I highly recommend going through part 1 and part 2 before delving into constructor functions.

As always, I would love your questions, feedback and constructive criticism!

Part 3 — Prototypal Inheritance with Constructor Functions

A constructor function looks like the following:

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

As you can see, the above is just a plain old JavaScript function. Technically speaking, Animal is no more a “constructor” than any other function. In JavaScript, any function can be used as a constructor function; all we have to do is apply the new keyword before invoking it, as in const someAnimal = new animal(‘BlippityDop’).

Regardless, notice how Animal is capitalized? By convention, when we intend for a function to be a constructor function, we capitalize it. In fact, some linters complain if we call new on a function with a lowercase name, or if we don’t call new on a function that starts with a capital letter.

Now, when we declare any function F, JavaScript automatically creates a new object F.prototype. Thus, when we declared Animal above, the JavaScript runtime did the following:

  1. Created a function, Animal, and set its [[prototype]] reference to Function.prototype (Recall our previous discussion: the prototype of functions is Function.prototype).
  2. It created a new object
  3. It added a property, prototype, to the Animal function and pointed to this new object
  4. Set the prototype of the new object to Object.prototype

You can visualize the above as follows:

It is really important to note that [[prototype]] ≠ .prototype.

function Animal(name) {
this.name = name;
}
console.log(Object.getPrototypeOf(Animal) === Animal.prototype); // false

A deeper understanding of constructor functions will help you grasp the difference between [[prototype]] and .prototype . Thus, we will continue our discussion on constructors, then circle back to [[prototype]] and .prototype later. For now, just understand that they are not the same.

I recommend completing exercise 1 before continuing on.

The New Keyword

Previously, we mentioned that when the new keyword is used before invoking a function, it transforms the function call into a constructor call. So what does new do exactly?

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

const bloppy = new Animal('bloppy');

When we called new on the Animal function, the JavaScript runtime did the following:

  1. Created a blank object, bloppy
  2. Pointed bloppy’s [[prototype]] to Animal’s prototype property
  3. Executed the constructor function with the given arguments, binding bloppy as the this context (i.e, all references to this in the constructor function Animal now refer to the bloppy object)

You can visualize the above as follows:

Again, it is really important to notice that the prototype of bloppy is Animal.prototype, NOT Animal. I suggest executing the code below and making sure you understand why each statement evaluates to its respective outcome.

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

const bloppy = new Animal('bloppy');

console.log(bloppy); // Animal { name: 'bloppy'}
console.log(Animal.isPrototypeOf(bloppy)); // false
console.log(Animal.prototype === Object.getPrototypeOf(bloppy)); // true
console.log(Function.prototype.isPrototypeOf(bloppy)); // false
console.log(Function.prototype.isPrototypeOf(Animal)); // true
console.log(Object.prototype.isPrototypeOf(bloppy)); // true

What happens if we call a constructor function without the new keyword?

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

const bloppy = Animal('bloppy'); {2}
console.log(bloppy.name);
// throws TypeError: cannot read properties of undefined (reading 'name')

We get a TypeError. Essentially, the when the code above is executed, the following occurs:

  1. The Animal function is executed in the current scope, not as a constructor (since we didn’t use the new keyword).
  2. The this inside Animal does not refer to a newly created object but instead depends on the context in which the function is called (try console.log(this) in the Animal function, what do you get?).
  3. In JavaScript, functions that do not explicitly return a value will returned undefined by default. Thus, in {2}, we assign undefined to bloppy.
  4. When attempting to access bloppy.name in the console.log statement, a TypeError is thrown because bloppy is undefined and does not have a type property.

In summary, when a function F is invoked with the new keyword, it operates as a constructor, and the following steps are carried out:

  1. A new empty object is created. Let’s refer to this object as newInstance
  2. newInstance’s [[prototype]] internal property is set to the ‘prototype’ property of the function being called (I.e, Object.getPrototypeOf(newInstance) === F.prototype evaluates to true). This establishes the prototype chain, allowing newInstance to inherit properties and methods from F.prototype.
  3. The function is executed, with the this keyword referencing the newly created object, newInstance. This enables the function to modify or add properties and methods to the object.
  4. If the function does not explicitly return an object, the new expression automatically returns the newly created object. If the function does return an object, that object is returned instead.

The new keyword essentially facilitates the creation of objects of a particular type defined by the constructor function. It helps to distinguish constructor calls from regular function calls and provides a way to initialize and configure objects based on the constructor’s logic.

Before moving on, I highly recommend completing Exercises 2–5. Having a solid grasp of the concepts discussed above is crucial to understanding the content that follows.

Adding Methods to the Prototype Object

Let’s say we wanted every object created by calling new Animal(…) to have a walk method. Technically, we could do the following:

function Animal(name) {
this.name = name;
this.walk = function walk() { console.log(`${this.name}: I am walking`) }
}

const bloppy = new Animal('bloppy');
bloppy.walk(); // bloppy: I am walking
const floppy = new Animal('floppy');
floppy.walk(); // floppy: I am walking

We can visualize the snippet above as follows:

This seems to work just fine. However, notice we defined walk directly on Animal’s context (this variable). We are basically adding a new walk property to each object created via the new Animal(…) call. The statement bloppy.walk === floppy.walk evaluates to false because each individual object’s walk method is in a different location in memory.

Instead of defining walk in the Animal function, you should add it to Animal.prototype.

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

Animal.prototype.walk = function() {
console.log(`${this.name}: I am walking`);
};

const bloppy = new Animal('bloppy');
bloppy.walk(); // bloppy: I am walking

const floppy = new Animal('floppy');
floppy.walk(); // floppy: I am walking

console.log(bloppy.walk === floppy.walk); // true

Now, we have something like this:

This approach offers two key advantages:

  1. Memory Efficiency: When you define a method directly on the function itself (e.g., this.walk = function() { ... }), each object created using the constructor function will have its own copy of that method. This can consume unnecessary memory, especially if you have many instances. On the other hand, by adding the method to Animal.prototype, the method is shared among all instances, resulting in memory efficiency.
  2. Dynamic Method Addition: By adding methods to Animal.prototype, you can dynamically extend the functionality of existing objects. Since all instances inherit from the prototype, any new methods added to the prototype will be accessible by those instances.

Let’s elaborate on the second point by going through an example.

Example 1:

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

Animal.prototype.walk = function() {
console.log(`${this.name}: I am walking`);
};

const cat = new Animal('Whiskers');
cat.walk(); // Whiskers: I am walking

// Now let's dynamically add a new method to the Animal prototype
Animal.prototype.sleep = function() {
console.log(`${this.name}: Zzzz...`);
};

cat.sleep(); // Whiskers: Zzzz...

const dog = new Animal('Buddy');
dog.walk(); // Buddy: I am walking
dog.sleep(); // Buddy: Zzzz...

Above, we define a Animal constructor function with a walk method added to Animal.prototype. This method is shared among all instances of Animal.

Later in the code, we dynamically add a new method, sleep, to Animal.prototype using Animal.prototype.sleep = function() { ... }. This means that any existing instances, like cat, as well as future instances created by the Animal constructor, will now have access to the sleep method.

By adding the sleep method dynamically to the prototype, we extend the functionality of existing objects without modifying their individual definitions. This allows us to introduce new behaviors to objects on the fly. In the example, both the cat and dog instances can call the sleep method even though it was added after their creation.

In summary, when you want a method to be shared among all objects created by a specific constructor call, add them to the constructor function’s prototype.

Creating a Prototype Chain

So far, we’ve kept our running example rather simplistic. We created an Animal function, and used it to instantiate individual animals. Let’s try to add more complexity by revisiting Example 2 in Part 2 of this series. In that example, we used Object.create to build the following prototype chain:
null ← Object.prototype ← animal ← bear ← baloo

Here is the code we used:

const animal = {
walk: function walk() { console.log(`${this.name}: I am walking`) }
};
const bear = Object.create(animal, {
growl: {
value: function growl() { console.log(`${this.name}: rrrrrrrr`) }
}
});
const baloo = Object.create(bear, {
name: {
value: 'Baloo'
}
});
baloo.growl(); // Baloo: rrrrrrrr
baloo.walk(); // Baloo: I am walking

Now, let’s attempt to rewrite the above using constructor functions:

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

Animal.prototype.walk = function walk() {
console.log(`${this.name}: I am walking`);
}

function Bear(name) {
Animal.call(this, name);
}

Bear.prototype = Object.create(Animal.prototype)

Bear.prototype.growl = function growl() {
console.log(`${this.name}: rrrrrr`);
}

const baloo = new Bear('Baloo');
baloo.walk(); // Baloo: I am walking
baloo.growl(); // Baloo: rrrrrr

console.log(Object.getPrototypeOf(baloo) === Bear.prototype); //true
console.log(Object.getPrototypeOf(Bear.prototype) === Animal.prototype); //true
console.log(Object.getPrototypeOf(Bear) === Function.prototype); //true
console.log(Object.getPrototypeOf(Animal) === Function.prototype); //true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); //true

The code above creates the following relationships:

Or more simply, it creates the following prototype chain:

null ← Object.prototype ← Function.prototype ← Animal.prototype ← Bear.prototype ← baloo

Let’s examine the code more closely.

  1. We create an Animal function and add the walk method to its prototype
  2. We create a Bear function.
    - The Bear function includes the code Animal.call(this, name) to set the this object of the Animal function. By passing this to Animal.call, the newly created object (eventually assigned to baloo) is also referenced as this inside the Bear constructor. The additional arguments passed to call become the function arguments, so the name argument “Baloo” is passed to Animal. As a result, baloo.name is set to “Baloo”. Without this line, executing baloo.walk() or baloo.growl() would result in undefined: I am walking or undefined: rrrrrr, respectively.
  3. The statement Bear.prototype = Object.create(Animal.prototype) sets the prototype of Bear.prototype to be Animal.prototype. Thus, all properties and methods defined on Animal.prototype are accessible to objects further down the prototype chain, including instances of the Bear constructor (like baloo).
  4. We add the growl method to Bear.prototype.
  5. We use the Bear constructor to create an object, baloo.
  6. We call baloo.walk. When we do this, the JavaScript runtime checks if baloo has the walk property. It does not. Therefore, it moves up the prototype chain and checks if Bear.prototype has the walk property. It does not. Thus it moves up the prototype chain one more time and checks if Animal.prototype has the walk property. The runtime executes the walk function, setting this to baloo.
  7. We call baloo.growl. As above, the Javascript runtime travels up the prototype chain until it finds the growl method.

The Constructor Property

Our discussion of constructor functions would not be complete without taking a look at the constructor property.

Earlier, I mentioned that when we declare a function F, JavaScript automatically gives it a prototype property that points to an empty object. The prototype object by default (at declaration time) gets a public non-enumerable property called .constructor, and this property is a reference back to the function that the object is associated with.

function Animal(name) {
this.name = name;
}
console.log(Animal.prototype.constructor === Animal); // true

Now, let’s circle back to our Animal->Bear->baloo code block from the previous section. Can you spot the issue?

Take a look at this statement
Bear.prototype = Object.create(Animal.prototype)

Think about the constructor property. Do you see the problem?

When we declared the function Bear, JavaScript automatically created an empty object and assigned it to Bear.prototype. That object consisted of a constructor property that pointed back to the Bear function. However, when we executed Bear.prototype = Object.create(Animal.prototype), we replaced the original object with a new one. This new object does not have a constructor property. You can see this clearly here:

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

function Bear(name) {
Animal.call(this, name);
}

console.log(Bear.prototype.constructor); // [Function: Bear]
console.log(Bear.prototype.constructor === Bear); // true
console.log(Bear.prototype.hasOwnProperty('constructor')); // true

const arnold = new Bear('arnold');
console.log(arnold); // Bear { name: 'arnold' }

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

console.log(Bear.prototype.hasOwnProperty('constructor'));
// false - Bear.prototype no longer has a constructor property
console.log(Bear.prototype.constructor); // [Function: Animal]

const baloo = new Bear('Baloo');
console.log(baloo);
// Animal { name: 'Baloo' } -- wait a minute, isn't baloo a bear?

After executing Bear.prototype = Object.create(Animal.prototype), we see that Bear no longer has a constructor property. But then, why is it that when we call console.log(Bear.prototype.constructor), we get a result as opposed to undefined? Remember: The prototype chain. The prototype of Bear.prototype is Animal.prototype, and Animal.prototype has a constructor property that points at the Animal function. Thus, when we call Bear.prototype.constructor, the JavaScript engine first checks if Bear.prototype has the constructor property. It does not. Thus, the engine checks its prototype, Animal.prototype, which does have a constructor property.

So how do we fix this problem? We can replace Object.create with Object.setPrototypeOf. Our new code looks as follows:

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

Animal.prototype.walk = function walk() {
console.log(`${this.name}: I am walking`);
}

function Bear(name) {
Animal.call(this, name);
}

Bear.prototype.growl = function growl() {
console.log(`${this.name}: rrrrrr`);
}

Object.setPrototypeOf(Bear.prototype, Animal.prototype);

const baloo = new Bear('Baloo');
console.log(baloo); // Bear { name: 'Baloo' } -- He is a Bear! :)

You might be wondering, what are the consequences of losing the constructor reference in Bear? Does it really matter? Usually no — the language almost never reads the constructor property of an object. However, let us assume you have some code where the caller is using the constructor property to access the original class from an instance:

function Animal() {}

function Bear() {}

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

function createObjectOfSameType(someObject) {
return new someObject.constructor();
}

const anAnimal = new Animal();
console.log(createObjectOfSameType(anAnimal)); // Animal {}

const aBear = new Bear();
console.log(createObjectOfSameType(aBear)); // Animal {}

As you can see, we were attempting to create a Bear, but we created a generic Animal.

Circling Back: [[prototype]] vs .prototype

Earlier in this article, we mentioned that [[prototype]] ≠ .prototype. By now, you should understand the difference. However, it is easy to confuse the two so I thought I’d go over it one more time.

[[prototype]]:

  • It is an internal property of an object that defines its prototype
  • It represents the link to the prototype object from which the current object inherits properties and methods

.prototype:

  • It is a property that exists only on functions
  • It defines the object that will be assigned as the [[prototype]] of all objects created using that function as a constructor

In summary, [[prototype]] is the actual link to the prototype object of an instance, allowing inheritance of properties and methods. On the other hand, .prototype is a property on constructor functions that defines the prototype object to be assigned to objects created by that constructor.

Conclusion

In this article, we we explored how to use constructor functions to create multiple objects with the same shape. We saw that:

  1. By using the new keyword before invoking a function, we can transform it into a constructor call. This process involves creating a new object, setting its [[prototype]] to the constructor’s prototype property, and executing the function with the new object as the context.
  2. To add shared methods to objects created by a constructor, we define them on the constructor’s prototype.

You probably noticed, however, that using constructor functions to create prototype chains can be cumbersome. It requires dealing with intricate details of prototype configuration, which increases the chance of introducing bugs and can lead to unusual behaviour if not implemented perfectly (see Exercises 6–8).

And that is why we’ll delve into JavaScript classes in Part 4 — Prototypal Inheritance with Classes (coming soon).

Exercises

The following exercises are designed to reinforce your understanding of how to use constructor functions to build a prototype chain.

Exercise 1

When you execute the code below,

  1. What is the prototype of Shape?
  2. What does Shape.prototype point to?
  3. What is the prototype of Shape.prototype?
function Shape(type) {
this.type = type;
}

Solution:

  1. The prototype of Shape is Function.prototype, since Shape is a function
  2. Shape.prototype points to a newly created object
  3. The prototype of Shape.prototype (the new object) is Object.prototype

Exercise 2

When you execute the code below,

  1. What is the prototype of emma?
  2. What is the prototype of emma’s prototype?
  3. What is the prototype of Employee?
  4. What does Employee.prototype point to?
function Employee(name, hireDate) {
this.name = name;
this.hireDate = hireDate.toLocaleDateString('en-US');

const emma = new Employee('emma', new Date('January 10 2020'));

Solution:

  1. The prototype of emma is Employee.prototype
  2. The prototype of emma’s prototype (Employee.prototype) is Object.prototype
  3. The prototype of Employee is Function.prototype
  4. Employee.prototype points to an object, which is the prototype of emma (hence, the prototype of emma is Employee.prototype)

Exercise 3

function Employee(name, hireDate) {
this.name = name;
this.hireDate = hireDate.toLocaleDateString('en-US');
}

const emma = {};
Employee.call(emma, 'emma', new Date('January 10 2020'));
console.log(emma); // { name: 'emma', hireDate: '1/10/2020' }
  1. We never defined the call method on Employee, why aren’t we getting a TypeError: Employee.call is not a function?
  2. What is the prototype of the emma object?

Solution:

console.log(Employee.hasOwnProperty('call')); // false
console.log(Function.prototype.hasOwnProperty('call')); // true
  1. Since Employee is a function, it’s prototype is Function.prototype. The method, call, exists on Function.prototype.
    Recall from part 1, when attempting to access a property of an object, the search for that property extends beyond just the object itself. It continues on to the object’s prototype, the prototype of the prototype, and so on. In this case, the JavaScript runtime saw that Employee does not have the call function, and so it looked up the prototype chain and found call in Function.prototype. As a result, it executed the call method from the prototype.
  2. The prototype of the emma object is Object.prototype, NOT Employee.prototype. We didn’t call new Employee to create emma, nor did we use Object.create. Thus, we never established a prototypal relationship between Employee and emma.

Exercise 4

What happens when you run the code below?

function Shape(type) {
this.type = type;
}

const triangle = Shape('Triangle');
console.log(triangle.type);

Solution:

You get an error, TypeError: Cannot read properties of undefined (reading ‘type’). Since the new keyword is not used, the Shape function is executed in the current scope and no new object is created. The property type is added to whatever this is referencing in the current scope. Now, since functions in JavaScript return undefined by default, triangle is assigned undefined. Attempting to access a property of undefined results in an error.

Had we used the new keyword, the function would create a new object and bind it to the this context. Then, it would return this newly created object.

Exercise 5

What does the console.log statement evaluate to in the code below? Why?

function Employee(name, hireDate) {
this.name = name;
this.hireDate = hireDate.toLocaleDateString('en-US');
return {
name,
hireDate: hireDate.toLocaleDateString('en-US')
}
}

const emma = new Employee('emma', new Date());
console.log(Object.getPrototypeOf(emma) === Employee.prototype);

Solution:

It evaluates to false. Let’s break it down step by step:

  1. When new Ecmployee('emma', new Date()) is called, JavaScript internally creates a new object (let’s call it newObj). This object is set up with it’s [[prototype]] pointing to Employee.prototype. This is part of the standard behaviour of the new operator with constructor functions.
  2. Within the Employee function, this is bound to newObj. Any properties set on this (ex: this.name and this.hiredate) would be properties of newObj.
  3. However, the Employee function explicitly returns a new object. This object is distinct from newObj created and prepared by the new operator. It’s a plain object literal and as such, its prototype is Object.prototype not Employee.prototype
  4. Because the new Employee function returns the new object, the variable emma is assigned this object, not the newObj that was originally created and whose prototype was set to Employee.prototype
  5. When you compare the prototype of emma (Object.getPrototypeOf(emma)) with Employee.prototype , it evaluates to false . This is because emma's prototype is Object.prototype (the deafult for object literals) not Employee.prototype .

In summary, the key factor here is the explict return of a new object in the contructor function. This overrides the suual behaviour of the new operator, resulting in emma being a plain object with a prototype of Object.prototype , not Employee.prototype .

Exercise 6:

Why is the output of the code below undefined is flying the undefined? Can you fix it?

function Aircraft(pilot, type) {
this.pilot = pilot;
this.type = type;
}

Aircraft.prototype.fly = function() {
console.log(`${this.pilot} is flying the ${this.type}`);
}

function Jet(pilot, type, maxAltitude) {
this.maxAltitude = maxAltitude;
}

Object.setPrototypeOf(Jet.prototype, Aircraft.prototype);

const jumboJet = new Jet('Rayan', 'Jet', 35000 );
jumboJet.fly() // undefined is flying the undefined

Solution:

The output of the code is undefined is flying the undefined because we forgot to add Aircraft.call(this, pilot, type) to the Jet constructor function. As a result, this.pilot and this.type are undefined when the fly method is invoked.

You can fix the above code by modifying the Jet constructor as follows:

function Jet(pilot, type, maxAltitude) {
Aircraft.call(this, pilot, type); // Call Aircraft constructor
this.maxAltitude = maxAltitude;
}

Exercise 7:

The code below results in an error. Can you spot the bug?

function Tree() {}

Tree.prototype.grow = function() {
console.log('The tree is growing.');
};

function Oak() {}

Oak.prototype.dropLeaves = function() {
console.log('The oak tree is dropping leaves.');
};

Oak.prototype = Object.create(Tree.prototype);

const oakTree = new Oak();

oakTree.grow();
oakTree.dropLeaves();

Solution:

The error in the code occurs because the line Oak.prototype = Object.create(Tree.prototype); is placed after defining the dropLeaves() method on the Oak prototype. This order of statements causes the dropLeaves() method to be overwritten by the Object.create(Tree.prototype) assignment, resulting in an error when attempting to invoke oakTree.dropLeaves().

To fix the issue, you need to rearrange the order of statements so that the Object.create(Tree.prototype) assignment comes before adding the dropLeaves() method. Here's the corrected code:

function Tree() {}

Tree.prototype.grow = function() {
console.log('The tree is growing.');
};

function Oak() {}

Oak.prototype = Object.create(Tree.prototype);

Oak.prototype.dropLeaves = function() {
console.log('The oak tree is dropping leaves.');
};

const oakTree = new Oak();

oakTree.grow();
oakTree.dropLeaves();

Exercise 8:

When we run the code below, we get TypeError: someHome.details is not a function. Why? Fix it.

function Residence(type, owner, size) {
this.type = type;
this.owner = owner;
this.size = size;
};

Residence.prototype.details = function details() {
console.log(`This ${this.size} sq ft ${this.type} is owned by ${this.owner}`);
}

function TownHouse(type, owner, size, maintenanceFee) {
Residence.call(this, type, owner, size);
this.maintenanceFee = maintenanceFee;
}

Object.setPrototypeOf(TownHouse, Residence);

const someHome = new TownHouse('Townhouse', 'Harry Potter', 3300, 400);
someHome.details();

The reason you’re getting a TypeError is because you are setting the prototype of TownHouse to Residence incorrectly. Instead of using Object.setPrototypeOf(TownHouse, Residence), you should use Object.setPrototypeOf(TownHouse.prototype, Residence.prototype).

Here’s the corrected code:

function Residence(type, owner, size) {
this.type = type;
this.owner = owner;
this.size = size;
}

Residence.prototype.details = function details() {
console.log(`This ${this.size} sq ft ${this.type} is owned by ${this.owner}`);
};

function TownHouse(type, owner, size, maintenanceFee) {
Residence.call(this, type, owner, size);
this.maintenanceFee = maintenanceFee;
}

Object.setPrototypeOf(TownHouse.prototype, Residence.prototype);

const someHome = new TownHouse('Townhouse', 'Harry Potter', 3300, 400);
someHome.details(); // This 3300 sq ft Townhouse is owned by Harry Potter

By setting TownHouse.prototype to Residence.prototype, you establish the correct prototype chain, allowing someHome to access the details method defined in the Residence prototype.

Exercise 9:

Rewrite the following using constructor functions

const Person = {
init(name, age) {
this.name = name;
this.age = age;
},
introduce() {
console.log(`Hi, my name is ${this.name} and I am ${this.age} years old.`);
}
};

const Student = Object.create(Person);
Student.init = function init(name, age, grade) {
Person.init.call(this, name, age);
this.grade = grade;
}
Student.study = function study() {
console.log(`${this.name} is studying`);
}

const Teacher = Object.create(Person);
Teacher.init = function init(name, age, subject){
Person.init.call(this, name, age);
this.subject = subject;
}
Teacher.teach = function() {
console.log(`${this.name} is teaching ${this.subject}.`);
};

const Parent = Object.create(Person);
Parent.init = function init(name, age, numChildren) {
Person.init.call(this, name, age);
this.numChildren = numChildren;
};
Parent.care = function() {
console.log(`${this.name} is taking care of their children.`);
};

const faisal = Object.create(Student);
const izzy = Object.create(Teacher);
const babak = Object.create(Parent);

faisal.init("Faisal", 18, 12);
faisal.introduce(); // Hi, my name is Faisal and I am 18 years old.
faisal.study(); // Faisal is studying.

izzy.init("Izzy", 33, "Math");
izzy.introduce(); // Hi, my name is Izzy and I am 33 years old.
izzy.teach(); // Izzy is teaching Math.

babak.init("Babak", 43, 2);
babak.introduce(); // Hi, my name is Babak and I am 43 years old.
babak.care(); // Babak is taking care of their children.

Solution:

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

Person.prototype.introduce = function introduce() {
console.log(`Hi, my name is ${this.name} and I am ${this.age} years old.`);
}

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

Student.prototype.study = function study() {
console.log(`${this.name} is studying`);
}

Object.setPrototypeOf(Student.prototype, Person.prototype);

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

Teacher.prototype.teach = function teach() {
console.log(`${this.name} is teaching ${this.subject}.`);
}

Object.setPrototypeOf(Teacher.prototype, Person.prototype);

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

Parent.prototype.care = function care() {
console.log(`${this.name} is taking care of their children.`);
}

Object.setPrototypeOf(Parent.prototype, Person.prototype);

const faisal = new Student('Faisal', 18, 12);
faisal.introduce();
faisal.study();

const izzy = new Teacher('Izzy', 33, 'Math');
izzy.introduce();
izzy.teach();

const babak = new Parent('Babak', 43, 2);
babak.introduce();
babak.care()

Exercise 10

  1. Write an Employee constructor that initializes name and salary from arguments
  2. All employees should be able to requestRaise. An employee that requests a raise says “My name is <name>. I need a raise”
  3. A manager is a type of employee. Write a Manager constructor that can promoteEmployee. Promoting an employee increases that employee’s salary. Managers can’t raise their own salary or that of another manager.
  4. A developer is a type of employee. Developers should be able to code.
  5. A front end developer is a type of Developer. A front end developer that codes should say “I am building the front end”.
  6. A back end developer is a type of Developer. A back end developer that codes should say “I am building the back end”.

Test your code with the following

const rana = new FrontEndDeveloper('Rana', 130000); 
const lamya = new BackEndDeveloper('Lamya', 140000);
const fadi = new Manager('Fadi', 120000);
const wessam = new Manager('Wessam', 160000);

lamya.requestRaise(); // My name is Lamya. I need a raise.
fadi.promoteEmployee(lamya, 10000);
console.log(lamya.salary); //150000

rana.requestRaise(); // My name is Rana. I need a raise.
fadi.promoteEmployee(rana, 20000);
console.log(rana.salary); //150000

fadi.requestRaise(); // My name is Fadi. I need a raise.
fadi.promoteEmployee(fadi, 100000); // Fadi, you cannot promote yourself
wessam.promoteEmployee(fadi, 100000); // Wessam, managers cannot promote managers.
console.log(fadi.salary); // 120000

lamya.code(); // "I am building the back end."
rana.code(); // "I am building the front end."

lamya.promoteEmployee(fadi); // This should throw a TypeError: lamya.promoteEmployee is not a function
fadi.code(); // This should throw a TypeError: fadi.code is not a function

Solution:

function Employee(name, salary) {
this.name = name;
this.salary = salary;
}

Employee.prototype.requestRaise = function requestRaise() {
console.log(`My name is ${this.name}. I need a raise.`);
}

function Manager(name, salary) {
Employee.call(this, name, salary);
}

Manager.prototype.promoteEmployee = function promoteEmployee(employee, amount) {
if(employee === this) {
return console.log(`${this.name}, you cannot promote yourself.`);
}
if(Manager.prototype.isPrototypeOf(employee)) {
return console.log(`${this.name}, managers cannot promote managers.`);
}
employee.salary += amount;
}

Object.setPrototypeOf(Manager.prototype, Employee.prototype);

function Developer(name, salary) {
Employee.call(this, name, salary);
}

Developer.prototype.code = function code() {
let workType;
if(FrontEndDeveloper.prototype.isPrototypeOf(this)) {
workType = "front end";
}
if(BackEndDeveloper.prototype.isPrototypeOf(this)) {
workType = "back end"
}
console.log(`I am building the ${workType}.`);
}

Object.setPrototypeOf(Developer.prototype, Employee.prototype);

function FrontEndDeveloper(name, salary) {
Developer.call(this, name, salary);
}

Object.setPrototypeOf(FrontEndDeveloper.prototype, Developer.prototype);

function BackEndDeveloper(name, salary) {
Developer.call(this, name, salary);
}

Object.setPrototypeOf(BackEndDeveloper.prototype, Developer.prototype);

--

--

Mom. Software Engineer.
0 Followers

I am a mom to two beautiful daughters, and a Software Engineer. This blog is my very personal experience trying to navigate both identities.