Prototype, this stranger!
A guide to objects, classes, and inheritance in JavaScript
In this article we will see how to use objects, classes and inheritance in JavaScript. In particular we will discuss the concept of “prototype” and how it can help us solve particular problems related to Object Oriented Programming in JavaScript.
General concepts
Class, objects, and inheritance are very important concepts in object-oriented programming. In its classic version, the basic concepts are basically three:
- Class: an abstraction of the real world with which we can represent, for example, a person, a thing etc. A class defines a state (attributes) and a behavior (methods).
- Object: it’s the specific instance of a certain class, the one that is returned to us when we use the
new
operator. It has its own specific state and behavior. - Inheritance: represents a mechanism that allows you to create an object (child) based on another already defined (parent).
Graphically we can schematize the concepts just expressed with a very simple example:
The Cat and Dog classes inherit the behaviors from the Mammal class, while c1, c2 are instances of the Cat class and d1, d2 are instances of the Dog class.
JavaScript objects
Anything in JavaScript that is not a primitive type is an object. An object is a set of key-value pairs, where the values can in turn be objects, primitive variables or functions. If an object has multiple key-value pairs, they must be separated by a comma. You can declare an object in two ways, the first is to use the Object
constructor and the second is to declare a variable in which the key-value pairs are enclosed by {}
. Let’s see how to declare an object with a small example:
const student = {
name: 'Davide',
number: '123456'
}const classroom = new Object({
name: 'Computer Science I',
teacher: 'Dennis Ritchie'
})console.log(student)
console.log(classroom)
If you run this piece of code the result will be
{ name: 'Davide', number: '123456' }
{ name: 'Computer Science I', teacher: 'Dennis Ritchie' }
You can access to the object properties using the dot .
:
const student = {
name: 'Davide',
number: '123456'
}const classroom = new Object({
name: 'Computer Science I',
teacher: 'Dennis Ritchie'
})console.log(`Student: ${student.name}`)
console.log(`Teacher: ${classroom.teacher}`)
In this case the result will be:
Student: Davide
Teacher: Dennis Ritchie
Define object properties
There are several ways to define the properties of an object in JavaScript. The first is to specify the object name followed by: a dot, the name of the new property, an equal sign and the value of the new property:
const student = {
name: 'Davide',
number: '123456'
}student.birthday = '4th July'
console.log(student)
Obviously, if we run this code, the result will be:
{ name: 'Davide', number: '123456', birthday: '4th July' }
Combining properties between objects is a common practice. Doing it property by property is boring. The static function Object.assign
helps us to do this with a few lines of code. Let’s see how it works with a small example:
const obj = {}
const student = {
name: 'Davide',
number: '123456'
}Object.assign(obj, student, {
birthday: '4th July'
})console.log(obj)
Also in this case the result we will get by running the script will be:
{ name: 'Davide', number: '123456', birthday: '4th July' }
The function iterates all the properties of the objects passed in input starting from the last one and assigns them to the object given in input previously. So in the example above, the birthday
property is assigned to the student
object and after the properties of the student
object are assigned to obj
. If the property we are assigning exists in the leftmost object, passed as a parameter, this will be overwritten with the new one. If we modify the script as follows:
const obj = {
birthday: '2nd July'
}const student = {
name: 'Davide',
number: '123456',
birthday: '3rd July'
}Object.assign(obj, student, {
birthday: '4th July'
})console.log(obj)
It’s no wonder that the result we will get by running this script will be the same as that obtained in the previous examples:
{ name: 'Davide', number: '123456', birthday: '4th July' }
Another way to define an object property is to use the static function Object.assign
. It allows us to define not only the name of the property and possibly its value, but it provides us with a series of options that can be useful on the property we are defining. In particular, the function takes three parameters as input:
- The object to which you want to add a property;
- The name of the property;
- An options object.
With the options object passed as the third parameter we can specify the following properties:
- configurable: if set to
true
the property can be deleted or modified. Its default value isfalse
. - enumerable: if set to
true
, the property will be visible during the enumeration of properties on the object (for examplefor..in
orObject.keys()
). The default value isfalse
; - value: the value we want to assign to the property. Its default value is
undefined
; - writable: if set to
true
the property can be overwritten. Its default value isfalse
; - get: a function representing the method that returns the value of the added property. Its default value is
undefined
; - set: A function representing the method that sets the value of the added property. Its default value is
undefined
;
Let’s see how to use this function with some practical examples. We define a new birthday
property on our student
object. This property will only be readable once defined and will not be editable so, in addition to giving an initial value to the value property, we also set the writable property to false
:
const student = {
name: 'Davide',
number: '123456'
}Object.defineProperty(student, 'birthday', {
writable: false,
value: '4th July'
})console.log(student) // [1]
console.log(student.birthday) // [2]// [3]
student.birthday = 'Another date'
console.log(student.birthday)
Running the script you will get a run-time error such as:
TypeError: Cannot assign to read only property ‘birthday’ of object ‘#<Object>’
Let’s analyze the three points highlighted in the script:
- We have not set the
enumerable
property, then the property will have the default value offalse
, consequently when we try to print the entire object on the screen withconsole.log
, thebirthday
property and its relative value will not be displayed. - The second
console.log
will correctly print the value of the birthday property on the screen. - We have not set the value of the
writable
option tofalse
, thebirthday
property cannot be overwritten, as a result we will get an error at run time.
Let’s see how the behavior of point 1 and point 3 changes, making this small change to our code:
...
Object.defineProperty(student, 'birthday', {
writable: true,
enumerable: true,
value: '4th July'
})
...
Having set the writable
and enumerable
properties to true
, the script will not exit with an error and will also display the birthday
property that previously was not visible:
{ name: 'Davide', number: '123456', birthday: '4th July' }
4th July
Another date
In JavaScript, you can delete the properties of an object using delete
. Now let’s try to delete the birthday
property defined previously. Let’s add these two lines at the end of our script:
...
Object.defineProperty(student, 'birthday', {
writable: true,
enumerable: true,
value: '4th July'
})
...delete student.birthday
console.log(student)
If we run this script we will get the following error at run time:
TypeError: Cannot delete property 'birthday' of #<Object>
We get this error as the configurable
parameter default value its false
. This means that it is not possible to delete the birthday
property from our object. If we try to set it to true as follows:
...
Object.defineProperty(student, 'birthday', {
configurable: true,
writable: true,
enumerable: true,
value: '4th July'
})
...delete student.birthday
console.log(student)
The script will no longer end with an error, but as expected the results will be correctly printed on the screen:
{ name: 'Davide', number: '123456', birthday: '4th July' }
4th July
Another date
{ name: 'Davide', number: '123456' }
Prototype
All JavaScript objects have [[prototype]]. This property is an implicit reference to another object that is queried in property searches.
If an object does not have a particular property, the prototype
of the object is checked for that property. If the object prototype
does not have that property, the object prototype
is checked and so on. This is how inheritance works in JavaScript, JavaScript is a prototype language. This will be explored in detail later in this article.
Object.create
In JavaScript this method can be used to replace new keyword. We can use it to create an empty object based on a defined prototype and then assign it to a different prototype:
const anotherObject = {
a: 2
}
// create an object linked to anotherObject
const myObject = Object.create(anotherObject)
myObject.a // the value will be 2
Now myObject
is linked to anotherObject
. Clearly myObject.a
does not actually exist but, however, access to the property is successful because exists on anotherObject
and in fact finds the value 2
.
Inheritance
Basically in JavaScript the inheritance it’s obtained with a chain of [[prototype]]. There are several ways to create this prototype chain, but in this article we will analyze the most common:
- Functional
- Constructor functions
- Classes
Using [[prototype]] we can add new properties and methods to an existing object constructor. We can then essentially tell our JavaScript code to inherit properties from a prototype. This prototype chain allows us to reuse properties and methods from one JavaScript object to another via a pointer function reference. Wanting to make a comparison with classical inheritance, here’s what happens graphically:
Functional inheritance
To create the prototype chains with this methodology just use the Object.create
function showed previously. Let’s see how to do it with an example:
const mammal = {
introduceYourself: function () {
console.log(`Hello I'm a ${this.type} and my name is: ${this.name}`)
}
}const cat = Object.create(mammal, {
type: { value: 'cat' },
noise: { value: 'meow' },
meow: { value: function () {
console.log(`I ${this.verso}: MEEEEEOOOOOW`)
}}
})const dog = Object.create(mammal, {
type: { value: 'dog' },
noise: { value: 'bark' },
bark: { value: function () {
console.log(`I ${this.noise}: WOOF WOOF`)
}}
})const fuffy = Object.create(cat, { name: { value: 'Fuffy' } })
fuffy.introduceYourself()
fuffy.meow()const bobby = Object.create(dog, { name: { value: 'Bobby' } })
bobby.introduceYourself()
bobby.bark()
The mammal
object is a simple JavaScript object, defined using the braces {}
. The prototype for simple objects like this is Object.prototype
. The Object.create
function, described previously, we have created the cat
and dog
objects passing the prototype of the desired object as first argument, in this case mammal
. So mammal
is the prototype of cat
and dog
. When the bobby
and fuffy
objects are created, they are passed as first argument to the Object.create
function, dog
and cat
, respectively. So dog
is the prototype of bobby
and cat
is the prototype of fuffy
. The entire prototype chain is:
- The prototype of
fuffy
iscat
; - The prototype of
bobby
isdog
; - The
dog
andcat
prototype ismammal
; - The
mammal
prototype isObject.prototype
.
Analyzing the steps performed by fuffy.introduceYourself()
(and in the same way bobby.introduceYourself()
), let’s try to understand even better which steps are performed:
- It is checked if
fuffy
has a propertyintroduceYourself
; It is not so; - It is checked whether the prototype of
fuffy
,cat
, has a propertyintroduceYourself
; It is not so; - It is checked whether the prototype of
cat
,mammal
, has a propertyintroduceYourself
; performs it; - Performs the function
introduceYourself
onfuffy
, somammal
type,this.type
will becat
andthis.name
will be “Fuffy”.
To complete the functional paradigm applied to prototype inheritance, the creation of an instance of a dog
and a cat
can be generalized with a function:
...
function createCat (name) {
return Object.create(cat, {
name: { value: name }
})
}function createDog (name) {
return Object.create(dog, {
name: { value: name }
})
}const fuffy = createCat('Fuffy')
fuffy.introduceYourself()
fuffy.meow()const bobby = createCat('Bobby')
bobby.introduceYourself()
bobby.bark()
Remember that the prototype of an object can be inspected with Object.getPrototypeOf()
:
console.log(Object.getPrototypeOf(fuffy) === cat) //true
console.log(Object.getPrototypeOf(bobby) === dog) //true
Constructor functions Inheritance
This approach is frequently used and simple, just declare a function and call it using the new keyword. Let’s go back to the example showed before:
function Mammal (name) {
this.name = name
}Mammal.prototype.introduceYourself = function () {
console.log(`Hello I'm a ${this.type} and my name is: ${this.name}`)
}function Cat (name) {
this.type = 'cat'
this.noise = 'meow'
Mammal.call(this, name)
}Cat.prototype.meow = function () {
console.log(`I ${this.noise}: MEEEEEOOOOOW`)
}Object.setPrototypeOf(Cat.prototype, Mammal.prototype)function Dog (name) {
this.type = 'dog'
this.noise = 'bark'
Mammal.call(this, name)
}Dog.prototype.bark = function () {
console.log(`I ${this.noise}: WOOF WOOF`)
}Object.setPrototypeOf(Dog.prototype, Mammal.prototype)const fuffy = new Cat('Fuffy')
fuffy.introduceYourself()
fuffy.meow()
const bobby = new Dog('Bobby')
bobby.introduceYourself()
bobby.bark()
The constructor functions Dog
, Cat
, and Mammal
have a first capital letter. Just as in Object Oriented Programming in general it is a convention, also in this case it is.
When new Cat('Fuffy')
is invoked, a new instance of Cat
is created (fuffy
). This new object is also the this
object inside the Cat
constructor function. After,Cat
passes the reference to itself, this
, to Mammal.call
. Using this method allows you to set the this
object of the function called through the first argument that was passed to it. Then, when this
is passed to Mammal.call
, the reference is also created to the newly created object (which is eventually assigned to fuffy
) via the this
object inside the Cat
constructor function. All subsequent arguments passed to the call become the arguments of the function, so the argument of the name passed to Mammal is “Fuffy”. The Mammal
constructor sets this.name
to “Fuffy”, which means that eventually fuffy.name
will also be “Fuffy”. So if we want to describe the entire prototype chain:
- The
fuffy
prototype isCat.prototype
; - The
bobby
prototype isDog.prototype
; - The
dog
andcat
prototype isMammal.prototype
; - The
Mammal
prototype isObject.prototype
.
Classes inheritance
A little syntactic sugar has been introduced in modern versions of JavaScript. Obviously we are talking about the keyword class, and it is recommended not to confuse this keyword with the same word in other OOP languages, such as Java.
This keyword in JavaScript does nothing more than create a constructor function which must then be called with new. In fact, if you use the developer console of any browser and try to type :
typeof class Mammal {}
the results will be:
The use of class only reduces the number of lines of code, thereby increasing readability, which is useful for creating prototype chains. Let us now return to our example for the third and last time:
class Mammal {
constructor (name) {
this.name = name
} introduceYourself () {
console.log(`Hello I'm a ${this.type} and my name is: ${this.name}`)
}
}class Cat extends Mammal {
constructor (name) {
super(name) this.type = 'cat'
this.noise = 'meow'
} meow () {
console.log(`I ${this.noise}: MEEEEEOOOOOW`)
}
}class Dog extends Mammal {
constructor (name) {
super(name) this.type = 'dog'
this.noise = 'bark'
} bark () {
console.log(`I ${this.noise}: WOOF WOOF`)
}
}const fuffy = new Cat('Fuffy')
fuffy.introduceYourself()
fuffy.meow()const bobby = new Dog('Bobby')
bobby.introduceYourself()
bobby.bark()
Also in this case the prototype chain is:
- The
fuffy
prototype isCat.prototype
; - The
bobby
prototype isDog.prototype
; - The
dog
andcat
prototype isMammal.prototype
; - The
Mammal
prototype isObject.prototype
.
The extends
keyword makes prototype inheritance much easier. In the example code, the Dog
extends Mammal
class will ensure that the prototype of Dog.prototype
will be Mammal.prototype
. The constructor method in each class is the equivalent of the function body of a constructor function. So, for example, the function:
function Dog (name) {
this.name = name
}
is the equivalent of:
class Dog {
constructor (name) {
this.name = name
}
}
The super
keyword in the Dog
class constructor method is a generic way to call the parent class constructor by setting this
into the current instance. In the example of the constructor function Dog.call(this, name)
here becomes the equivalent of super(name)
.
Conclusion
The concept of prototype inheritance in JavaScript often causes some confusion. I hope that by reading this article it will be clearer and much simpler. Well for now …
HAPPY, CODING!
Bibliography
More content at plainenglish.io