JavaScript OOP Course 4: Prototypical Inheritance

From WikiMLT
Revision as of 09:40, 18 June 2022 by Spas (talk | contribs) (Стадий: 6 [Фаза:Утвърждаване, Статус:Утвърден]; Категория:JavaScript)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Ref­er­ences

Cre­at­ing Your Own Pro­to­typ­i­cal In­her­i­tance

function Shape(color) {
    this.color = color;
}
Shape.prototype.duplicate = function() {
    console.log('duplicate');
};

const shape = new Shape('red');
Object.getPrototypeOf(shape);
{duplicate: ƒ, constructor: ƒ}
    duplicate: ƒ ()
    constructor: ƒ Shape(color)
    [[Prototype]]: Object

Now we want to in­her­it the du­pli­cate() method by an­oth­er type of ob­jects, like Cir­cle and Square.

function Circle(radius) {
    this.radius = radius;
}
Circle.prototype.draw = function() {
    console.log('draw');
};

const circle1 = new Circle(1);

Note the Con­struc­tor here is ƒ Circle(radius).

Object.getPrototypeOf(circle1);
{draw: ƒ, constructor: ƒ}
    draw: ƒ ()
    constructor: ƒ Circle(radius)
    [[Prototype]]: Object

Change the Pro­to­type of the Con­struc­tor and cre­ate a new Ob­ject at this Base.

Circle.prototype = Object.create(Shape.prototype);
const circle2 = new Circle(2);

Note now the Con­struc­tor is ƒ Shape(color).

Object.getPrototypeOf(circle2);
Shape {}
    [[Prototype]]: Object
        duplicate: ƒ ()
        constructor: ƒ Shape(color)
        [[Prototype]]: Object
circle2.duplicate();
duplicate

Re­set­ting the Con­struc­tor

As best prac­tice when we re­set the Pro­to­type make sure you re­set the con­struc­tor as well. Let's con­tin­ue by the last ex­am­ple, where we saw when the Pro­to­type was changed to Shape al­so the Con­struc­tor was changed to Shape, here is how to set it back to Cir­cle.

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
const circle3 = new Circle(3);

Note in the ex­am­ple be­low the Con­struc­tor is changed back to Cir­cle.

Object.getPrototypeOf(circle3);
Shape {constructor: ƒ}
    constructor: ƒ Circle(radius)
    [[Prototype]]: Object
        duplicate: ƒ ()
        constructor: ƒ Shape(color)
        [[Prototype]]: Object

Call­ing the Su­per Con­struc­tor

function Shape(color) {
    this.color = color;
}
Shape.prototype.duplicate = function() {
    console.log('duplicate');
};
function Circle(radius, color) {
    Shape.call(this, color);
    this.radius = radius;
}
Circle.prototype.draw = function() {
    console.log('draw');
};
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
const shape = new Shape('red');
const circle = new Circle(10, 'blue');
circle;
Circle {color: 'blue', radius: 10}
    color: "blue"
    radius: 10
    [[Prototype]]: Shape
        constructor: ƒ Circle(radius, color)
        [[Prototype]]: Object
            duplicate: ƒ ()
            constructor: ƒ Shape(color)
            [[Prototype]]: Object

In­ter­me­di­ate Func­tion In­her­i­tance

function Shape(color) {
    this.color = color;
}
Shape.prototype.duplicate = function() {
    console.log('duplicate');
};
function Square(size, color) {
    Shape.call(this, color);
    this.size = size;
}
Square.prototype = Object.create(Shape.prototype);
Square.prototype.constructor = Square;
const square = new Square(10, 'green');
square;
Square {color: 'green', size: 10}
    color: "green"
    size: 10
    [[Prototype]]: Shape
        constructor: ƒ Square(size, color)
        [[Prototype]]: Object
            duplicate: ƒ ()
            constructor: ƒ Shape(color)
            [[Prototype]]: Object

Now let's rewrite the above code and ex­tract the two lines that changes the Pro­to­type and the Con­struc­tor in a Func­tion that we can reuse mul­ti­ple times.

function extend(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

This ex­tend() func­tion is what we call In­ter­me­di­ate func­tion in­her­i­tance. Here is how to use it.

function Shape(color) {
    this.color = color;
}
Shape.prototype.duplicate = function() { console.log('duplicate'); };
function Circle(radius) {
    this.radius = radius;
}
extend(Circle, Shape);
function Square(size) {
    this.size = size;
}
extend(Square, Shape);
const circle = new Circle(10);
const square = new Square(10);
circle;
Circle {radius: 10}
    radius: 10
    [[Prototype]]: Shape
        constructor: ƒ Circle(radius)
        [[Prototype]]: Object
            duplicate: ƒ ()
            constructor: ƒ Shape(color)
            [[Prototype]]: Object
square;
Square {size: 10}
size: 10
    [[Prototype]]: Shape
        constructor: ƒ Square(size)
        [[Prototype]]: Object
            duplicate: ƒ ()
            constructor: ƒ Shape(color)
            [[Prototype]]: Object

Method Over­rid­ing

function extend(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Shape() { }
Shape.prototype.duplicate = function() { 
    console.log('duplicate'); 
};

function Circle() { }

extend(Circle, Shape);

const circle = new Circle();
circle;
Circle {}
    [[Prototype]]: Shape
    constructor: ƒ Circle()
    [[Prototype]]: Object
        duplicate: ƒ ()
        constructor: ƒ Shape()
        [[Prototype]]: Object
circle.duplicate();
duplicate

Now let's imag­ine the du­pli­cate() method should be­have dif­fer­ent­ly at Cir­cle ob­jects. In or­der to over­ride this method (or reim­ple­ment­ing a method on a child ob­ject) we need to put our de­c­la­ra­tion af­ter the re­set­ting the pro­to­type by the ex­tend() func­tion.

Circle.prototype.duplicate = function() { 
    console.log('duplicate circle'); 
};
circle.duplicate();
duplicate circle

Poly­mor­phism

Poly­mor­phism – many form. In the fol­low­ing ex­am­ple the poly­mor­phism man­i­fests in the du­pli­cate() method which is spe­cif­ic for each type of shape…

function extend(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Shape() { }
Shape.prototype.duplicate = function() { 
    console.log('duplicate'); 
};

function Circle() { }
extend(Circle, Shape);
Circle.prototype.duplicate = function() { 
    console.log('duplicate circle'); 
};

function Square() { }
extend(Square, Shape);
Square.prototype.duplicate = function() { 
    console.log('duplicate square'); 
};

Let's de­fine an ar­ray of shape ob­jects.

const shapes = [
    new Circle(),
    new Square()
];

Now we can it­er­ate over this ar­ray by us­ing for.. of.. loop in the fol­low­ing way.

for (let shape of shapes)
    shape.duplicate();
duplicate circle
duplicate square

When to Use In­her­i­tance

Figure 1. JavaScript In­her­i­tance vs Com­po­si­tion.
  • In­her­i­tance is great tool to solv­ing the prob­lem of code reuse. You have to be re­al­ly care­ful about us­ing it be­cause it can make your source code com­plex and frag­ile, so don't use in­her­i­tance just for the sake of us­ing it, es­pe­cial­ly in small projects. Keep it sim­ple and stu­pid.
  • Start with sim­ple ob­jects and then if you see num­ber of these ob­jects share sim­i­lar fea­tures then per­haps you can en­cap­su­late these fea­tures in­side of an gener­ic ob­ject and use in­her­i­tance.
  • But re­mem­ber in­her­i­tance is not the on­ly so­lu­tion that en­abled code reuse. There is an­oth­er tech­nique called Com­po­si­tion (that can be used for the same pur­pose). Fa­vor Com­po­si­tion over In­her­i­tance.
  • Avoid cre­at­ing in­her­i­tance hi­er­ar­chies, be­cause they are very frag­ile. If you want to use in­her­i­tance keep it to one lev­el – do not go than more one lev­el of in­her­i­tance.