JavaScript OOP Course 3: Object Prototypes
References
- Code with Mosh: The Ultimate JavaScript Mastery Series – Part 2
- W3School: JavaScript Tutorial
Inheritance
Inheritance is one of the core concepts of Object Oriented Programming that enabled an Object to take on the Properties and Methods of another Object. This makes it easy to reuse code in different parts of an application.
In the terms of Classes:
- Base Class === Super Class === Parent Class (i.e. Shape)
- Derived Class === Sub Class === Child Class (i.e. Circle and Square)
- IS‑A Relationship (i.e. Circle is a Shape)
"In JavaScript we not have classes, only have Objects" that's when Prototypical Inheritance comes to the picture. Essentially we have two types of Inheritance: Classical and Prototypical.
Prototypes
A prototype in JavaScript is just a regular object in the memory. Prototype is essentially a parent of another object. When you heard a prototype just think parent.
Every object in JavaScript has a prototype or parent except the ROOT Object. Every object in JavaScript accept only a single object as a prototype or parent. And it inherits all the members defined on this prototype.
let obj = {};
obj
{}
[[Prototype]]: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: (...)
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
We can see that our object x
has Prototype Object
and Constructor function ƒ Object()
.
obj.constructor;
ƒ Object() { [native code] }
Only the ROOT Object in JavaScript doesn't have a prototype or parent and it is a single instance. Look at the example below – both x
and y
have the exact same prototype.
let x = {};
let y = {};
Object.getPrototypeOf(x);
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
Object.getPrototypeOf(x) === Object.getPrototypeOf(y);
true
Prototypical Inheritance
When we accessing a property or a method of an object, JavaScript engine first looks for that property or method on the object itself. And if the engine can't find that member at this level, then it loos at the prototype of that object.
When accessing a property or a method of an object, JavaScript engine loads the prototype chain to find the target member.
Multilevel Inheritance
let array = [];
array
[]
length: 0
[[Prototype]]: Array(0)
at: ƒ at()
...
lastIndexOf: ƒ lastIndexOf()
length: 0
map: ƒ map()
pop: ƒ pop()
push: ƒ push()
reduce: ƒ reduce()
reduceRight: ƒ reduceRight()
reverse: ƒ reverse()
...
[[Prototype]]: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
...
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: (...)
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
Objects created by given Constructor will have the same Prototype.
function Circle(radius) {
this.radius = radius;
}
const circle1 = new Circle(10);
circle1;
Circle {radius: 10}
radius: 10
[[Prototype]]: Object
constructor: ƒ Circle(radius)
[[Prototype]]: Object
const circle2 = new Circle(20);
circle2;
Circle {radius: 20}
radius: 20
[[Prototype]]: Object
constructor: ƒ Circle(radius)
[[Prototype]]: Object
Property Descriptors
let person = {name: "Spas"};
The new object person
has multiple members (properties and methods) inherited by its prototype – for example .toString()
method – but if we gonna iterate over the person members we not going to see these inherited members.
person.toString();
'[object Object]'
for (let key in person)
console.log(key);
name
console.log(Object.keys(person));
['name']
The reason why we don't see these inherited members is that in JavaScript the Properties (and the Methods) has Attributes attached to them. Sometimes these attributes prevent a property have been enumerated. Here is an example:
let person = {name: "Spas"};
let objectBase = Object.getPrototypeOf(person);
let descriptor = Object.getOwnPropertyDescriptor(objectBase, 'toString');
console.log( descriptor );
{writable: true, enumerable: false, configurable: true, value: ƒ}
The available Property Descriptors are:
configurable: true
– that means we can delete this member if we want to.enumerable: false
– that means (that's why) we can't iterate over this member.writable: true
– that means we can override this method (member).value: ƒ toString()
– is the implementation of the method of this particular property.
Set Property Descriptors. When we creating our own objects we can set these attributes. Here is an example.
let person = {name: "Spas"};
Object.defineProperty(person, 'name', {
writable: false,
enumerable: false,
configurable: false
});
person.name = 'John';
console.log(person.name); // You can see the name is not changed, because of the attribute 'writable: false'
Spas
for (let key in person)
console.log(key); // will return empty output because we have only one property with attribure 'enumerable: false'
_
delete person.name; // The property 'name' cannot be deleted because it has attribute 'configurable: false'
false
console.log(person.name);
Spas
Constructor Prototypes
We can get the prototype of an object by the following command.
let obj = {name: "Spas"};
Object.getPrototypeOf(obj);
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
The Constructors also have prototype property.
function Circle(radius) {
this.radius = radius;
}
const circle = new Circle(10);
Circle.prototype;
{constructor: ƒ}
constructor: ƒ Circle(radius)
[[Prototype]]: Object
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
Object.getPrototypeOf(circle);
{constructor: ƒ}
constructor: ƒ Circle(radius)
[[Prototype]]: Object
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
Prototype vs Instance Members
function Circle(radius) {
this.radius = radius;
this.draw = function() {
console.log('Draw a circle with R=' + this.radius);
}
}
const circle1 = new Circle(10);
const circle2 = new Circle(20);
const circle3 = new Circle(30);
In the above example we have Circle constructor with two members – radius
and draw()
. Each time when we create an instance by using the new
operator we creating a copy of all member definitions despite in some case the code is identical – lake it is for the draw()
method. This consume additional resources – as RAM, etc. – especially when there are large number of such members – imagine 50 methods within 500 instances of the object… In such cases we can define these members at the level of the prototype of the Constructor.
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.draw = function() {
console.log('Draw a circle with R=' + this.radius);
};
Defined at the prototype level, the method will be inherited by the instances, but won't be their member. It doesn't matter when we change the prototype (before defining a new object or later after that). The new method will be available to the object because here (in JavaScript) we dealing with object references (and prototypical inheritance, instead of real classes), so we have a single object in memory and as soon as we modify that (the prototype) all the changes are immediately visible.
const circle1 = new Circle(10);
circle1.draw();
Draw a circle with R=10
Object.keys(circle1);
['radius']
circle1;
Circle {radius: 10}
Object.getPrototypeOf(circle1);
{draw: ƒ, constructor: ƒ}
draw: ƒ ()
constructor: ƒ Circle(radius)
[[Prototype]]: Object
In the same way we can override the implementation of the default methods of the prototype. For example let's change the built-in .toString()
method.
circle1.toString();
'[object Object]'
Circle.prototype.toString = function() {
return 'Circle with Radius ' + this.radius;
};
circle1.toString();
'Circle with Radius 10'
Essentially we have Two kinds of methods and properties in JavaScript:
- Instance members and
- Prototype members.
In both of these kind of members we can reference the other members. In the example below, within the draw()
method, which is a prototype method, we calling the instance method move()
.
function Circle(radius) {
this.radius = radius;
this.move = function() {
console.log('move...');
}
}
Circle.prototype.draw = function() {
this.move();
console.log('Draw a circle with R=' + this.radius);
};
const circle1 = new Circle(10);
circle1.draw();
move...
Draw a circle with R=10
Iterating Instance (or Own) and Prototype Members
function Circle(radius) {
this.radius = radius; // Instance member
this.move = function() { // Instance member
console.log('move');
}
}
Circle.prototype.draw = function() { // Prototype member
console.log('draw');
};
const circle1 = new Circle(10);
console.log( Object.keys(circle1) );
(2) ['radius', 'move']
We can see Object.keys(circle1)
returns only the instance members radius
and move()
, draw()
is not there because it is a prototype member. In other hand the for.. in..
loop will return all the instance and prototype members.
for (let key in circle1) console.log(key);
radius
move
draw
Here is how to test whether a member is Own (instance) member or Prototype member.
circle1.hasOwnProperty('move'); // 'move' is instance (own) property
true
circle1.hasOwnProperty('draw'); // 'draw' is prototype property
false
Avoid Extending the Built-in Objects
JavaScript is a dynamic language and it is very easy to add methods and properties to existing objects, but that doesn't mean you should modify the built-in objects.
Array.prototype.shuffle = function() {
// ... the shuffle algorithm
};
const array = [];
array.shaffle();
That is something easy to accomplish in JavaScript but it's something you should avoid. You should not modify the built-in objects in JavaScript. For example, it is possible that tomorrow you have going to use an external library and that library also change the same things at the built-in objects but with different implementation – then you have to spent all day debugging the problem.