Prototypal Inheritance with Constructor Functions
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:
- Created a function,
Animal
, and set its [[prototype]] reference toFunction.prototype
(Recall our previous discussion: the prototype of functions isFunction.prototype
). - It created a new object
- It added a property,
prototype
, to the Animal function and pointed to this new object - 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:
- Created a blank object,
bloppy
- Pointed
bloppy
’s [[prototype]] toAnimal
’s prototype property - Executed the constructor function with the given arguments, binding
bloppy
as thethis
context (i.e, all references tothis
in the constructor functionAnimal
now refer to thebloppy
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:
- The
Animal
function is executed in the current scope, not as a constructor (since we didn’t use thenew
keyword). - The
this
insideAnimal
does not refer to a newly created object but instead depends on the context in which the function is called (tryconsole.log(this)
in theAnimal
function, what do you get?). - In JavaScript, functions that do not explicitly return a value will returned
undefined
by default. Thus, in {2}, we assignundefined
tobloppy
. - When attempting to access
bloppy.name
in theconsole.log
statement, aTypeError
is thrown becausebloppy
isundefined
and does not have atype
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:
- A new empty object is created. Let’s refer to this object as
newInstance
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, allowingnewInstance
to inherit properties and methods fromF.prototype
.- 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. - 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:
- 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 toAnimal.prototype
, the method is shared among all instances, resulting in memory efficiency. - 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.
- We create an
Animal
function and add thewalk
method to its prototype - We create a
Bear
function.
- TheBear
function includes the codeAnimal.call(this, name)
to set thethis
object of theAnimal
function. By passingthis
toAnimal.call
, the newly created object (eventually assigned tobaloo
) is also referenced asthis
inside theBear
constructor. The additional arguments passed tocall
become the function arguments, so the name argument “Baloo” is passed toAnimal
. As a result,baloo.name
is set to “Baloo”. Without this line, executingbaloo.walk()
orbaloo.growl()
would result inundefined: I am walking
orundefined: rrrrrr
, respectively. - The statement
Bear.prototype = Object.create(Animal.prototype)
sets the prototype ofBear.prototype
to beAnimal.prototype
. Thus, all properties and methods defined onAnimal.prototype
are accessible to objects further down the prototype chain, including instances of theBear
constructor (likebaloo
). - We add the
growl
method toBear.prototype
. - We use the
Bear
constructor to create an object,baloo
. - We call
baloo.walk
. When we do this, the JavaScript runtime checks ifbaloo
has the walk property. It does not. Therefore, it moves up the prototype chain and checks ifBear.prototype
has thewalk
property. It does not. Thus it moves up the prototype chain one more time and checks ifAnimal.prototype
has thewalk
property. The runtime executes thewalk
function, settingthis
tobaloo
. - We call
baloo.growl
. As above, the Javascript runtime travels up the prototype chain until it finds thegrowl
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 statementBear.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:
- 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’sprototype
property, and executing the function with the new object as the context. - 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,
- What is the prototype of
Shape
? - What does
Shape.prototype
point to? - What is the prototype of
Shape.prototype
?
function Shape(type) {
this.type = type;
}
Solution:
- The prototype of
Shape
isFunction.prototype
, sinceShape
is a function Shape.prototype
points to a newly created object- The prototype of
Shape.prototype
(the new object) isObject.prototype
Exercise 2
When you execute the code below,
- What is the prototype of
emma
? - What is the prototype of emma’s prototype?
- What is the prototype of
Employee
? - 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:
- The prototype of
emma
isEmployee.prototype
- The prototype of emma’s prototype (
Employee.prototype
) isObject.prototype
- The prototype of
Employee
isFunction.prototype
Employee.prototype
points to an object, which is the prototype ofemma
(hence, the prototype ofemma
isEmployee.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' }
- We never defined the
call
method onEmployee
, why aren’t we getting aTypeError: Employee.call is not a function
? - What is the prototype of the
emma
object?
Solution:
console.log(Employee.hasOwnProperty('call')); // false
console.log(Function.prototype.hasOwnProperty('call')); // true
- Since
Employee
is a function, it’s prototype isFunction.prototype
. The method, call, exists onFunction.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 thatEmployee
does not have thecall
function, and so it looked up the prototype chain and foundcall
inFunction.prototype
. As a result, it executed thecall
method from the prototype. - The prototype of the
emma
object isObject.prototype
, NOTEmployee.prototype
. We didn’t callnew Employee
to createemma
, nor did we useObject.create
. Thus, we never established a prototypal relationship betweenEmployee
andemma
.
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:
- When
new Ecmployee('emma', new Date())
is called, JavaScript internally creates a new object (let’s call itnewObj
). This object is set up with it’s [[prototype]] pointing to Employee.prototype. This is part of the standard behaviour of thenew
operator with constructor functions. - Within the
Employee
function,this
is bound tonewObj
. Any properties set onthis
(ex:this.name
andthis.hiredate
) would be properties ofnewObj
. - However, the
Employee
function explicitly returns a new object. This object is distinct fromnewObj
created and prepared by thenew
operator. It’s a plain object literal and as such, its prototype isObject.prototype
notEmployee.prototype
- Because the new
Employee
function returns the new object, the variableemma
is assigned this object, not thenewObj
that was originally created and whose prototype was set toEmployee.prototype
- When you compare the prototype of
emma
(Object.getPrototypeOf(emma)
) withEmployee.prototype
, it evaluates tofalse
. This is becauseemma
's prototype isObject.prototype
(the deafult for object literals) notEmployee.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
- Write an Employee constructor that initializes name and salary from arguments
- All employees should be able to
requestRaise
. An employee that requests a raise says “My name is <name>. I need a raise” - 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. - A developer is a type of employee. Developers should be able to
code
. - A front end developer is a type of Developer. A front end developer that codes should say “I am building the front end”.
- 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);