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:
- Simple and easy to implement
Disadvantages:
- 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.
- 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:
-
Solves the problem of child class instances sharing parent class reference properties
-
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:
- 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:
- No reference property sharing issues
- Can pass parameters
- Functions can be reused
Disadvantages:
- (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:
- Derive new objects from existing objects, no need to create custom types (more like object copying than inheritance...)
Disadvantages:
-
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.
-
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:
- Still don't need to create custom types
Disadvantages:
- 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
P.S. Dashed lines indicate auxiliary role, solid lines indicate decisive role.
References
-
"JavaScript: The Good Parts"
-
"JavaScript: The Definitive Guide"

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