JavaScript OOP Course 3: Object Prototypes

From WikiMLT

Ref­er­ences

Figure 1. Clas­si­cal De­f­i­n­i­tion of In­her­i­tance.
Figure 2. Pro­to­types and In­her­i­tance in JavaScript.

In­her­i­tance

In­her­i­tance is one of the core con­cepts of Ob­ject Ori­ent­ed Pro­gram­ming that en­abled an Ob­ject to take on the Prop­er­ties and Meth­ods of an­oth­er Ob­ject. This makes it easy to reuse code in dif­fer­ent parts of an ap­pli­ca­tion.

In the terms of Class­es:

  • Base Class === Su­per Class === Par­ent Class (i.e. Shape)
  • De­rived Class === Sub Class === Child Class (i.e. Cir­cle and Square)
  • IS‑A Re­la­tion­ship (i.e. Cir­cle is a Shape)

"In JavaScript we not have class­es, on­ly have Ob­jects" that's when Pro­to­typ­i­cal In­her­i­tance comes to the pic­ture. Es­sen­tial­ly we have two types of In­her­i­tance: Clas­si­cal and Pro­to­typ­i­cal.

Pro­to­types

A pro­to­type in JavaScript is just a reg­u­lar ob­ject in the mem­o­ry. Pro­to­type is es­sen­tial­ly a par­ent of an­oth­er ob­ject. When you heard a pro­to­type just think par­ent.

Every ob­ject in JavaScript has a pro­to­type or par­ent ex­cept the ROOT Ob­ject. Every ob­ject in JavaScript ac­cept on­ly a sin­gle ob­ject as a pro­to­type or par­ent. And it in­her­its all the mem­bers de­fined on this pro­to­type.

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 ob­ject x has Pro­to­type Ob­ject and Con­struc­tor func­tion ƒ Ob­ject().

obj.constructor;
ƒ Object() { [native code] }

On­ly the ROOT Ob­ject in JavaScript doesn't have a pro­to­type or par­ent and it is a sin­gle in­stance. Look at the ex­am­ple be­low – both x and y have the ex­act same pro­to­type.

let x = {};
let y = {};
Object.getPrototypeOf(x);
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
Object.getPrototypeOf(x) === Object.getPrototypeOf(y);
true
Figure 3. Pro­to­types and Mul­ti­level In­her­i­tance in Ja­va­Script.

Pro­to­typ­i­cal In­her­i­tance

When we ac­cess­ing a prop­er­ty or a method of an ob­ject, JavaScript en­gine first looks for that prop­er­ty or method on the ob­ject it­self. And if the en­gine can't find that mem­ber at this lev­el, then it loos at the pro­to­type of that ob­ject.

When ac­cess­ing a prop­er­ty or a method of an ob­ject, JavaScript en­gine loads the pro­to­type chain to find the tar­get mem­ber.

Mul­ti­level In­her­i­tance

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__()

Ob­jects cre­at­ed by giv­en Con­struc­tor will have the same Pro­to­type.

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

Prop­er­ty De­scrip­tors

let person = {name: "Spas"};

The new ob­ject per­son has mul­ti­ple mem­bers (prop­er­ties and meth­ods) in­her­it­ed by its pro­to­type – for ex­am­ple .toString() method – but if we gonna it­er­ate over the per­son mem­bers we not go­ing to see these in­her­it­ed mem­bers.

person.toString();
'[object Object]'
for (let key in person) 
    console.log(key);
name
console.log(Object.keys(person));
['name']

The rea­son why we don't see these in­her­it­ed mem­bers is that in JavaScript the Prop­er­ties (and the Meth­ods) has At­trib­ut­es at­tached to them. Some­times these at­trib­ut­es pre­vent a prop­er­ty have been enu­mer­at­ed. Here is an ex­am­ple:

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 avail­able Prop­er­ty De­scrip­tors are:

  • con­fig­urable: true – that means we can delete this mem­ber if we want to.
  • enu­mer­able: false – that means (that's why) we can't it­er­ate over this mem­ber.
  • writable: true – that means we can over­ride this method (mem­ber).
  • val­ue: ƒ toString() – is the im­ple­men­ta­tion of the method of this par­tic­u­lar prop­er­ty.

Set Prop­er­ty De­scrip­tors. When we cre­at­ing our own ob­jects we can set these at­trib­ut­es. Here is an ex­am­ple.

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

Con­struc­tor Pro­to­types

We can get the pro­to­type of an ob­ject by the fol­low­ing com­mand.

let obj = {name: "Spas"};
Object.getPrototypeOf(obj);
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

The Con­struc­tors al­so have pro­to­type prop­er­ty.

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__: ƒ, …}

Pro­to­type vs In­stance Mem­bers

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 ex­am­ple we have Cir­cle con­struc­tor with two mem­bers – ra­dius and draw(). Each time when we cre­ate an in­stance by us­ing the new op­er­a­tor we cre­at­ing a copy of all mem­ber de­f­i­n­i­tions de­spite in some case the code is iden­ti­cal – lake it is for the draw() method. This con­sume ad­di­tion­al re­sources – as RAM, etc. – es­pe­cial­ly when there are large num­ber of such mem­bers – imag­ine 50 meth­ods with­in 500 in­stances of the ob­ject… In such cas­es we can de­fine these mem­bers at the lev­el of the pro­to­type of the Con­struc­tor.

function Circle(radius) {
    this.radius = radius;
}

Circle.prototype.draw = function() {
    console.log('Draw a circle with R=' + this.radius);
};

De­fined at the pro­to­type lev­el, the method will be in­her­it­ed by the in­stances, but won't be their mem­ber. It doesn't mat­ter when we change the pro­to­type (be­fore defin­ing a new ob­ject or lat­er af­ter that). The new method will be avail­able to the ob­ject be­cause here (in JavaScript) we deal­ing with ob­ject ref­er­ences (and pro­to­typ­i­cal in­her­i­tance, in­stead of re­al class­es), so we have a sin­gle ob­ject in mem­o­ry and as soon as we mod­i­fy that (the pro­to­type) all the changes are im­me­di­ate­ly vis­i­ble.

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 over­ride the im­ple­men­ta­tion of the de­fault meth­ods of the pro­to­type. For ex­am­ple 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'

Es­sen­tial­ly we have Two kinds of meth­ods and prop­er­ties in JavaScript:

  • In­stance mem­bers and
  • Pro­to­type mem­bers.

In both of these kind of mem­bers we can ref­er­ence the oth­er mem­bers. In the ex­am­ple be­low, with­in the draw() method, which is a pro­to­type method, we call­ing the in­stance 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

It­er­at­ing In­stance (or Own) and Pro­to­type Mem­bers

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) re­turns on­ly the in­stance mem­bers ra­dius and move(), draw() is not there be­cause it is a pro­to­type mem­ber. In oth­er hand the for.. in.. loop will re­turn all the in­stance and pro­to­type mem­bers.

for (let key in circle1) console.log(key);
radius
move
draw

Here is how to test whether a mem­ber is Own (in­stance) mem­ber or Pro­to­type mem­ber.

circle1.hasOwnProperty('move');  // 'move' is instance (own) property
true
circle1.hasOwnProperty('draw');  // 'draw' is prototype property
false

Avoid Ex­tend­ing the Built-in Ob­jects

JavaScript is a dy­nam­ic lan­guage and it is very easy to add meth­ods and prop­er­ties to ex­ist­ing ob­jects, but that doesn't mean you should mod­i­fy the built-in ob­jects.

Array.prototype.shuffle = function() {
    // ... the shuffle algorithm
};
const array = [];
array.shaffle();

That is some­thing easy to ac­com­plish in JavaScript but it's some­thing you should avoid. You should not mod­i­fy the built-in ob­jects in JavaScript. For ex­am­ple, it is pos­si­ble that to­mor­row you have go­ing to use an ex­ter­nal li­brary and that li­brary al­so change the same things at the built-in ob­jects but with dif­fer­ent im­ple­men­ta­tion – then you have to spent all day de­bug­ging the prob­lem.