A deeper look at object-oriented programming in JavaScript
JavaScript is a class-less programming language. Classes, as you know in other languages like Java, do not exist in JavaScript.
Even though JavaScript doesn’t provide support for Classes, clever use of existing concepts (constructor function and prototypal inheritance) in JavaScript allowed the developers to emulate this behavior successfully. So the class
keyword which you can find in JavaScript after the ES6 update is a syntactical abstraction for the steps required to emulate classes using existing JavaScript concepts. In this post, we will see what are the underlying concepts behind the class keyword.
But first, let’s see two of the most simple ways to create objects
Object Constructor and Object literals
JavaScript lets you create objects using the Object()
constructor. The Object constructor creates an object wrapper for the given value. And if the value is null it will create and returns an empty object.
In the above example by calling the Object constructor with empty parameters we have initialized an empty object and assigned it to the variable dog. Then we simply add object properties with the help of the dog
variable
After the initial years of JavaScript, object literals became the preferred way of creating objects. We can rewrite our previous example as:
Though very simple to use Object Constructor
and Object literal
have some downsides when creating objects. Creating multiple objects with the same interface requires a lot of code duplication.
Creating Objects
We could just simply use class
to create objects and that is the preferred way since the introduction of classes in ES6. But to understand how OOP in Javascript works, we must understand the underlying concepts that are replaced by classes. So we should take a look at some ways to implement Object creation with interfaces.
The function constructor pattern
JavaScript objects can be created using a constructor. There are some native constructors available right out the box in JavaScript, such as Object
and Array
. It is possible to define custom constructors using functions that define the properties and methods. So let's start by creating an object called dog
.
Note that to create an object using the dog
function as a constructor we use the new
operator in line number 10
. When line 10
is executed the following steps take place:
- A new Object is created in the memory
- It sets this new object’s internal, inaccessible,
[[prototype]]
property to be the constructor function’s external, accessible, prototype object (every function object automatically has a prototype property). - The
this
value is assigned to the newly created object. - It executes the constructor function, using the newly created object whenever
this
is mentioned. - the newly created object is returned unless the constructor function returns a
non-null
object reference. In this case, that object reference is returned instead.
The only difference between constructor functions and other functions is the way in which they are called. Any function that is called with the new
operator acts as a constructor.
While creating the object using the constructor, one thing worth noticing is that tuffy
object had his own copy of the properties present inside the dog
constructor. ie. changing the property associated with one object will have no effect in the properties of the constructor function. Let’s take a look at an example:
As you can see from the above example changing the toys property in the tuffy
object has not affect on the toys
property inside the tyson
object. Because both tuffy
and tyson
have their own copies of constructor properties.
But due to this nature of objects having their own copy of properties we can face memory related issue. As multiple copies of the same property takes up a lot of space. This is the major drawback of the function constructed pattern.
The Prototype pattern
Whenever you create a function in JavaScript you will notice that an object name prototype is appended in the definition of the function. Inside the prototype object we can add properties that the resulting object should inherit. The benefit of using a prototype is that the resulting object will not have a copy of the properties but it can still access the properties and methods by searching for them in the prototype object.
Let’s take an example:
The way prototype works is that when a property is accessed a search starts to find the property. The search begins on the object first and if the property is found in the object then it returns the value. Like when we tried to call dog.legs
in line 12
it returns the value 4 as the property legs exist in the object. And when the property is not found in the object the search continues up the prototype. And the prototype is searched for the property. If the property is found on the prototype then it returns the value. If not found the search continues up the prototype chain till it encounters the property. Like when we tried to access the property legs on boy
the search moves to the prototype because the object boy doesn’t have the property leg. Since the prototype of the object boy has the property legs it returns the value.
One thing that we need to keep in mind that prototypes are dynamic. So any changes made in the prototype at any point will be reflected on the objects.
Even the prototype pattern is not perfect. the first downside is that you don’t have the ability to pass the arguments like we could do it in function constructors. But honestly, this is not a very big downside it’s inconvenient but developers can easily overcome it.
But the real problem arrives with reference properties. Let's take an example to understand this drawback:
As you can see that dog1
and dog2
both have the same property. This happened because in line 6
the push function is actually referencing the property which returns the toys
property present inside the prototype and added a new value to the toys
array. So properties that can be referenced can cause this inconsistent behavior.
This behavior of prototypes can be desirable for some and could be a problem for others. Although generally it is desired for the objects to have their own copies of properties. Hence it is rare that prototype pattern is used alone.
Inheritance
Inheritance in JavaScript is primarily done through prototype chaining. First, we should know what prototype chaining is.
Prototype chaining
JavaScript has only one construct that is the object. As we have seen that each object has a property called prototype. A prototype is an object which points to the properties and methods of the constructor. But it's not necessary for the prototype to be constructor, it can be an object also. And that object will also have a prototype of its own and its prototype might have a prototype of its own and this pattern will continue, forming a chain of prototypes called prototype chain.
Let’s take a look at how prototype chaining is implemented:
So as you can see that human
inherits animals
by pointing its prototype pointer to the animals
object. So when we access Jon.canWalk
it searches for the canWalk property in its object then inside its prototype: human
object and then the search reaches to animals
object and finally returning its value.
The problem with prototype chaining is the same problem that we face with prototypes. When we are using properties that contains reference values these properties are shared among the objects and this is why properties are defined using constructors.
Properties are typically defined in constructor instead of using prototypes. Because we have seen that properties with reference values can be irritating.
Classical Inheritance
Classical Inheritance or Constructor Stealing or Masquerading all means the same thing. The idea behind classical inheritance is to call the constructor of an object inside the constructor of another object. We can leverage the apply()
and call()
methods to execute the constructor on a newly created object. Let’s take a look at the example.
By using the call()
or apply()
method we are essentially stealing the constructor function from the shapes. The shapes constructor when called inside the square constructor it is running the code initialization of the shapes constructor and because of which every object of square type has a copy of its own colors.
Constructor stealing (classical inheritance) comes with a benefit over prototype chaining. With classical inheritance we have the ability to pass arguments which was a downside in prototype chaining.
That being said classical inheritance has its own drawback. Methods must be written inside the constructor so there’s no function reuse. Constructor stealing doesn’t automatically create prototypes hence a sub-type object can’t use the prototype from the super-type.
Combined Inheritance
Combined Inheritance (also known as pseudo-classical inheritance) is a combination of prototype chaining and classical inheritance. So we can inherit properties and methods from the prototype and use classical inheritance to inherit instance properties.
As you can see that we have used constructor stealing(classical inheritance) to inherit properties and we have used prototype chaining to inherit methods. So we have created two instance of object dog which have their own copies of properties and at the same time they are sharing a method.
Classes
Up until this point we have gone through the in-depth view of how it is possible to emulate classes in JavaScript. But implementing these methods can be tedious task and it increases the probability of creating bugs in your code. So starting from ES6 class
was introduced that abstracts the logic behind OOPs in JavaScript. The ES6 classes appear to feature canonical object-oriented- programming, but they still use the prototypes and constructor under the hood.
We can write a class definition in two ways: class declaration and class expressions
Note: By default, everything inside the class method executes in strict mode.
A class can be composed of the constructor, get methods, set methods, methods, and static methods.
To create an object using class
we use the new
keyword. And the new
keyword will follow the same steps that we saw when we created an object using function constructor
in our previous sections.
In JavaScript, classes are first-class citizens. That means that classes can do everything that other things can do. Classes can be passed around like you would pass any other object or function reference. Classes can be used anywhere a function (for example in arrays and function parameters).
Below are some of the members that we generally see inside a class object:
Inside a class, all the members and properties defined inside the constructor will not be shared to prototype. Every Instance will have a copy of all the properties and methods inside the constructor. These properties are the same as we saw when we used function constructors while creating objects.
And everything defined outside the constructor and in the root of the class body is appended to the prototype object.
Note: Primitive data types and objects can not be added to the root of the class.
Static methods are methods that can be used even without an instance of the class. As we can see in Line 16
the method sayHello
was used without creating an instance of the class because it was a static method.
Inheritance
One more addition of the ES6 update was inheritance. The syntax is different but under the hood class inheritance still uses prototype chaining to achieve inheritance.
The extend
keyword allows to inherit from anything that has a [[construct]]
property and a prototype. It means it is backward compatible so, you can inherit from another class and you can also inherit from function constructors.
In this example, we are inheriting the properties and method in the Dog
class from the Animal
class. We have used the super()
method to invoke the constructor of the Animal
class.
This concludes OOPs pattern in JavaScript. We have seen that without having any formal concept of class we can still achieve classes and inheritance in JS. It’s important that we know how exactly JavaScript works. Prototypes and function constructors are important in JavaScript and one should invest time to learn about these.
Happy Coding :)