Skip to main content

Revisiting the 6 Inheritance Patterns in JavaScript

Free2015-05-02#JS#js继承

This article provides a detailed analysis of simple prototype chain inheritance, constructor borrowing inheritance, combination inheritance, prototypal inheritance, parasitic inheritance, and parasitic combination inheritance, along with the advantages, disadvantages, and relationships of each inheritance pattern

Preface

I've never been fond of JavaScript's OOP. During the learning phase, it seemed unnecessary, and I always felt JavaScript's OOP was somewhat awkward. Perhaps because I encountered Java first, I had some resistance toward JavaScript's OO aspects.

Bias aside, since interviewers ask about JavaScript's OOP, it must be useful. I should set aside my biases and seriously learn about it.

Conventions

P.S. The story below is somewhat long, so it's necessary to establish common language in advance:

/*
 * Conventions
 */
function Fun(){
    // Private properties
    var val = 1;        // Private primitive property
    var arr = [1];      // Private reference property
    function fun(){}    // Private function (reference property)
    
    // Instance properties
    this.val = 1;               // Instance primitive property
    this.arr = [1];             // Instance reference property
    this.fun = function(){};    // Instance function (reference property)
}
 
// Prototype properties
Fun.prototype.val = 1;              // Prototype primitive property
Fun.prototype.arr = [1];            // Prototype reference property
Fun.prototype.fun = function(){};   // Prototype function (reference property)

The conventions above should be relatively reasonable. If you find them difficult to understand, you can check ayqy: JS Learning Notes 2_Object-Oriented to learn more basic common sense.

I. Simple Prototype Chain

This is the simplest way to implement inheritance, really super simple, with just one core line (marked with a comment in the code)

1. Implementation

function Super(){
    this.val = 1;
    this.arr = [1];
}
function Sub(){
    // ...
}
Sub.prototype = new Super();    // Core

var sub1 = new Sub();
var sub2 = new Sub();
sub1.val = 2;
sub1.arr.push(2);
alert(sub1.val);    // 2
alert(sub2.val);    // 1
 
alert(sub1.arr);    // 1, 2
alert(sub2.arr);    // 1, 2

2. Core

Use a parent class instance as the child class prototype object

3. Advantages and Disadvantages

Advantages:

  1. Simple and easy to implement

Disadvantages:

  1. After modifying sub1.arr, sub2.arr also changes, because reference properties from the prototype object are shared among all instances.

It can be understood this way: executing sub1.arr.push(2); first searches for properties on sub1, searches through all instance properties (there are none in this example), doesn't find it, then starts searching up the prototype chain, finds sub1's prototype object, searches it, and discovers the arr property. So it inserts 2 at the end of arr, which is why sub2.arr also changes.

  1. When creating child class instances, cannot pass parameters to the parent class constructor

II. Constructor Borrowing

Simple prototype chain is indeed simple, but having 2 fatal flaws makes it practically unusable. So jsers at the end of the last century figured out how to fix these 2 defects, leading to the constructor borrowing pattern.

1. Implementation

function Super(val){
    this.val = val;
    this.arr = [1];
    
    this.fun = function(){
        // ...
    }
}
function Sub(val){
    Super.call(this, val);   // Core
    // ...
}

var sub1 = new Sub(1);
var sub2 = new Sub(2);
sub1.arr.push(2);
alert(sub1.val);    // 1
alert(sub2.val);    // 2

alert(sub1.arr);    // 1, 2
alert(sub2.arr);    // 1

alert(sub1.fun === sub2.fun);   // false

2. Core

Borrow the parent class constructor to enhance child class instances, essentially copying the parent class's instance properties to the child class instances (completely not using the prototype)

3. Advantages and Disadvantages

Advantages:

  1. Solves the problem of child class instances sharing parent class reference properties

  2. When creating child class instances, can pass parameters to the parent class constructor

P.S. Our predecessors were so efficient, fixing both defects instantly.

Disadvantages:

  1. Cannot achieve function reuse; each child class instance holds a new fun function. Too many will affect performance, memory explosion...

P.S. Well, just fixed the shared reference property problem, and now this new problem appears...

III. Combination Inheritance (Most Commonly Used)

Our constructor borrowing pattern still has problems (cannot achieve function reuse). No problem, let's continue fixing it. jsers worked hard and came up with combination inheritance.

1. Implementation

function Super(){
    // Only declare primitive and reference properties here
    this.val = 1;
    this.arr = [1];
}
//  Declare functions here
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
    Super.call(this);   // Core
    // ...
}
Sub.prototype = new Super();    // Core

var sub1 = new Sub(1);
var sub2 = new Sub(2);
alert(sub1.fun === sub2.fun);   // true

2. Core

Put all instance functions on the prototype object to achieve function reuse. At the same time, retain the advantages of the constructor borrowing pattern. Inherit parent class primitive and reference properties through Super.call(this); and retain the ability to pass parameters; inherit parent class functions through Sub.prototype = new Super(); to achieve function reuse.

3. Advantages and Disadvantages

Advantages:

  1. No reference property sharing issues
  2. Can pass parameters
  3. Functions can be reused

Disadvantages:

  1. (A minor flaw) There's an extra copy of parent class instance properties on the child class prototype, because the parent class constructor is called twice, generating two copies, and the one on the child class instance shadows the one on the child class prototype... Another memory waste, though better than the previous situation, it's indeed a flaw.

P.S. If you can't understand this "extra", you can check ayqy: JS Learning Notes 2_Object-Oriented, there's a more detailed explanation at the end of the article.

IV. Parasitic Combination Inheritance (Best Pattern)

From the name, you can tell it's another optimization of combination inheritance. Didn't we say combination inheritance has flaws? No problem, let's continue pursuing perfection.

1. Implementation

function beget(obj){   // Child-bearing function beget: dragon begets dragon, phoenix begets phoenix.
    var F = function(){};
    F.prototype = obj;
    return new F();
}
function Super(){
    // Only declare primitive and reference properties here
    this.val = 1;
    this.arr = [1];
}
//  Declare functions here
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
    Super.call(this);   // Core
    // ...
}
var proto = beget(Super.prototype); // Core
proto.constructor = Sub;            // Core
Sub.prototype = proto;              // Core

var sub = new Sub();
alert(sub.val);
alert(sub.arr);

P.S. Wait, what's this child-bearing function? Never heard of it? And those 3 lines marked as core, why don't I understand them? Don't worry, let's have a cup of tea and continue reading.

2. Core

Use beget(Super.prototype); to cut off the extra parent class instance properties on the prototype object

P.S. What? Didn't understand? Oh oh~ Forgot to talk about prototypal and parasitic inheritance, no wonder I kept feeling like I forgot to lock the door... What a memory.

P.S. Parasitic combination inheritance, this name isn't very appropriate, the relationship with parasitic inheritance isn't particularly strong.

3. Advantages and Disadvantages

Advantages: Perfect

Disadvantages: Theoretically none (unless using it being troublesome counts as a disadvantage...)

P.S. Being troublesome to use is one thing, on the other hand, parasitic combination inheritance appeared relatively late, it's from the early 21st century, people couldn't wait that long, so combination inheritance is the most commonly used, while this theoretically perfect pattern is just the textbook's best approach.

V. Prototypal

Actually, we could end after introducing the perfect pattern above, but there seems to be a significant conceptual jump from combination inheritance to the perfect pattern, so it's necessary to tell the story clearly.

1. Implementation

function beget(obj){   // Child-bearing function beget: dragon begets dragon, phoenix begets phoenix.
    var F = function(){};
    F.prototype = obj;
    return new F();
}
function Super(){
    this.val = 1;
    this.arr = [1];
}

// Get parent class object
var sup = new Super();
// Bear child
var sub = beget(sup);   // Core
// Enhance
sub.attr1 = 1;
sub.attr2 = 2;
//sub.attr3...

alert(sub.val);     // 1
alert(sub.arr);     // 1
alert(sub.attr1);   // 1

P.S. Hey~ See that, the child-bearing function beget appears.

2. Core

Use the child-bearing function to get a "pure" new object ("pure" because it has no instance properties), then gradually enhance it (fill in instance properties)

P.S. ES5 provides the Object.create() function, internally using prototypal inheritance, supported by IE9+

3. Advantages and Disadvantages

Advantages:

  1. Derive new objects from existing objects, no need to create custom types (more like object copying than inheritance...)

Disadvantages:

  1. Prototype reference properties are shared among all instances, because the entire parent class object is used as the child class prototype object, so this defect is unavoidable.

  2. Cannot achieve code reuse (the new object is taken as-is, properties are added on the spot, none are encapsulated in functions, how to reuse)

P.S. Does this have much to do with inheritance? Why did Nicholas list it as a way to implement inheritance? Not much relation, but there is some relation.

VI. Parasitic

This name is too far-fetched, and parasitic is a pattern (approach), not only used to implement inheritance.

1. Implementation

function beget(obj){   // Child-bearing function beget: dragon begets dragon, phoenix begets phoenix.
    var F = function(){};
    F.prototype = obj;
    return new F();
}
function Super(){
    this.val = 1;
    this.arr = [1];
}
function getSubObject(obj){
    // Create new object
    var clone = beget(obj); // Core
    // Enhance
    clone.attr1 = 1;
    clone.attr2 = 2;
    //clone.attr3...
    
    return clone;
}

var sub = getSubObject(new Super());
alert(sub.val);     // 1
alert(sub.arr);     // 1
alert(sub.attr1);   // 1

2. Core

Just put a disguise on prototypal inheritance, looks more like inheritance (the prototypal inheritance introduced above is more like object copying).

Note: The beget function is not necessary. In other words, the process of creating a new object -> enhancing -> returning that object is called parasitic inheritance. How the new object is created doesn't matter (born via beget, new'd, made with object literal... all work).

3. Advantages and Disadvantages

Advantages:

  1. Still don't need to create custom types

Disadvantages:

  1. Cannot achieve function reuse (didn't use prototype, of course not possible)

P.S. Plot analysis: Flawed parasitic inheritance + Imperfect combination inheritance = Perfect parasitic combination inheritance. Feel free to go back and find where parasitic is used.

VII. Relationships Among the 6 Inheritance Patterns

Inheritance

P.S. Dashed lines indicate auxiliary role, solid lines indicate decisive role.

References

  • "JavaScript: The Good Parts"

  • "JavaScript: The Definitive Guide"

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment