Object Oriented Concepts
Prototypes, Classes & the new operator
Object Prototypes
Prototypes are the mechanism by which Javascript objects inherit features from one another. Objects can have prototype object, which acts as a template object. This is the _proto_ object template that has all the default functions described on that object.
In arrays, which are objects in Javascript, we can see the key value pairs w.r.t the index, but there is another key called _proto_, which contains all the default functions on the array like push()
, pop()
, shift()
, etc.
We can check this using Array.prototype
and then expanding the same in the developer console.
[object].prototype.[function]
is the actual prototype object where we add the property/function to the prototype template object.
The __proto__
is the protperty on the object or primitive data type (like arrays, strings, etc.). It gives a reference to the prototype of that object or primitive data type. We can check that using [object].__proto__
.
Intro to OOP
Factory Functions
With the help of factory functions, we need not pass the same parameters for different operations with that data. We can use the factory function created in the object to accept the same set of paramerters for different operations.
function makeColor(r, g, b) {
const color = {};
color.r = r;
color.g = g;
color.b = b;
color.rgb = function () {
const { r, g, b } = this;
return `rgb(${r},${g},${b})`;
};
color.hex = function () {
const { r, g, b } = this;
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};
return color;
}
const firstColor = makeColor(25, 150, 187);
console.log(firstColor.rgb()); //rgb(25,150,187)
console.log(firstColor.hex()); //#1996bb
We make a function with a bunch of functions to perform different operations, and then return the object. This object we can then use to call the "factory" functions defined in the object and perform the required operation.
The problem with factory functions is that every time we create a new object to access the porperties of an object, there is a unique copy of the function we call, that is created, which is generally not needed.
So when we call firstColor.hex === secondColor.hex
, and since functions are reference types, it should return true
, but we get false
instead. This is because of the fact that each object will have it's own copy of the factory function.
Constructor Functions
Factory functions or patterns are not commonly used. Instead we use constructor functions or patterns, which is more common.
function Color(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
console.log(this);
}
When we call the Color()
function with the new
keyword, we are actually doing the following:
- Creates a black, plain Javascript object.
- Links (sets the constructor of) this object to another object
- Passes the newly created object from the first step as the this context.
- Returns this if the function does not return it's own object.
So when we call the Color()
function without the new
keyword, we see that logging this
gives us the Window
object, instead of the r,g,b
color values object.
Same function call with the new
keyword gives us the color object with the r,g,b
values.
Color(255, 10, 50); //Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
new Color(255, 10, 50); //Color {r: 255, g: 10, b: 50}
Also, in the function call with the new
keyword, the thing that is done implicitly is, in the developer tools, we can see under the __proto__
property of the returned object, there is a constructor
function as the key
with the value set to the Color()
function. This is what is meant by 'Links (sets the constructor of) this object to another object'.
This does not create a unique copy of the function, hence it's more efficient, since it goes into the __proto__
property of the object, hence increasing usability.
//constructor function
function Color(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
//function becomes a prototype for other objects from the same constructor function
Color.prototype.rgb = function () {
const { r, g, b } = this;
return `rgb(${r},${g},${b})`;
};
Color.prototype.hex = function () {
const { r, g, b } = this;
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
};
const color1 = new Color(15, 167, 87);
const color2 = new Color(14, 15, 16);
console.log(color1.rgb()); //rgb(15,167,87)
console.log(color1);
/*
Color {r: 15, g: 167, b: 87}
r: 15
g: 167
b: 87
__proto__:
rgb: ƒ ()
constructor: ƒ Color(r, g, b)
__proto__: Object
*/
Here, if we check color1.hex === color2,.hex
, we get true
, since the function hex
is now a prototype, so it will not be a unique property on the hex
function. The same __proto__
will be shared for both color1
and color2
objects.
JS Classes
This is syntactical sugar for constructor functions and their function prototypes.
Main advantages of using classes over constructor functions is that we don't have to declare the constructor and the object prototype functions seperately.
All the functions defined inside the class
will be by default added to the __proto__
property of the object when using the new
keyword to create a new object of that class
.
class Color {
constructor(r, g, b, name) {
this.r = r;
this.g = g;
this.b = b;
this.name = name;
}
innerRGB() {
const { r, g, b } = this;
return `${r},${g},${b}`;
}
rgb() {
return `rgb(${this.innerRGB()})`);
}
hex() {
const { r, g, b } = this;
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
rgba(a = 1.0) {
return `rgba(${this.innerRGB()},${a})`;
}
}
const c1 = new Color(231, 166, 91, 'mudbrown');
console.log(c1.rgba(0.4)); //rgba(231,166,91,0.4)
Inheritance - Extends, Super and Subclasses
Inheritance is mainly used to share common code across different classes. Like in the example for class Pet
, we share the function eat()
and the constructor
which is assigning the name
and age
property to our pet. The Pet
class is being extended
by the subclasses
Dog
and Cat
using the keyword extends
on the subclasses.
In case there is method overloading, i.e., when there are two or more functions with the same name, there is a way JS resolves this issue.
If the function is defined in the derived class
, that is looked up and called first. If not found in the derived class
, it is looked up in the base class
, from which this class is using the extends
keyword. Then that function is used. Even if the function is not found in the base class
, then Javascript will throw an error.
The super keyword
The super
keyword comes into play when we have to initialize more values in our derived class
constructor
than what are defined in the base class
constructor
.
Have a look at the example below:
class Pet {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
return `${this.name} is eating!`;
}
}
class Cat extends Pet {
constructor(name, age, livesLeft = 9) {
super(name, age);
this.livesLeft = livesLeft;
}
meow() {
return 'MEOW!!';
}
}
Here, in our base class
called Pet
, we are using the constructor
to initialize only name
and age
for the derived class
.
In our derived class
called Cat
, we need to define another variable in our referene object of class Cat
. This is again done by creating a constructor
inside the Cat
class. But we need not define the name
and age
again for the Cat
, we can simply call the base class
constructor using the keyword super
with the parameters as name
and age
, which in turn calls the base class's
(Pet
) constructor which assigns the name
and age
propoerties to out new Cat
reference object.