js 實現(xiàn)繼承的方式有:
- 原型鏈繼承;
- 構(gòu)造函數(shù)繼承;
- 組合繼承(原型鏈繼承 + 構(gòu)造函數(shù)繼承)(最常用);
(原型式繼承;
寄生式繼承;) - 寄生組合繼承 (最佳方式)

原型鏈繼承
function Super () {
this.name = 'hhh';
this.arr = ['nb'];
}
function Sub () {}
Sub.prototype = new Super();
let sub1 = new Sub();
let sub2 = new Sub();
sub1.name = 'yyy';
sub1.arr.push('hehe');
console.log(sub1.name, sub1.arr); // 'yyy', ['nb', 'hehe']
console.log(sub2.name, sub2.arr); // 'hhh', ['nb', 'hehe']
-
實現(xiàn)的本質(zhì)是重寫原型對象,代之以一個新類型的實例。
Sub.prototype=new Super();從而使子類得以繼承父類的屬性和方法。
注意這里sub1.constructor === Super為 true。(Sub.prototype.__proto__ === Super.prototype實例屬性的查找自下往上進行查找的原則)
- 優(yōu)缺點:
優(yōu)點:簡單,易于實現(xiàn)。
缺點:
- 包含引用類型的原型屬性會被所有實例共享;(這也是為什么要在構(gòu)造函數(shù)中定義屬性,而不是在原型對象中定義屬性的原因)。從例子中可以看出實例 sub1 修改 arr 后,也會改變 實例 sub2.arr 的值。
- 創(chuàng)建子類實例時,無法向父類構(gòu)造函數(shù)傳參。實際上,應該說無法在不影響所有實例對象的情況下,給父類的構(gòu)造函數(shù)傳遞參數(shù)。
構(gòu)造函數(shù)繼承
function Super(name) {
this.name = name;
this.arr = ['nb'];
this.fun = function() {
// ...
}
}
function Sub(name) {
this.hobby = 'hhh';
Super.call(this, name);
// Super.call(this); 可傳參 可不傳
}
let sub1 = new Sub('xql');
let sub2 = new Sub('nnn');
sub1.name = 'xxx';
sub1.arr.push('hehe')
console.log(sub1.name, sub1.arr); // xxx, ['nb', 'hehe']
console.log(sub2.name, sub2.arr); // nnn, ['nb']
實現(xiàn)的本質(zhì):借用父類的構(gòu)造函數(shù)來增強子類實例。等于是把父類的實例屬性復制了一份給子類實例裝上了(完全沒有用到原型)
優(yōu)缺點:
優(yōu)點:
- 解決了子類實例共享父類引用屬性的問題;
- 創(chuàng)建子類實例時,可以向父類構(gòu)造函數(shù)傳參;
- 可以實現(xiàn)多繼承(call多個父類對象);
缺點:
- 方法都在構(gòu)造函數(shù)中定義,函數(shù)復用也就無從談起了。每個子類都有父類實例屬性副本。每個子類實例都持有父類的所有函數(shù)方法,太多了就會影響性能,內(nèi)存爆炸。。
- 只能繼承父類的實例屬性和方法,不能繼承父類原型屬性和方法。
- 實例并不是父類的實例,只是子類的實例。
組合繼承(原型鏈繼承 + 構(gòu)造函數(shù)繼承)?。?!
function Super(name) {
// 只在此處聲明基本屬性和引用屬性
this.name = name;
this. arr = ['nb'];
}
// 在原型處聲明函數(shù)
Super.prototype.fun1 = function() {}
Super.prototype.fun2 = function() {}
// ...
function Sub(name) {
this.hobby = 'hhh';
Super.call(this, name); // 核心 第二次調(diào)用父類構(gòu)造函數(shù)
}
Sub.prototype = new Super(); // 核心 第一次調(diào)用父類構(gòu)造函數(shù)
Sub.prototype.constructor = Sub; // 需要修復構(gòu)造函數(shù)的指向
Sub.prototype.fun11 = function() {}
let sub1 = new Sub('xql');
console.log(sub1);

- 實現(xiàn)的本質(zhì):把實例函數(shù)都放在原型對象上,以實現(xiàn)函數(shù)復用。同時還要保留借用構(gòu)造函數(shù)方式的優(yōu)點。(使用原型鏈實現(xiàn)對原型屬性和方法的繼承(一般不在原型上寫引用屬性),而通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承)。即在原型上定義方法實現(xiàn)了函數(shù)復用,又能保證每個實例都有它自己的屬性。
通過Super.call(this);繼承父類的基本屬性和引用屬性并保留能傳參的優(yōu)點;
通過Sub.prototype = new Super();繼承父類函數(shù),實現(xiàn)函數(shù)復用。
- 注:組合繼承需要修復構(gòu)造函數(shù)的指向。因為重寫了原型對象
Sub.prototype,這里Sub.prototype.constructor便指向了構(gòu)造函數(shù)Super,需要修復!??!
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
- 優(yōu)缺點:
優(yōu)點:
- 函數(shù)可復用;
- 可以繼承實例屬性/方法,也可以繼承原型屬性/方法;
- 既是子類的實例,也是父類的實例;
- 不存在引用屬性共享問題;
- 可傳參
ps: 修復了上述兩種方式的缺點。
缺點:子類原型上有一份多余的父類實例屬性;因為父類構(gòu)造函數(shù)被調(diào)用了兩次,生成了兩份;而子類實例上的那一份屏蔽了子類原型上的。又是內(nèi)存浪費。
第一次Super.call(this);語句從父類拷貝了一份父類實例屬性給子類作為子類的實例屬性;
第二次Sub.prototype = new Super();創(chuàng)建父類實例作為子類原型,此時這個父類實例就又有了一份實例屬性,但這份會被第一次拷貝來的實例屬性屏蔽掉,所以多余。
即子類原型上的這份父類實例永遠用不到,被子類實例上的父類實例覆蓋掉了。
原型式繼承(對象的淺復制)
function object(obj) {
function F() {} // 臨時的構(gòu)造函數(shù)
F.prototype = obj;
return new F();
}
- 實現(xiàn)的本質(zhì):借助原型,基于已有的對象創(chuàng)建一個新對象(相當于對這個對象進行一次淺復制),不必創(chuàng)建自定義類型了(構(gòu)造函數(shù)),得到一個‘純潔’的新對象(沒有任何實例屬性,可看下面的例子)。(類似簡化版的原型鏈繼承)
ES5提供了Object.create()函數(shù),內(nèi)部就是原型式繼承,IE9+支持
let person = {
name: 'xql',
friends: ['bb', 'jj', 'hh', 'yy', 'nn']
}
let per1 = object(person);
// let per1 = Object.create(person);
let per2 = object(person);
// let per2 = Object.create(person);
per1.name = 'www';
per1.age = '18'; // 增強
per1.friends.push('xxx');
console.log(per1); // {name: "www", age: "18"}
console.log(per2); // {} 純潔的新對象
console.log(per1.name, per1.friends); // www, ["bb", "jj", "hh", "yy", "nn", "xxx"]
console.log(per2.name, per2.friends); // xql, ["bb", "jj", "hh", "yy", "nn", "xxx"]
- 優(yōu)缺點:
優(yōu)點: 從已有對象衍生新對象,不需要創(chuàng)建自定義類型(更像是對象復制,而不是繼承。)
缺點:和原型鏈繼承一樣
- 原型引用屬性會被所有實例共享;
- 無法實現(xiàn)代碼復用;
寄生式繼承(穿個馬甲)
function getSubObject(obj) {
let clone = Object.create(obj);
clone.attr1 = 1;
clone.attr2 = 2;
// ...
return clone;
}
let person = {
name: 'xql';
friends: ['nn'];
}
let per1 = getSubObject(person);
console.log(per1); // {attr1: 1, attr2: 2}
- 實現(xiàn)的本質(zhì): 給原型式繼承穿了個馬甲而已,看起來比較像繼承。同樣是基于某個對象或某個信息創(chuàng)建一個對象,然后增強對象,最后返回對象。
- 優(yōu)缺點:
同原型式繼承。
ps: 有缺陷的寄生式繼承 + 不完美的組合繼承 = 完美的寄生組合式繼承
寄生組合繼承
組合繼承最大的問題是,無論什么情況下,都會調(diào)用兩次父類構(gòu)造函數(shù):一次是在創(chuàng)建子類原型時(實現(xiàn)方法的繼承);另一次是在子類構(gòu)造函數(shù)內(nèi)部(實現(xiàn)屬性的繼承)。(第二次子類實例調(diào)用構(gòu)造函數(shù),會覆蓋掉第一次子類原型調(diào)用構(gòu)造函數(shù)生成的實例屬性,使子類原型上的父類構(gòu)造函數(shù)執(zhí)行生成的實例屬性永遠排不上用場,占用了多余的內(nèi)存)。
怎么解決呢?
- 所謂寄生組合繼承的實現(xiàn)本質(zhì):不必為了指定子類構(gòu)造函數(shù)的原型而調(diào)用父類構(gòu)造函數(shù),我們所需要的無非就是父類構(gòu)造函數(shù)的原型的一個副本而已。為了繼承父類實例的方法(父類構(gòu)造函數(shù)的原型上的方法),使用寄生式繼承來繼承父類構(gòu)造函數(shù)的原型。
/**
* @param {function} 子類構(gòu)造函數(shù)
* @param {function} 父類構(gòu)造函數(shù)
*/
function inherit(SubType, SuperType) {
let proto = Object.create(SuperType.prototype);
// let proto = object(SuperType.prototype);
// object函數(shù)見上 原型式繼承
proto.constructor = SubType;
SubType.prototype = proto;
}
function Super(name) {
this.name = name;
this.friends = ['nnn'];
}
Super.prototype.fun1 = function() {}
Super.prototype.fun2 = function() {}
// ...
function Sub(name) {
this.hobby = 'hhh';
Super.call(this, name);
}
inherit(Sub, Super); // 核心
Sub.prototype.fun11 = function() {}
// ...
let sub1 = new Sub('xql');
console.log(sub1);

- 優(yōu)缺點:
優(yōu)點:同組合繼承。但解決了組合繼承的缺點,只調(diào)用了一次父類構(gòu)造函數(shù),避免了在子類構(gòu)造函數(shù)的原型上創(chuàng)建不必要的、多余的屬性。
缺點:理論上沒有。