之前的JavaScript繼承一文中已經(jīng)介紹了繼承,但那篇只能算簡介砸狞。本篇結(jié)合原型鏈詳細介紹一下JavaScript的繼承阳似。
通常除非小應(yīng)用闷堡,那像JavaScript繼承一文中那樣直接寫寫代碼就行了隘膘。如果是大型應(yīng)用或者庫函數(shù),對于繼承這種稍顯復(fù)雜的代碼結(jié)構(gòu)杠览,通常會封裝成一個inherit函數(shù)弯菊。例如:
function Parent(n) { //父構(gòu)造函數(shù)
this.name = n || 'Adam';
}
Parent.prototype.say = function() {
return this.name;
}
function Child(n) {} //空白的子構(gòu)造函數(shù)
inherit(Child, Parent); //繼承
現(xiàn)在我們來實現(xiàn)inherit。
模式一:默認模式踱阿,將原型對象指向父對象
function inherit(Child, Parent) {
Child.prototype = new Parent(); //原型對象指向父對象
}
例子:
function Parent(n) { //父構(gòu)造函數(shù)
this.name = n || 'Adam';
}
Parent.prototype.say = function() {
return this.name;
}
function Child(n) {} //空白的子構(gòu)造函數(shù)
function inherit(Child, Parent) {
Child.prototype = new Parent(); //原型對象指向父對象
}
inherit(Child, Parent); //繼承
var c1 = new Child("Jack");
console.log(c1.name); //Adam
c1.say(); //Adam
原型鏈圖:
見上面的結(jié)果為Adam管钳。這就是該模式的缺點之一,即無法將子構(gòu)造函數(shù)的參數(shù)給父構(gòu)造函數(shù)软舌。這個缺點很致命才漆,因此通常我們不用該模式。即使你能保證父子構(gòu)造函數(shù)都不需要參數(shù)佛点,那從結(jié)果上看是OK的醇滥,但效率是低下的,例如你再創(chuàng)建一個子對象:
var c2 = new Child("Betty");
console.log(c2.name); //Adam
c2.say(); //Adam
兩個子對象c1和c2超营,都分別新建了一個父對象鸳玩,因此存在兩個父對象。這是該模式的缺點之二糟描,即每個子對象都會重復(fù)地創(chuàng)建父對象怀喉,效率不高书妻。
模式二:借用構(gòu)造函數(shù)
該方法解決了模式一中無法通過子構(gòu)造函數(shù)傳遞參數(shù)給父構(gòu)造函數(shù)的問題:
function Child(a, b, c, d) { //子構(gòu)造函數(shù)
Parent.apply(this, arguments); //借用父構(gòu)造函數(shù)
}
例子:
function Parent(n) { //父構(gòu)造函數(shù)
this.name = n || 'Adam';
}
Parent.prototype.say = function() {
return this.name;
}
function Child(n) { //子構(gòu)造函數(shù)
Parent.apply(this, arguments); //借用父構(gòu)造函數(shù)
}
var c2 = new Child("Patrick");
console.log(c2.name); //Patrick
c2.say(); //error,未定義
結(jié)果看出Child的參數(shù)順利傳入了船响,但say方法會報未定義的錯。原因就是該模式并沒有將prototype指向Parent躲履,只不過借用了一下Parent的實現(xiàn)见间。因此看似是繼承,其實不然工猜,從原型鏈角度來看米诉,兩者毫無關(guān)系。Child的實例對象里自然就沒有Parent原型中的say方法篷帅。圖示如下:
總結(jié)一下該模式:子類只是借用了父類構(gòu)造函數(shù)的實現(xiàn)史侣,從結(jié)果上看,獲得了一個父對象的副本魏身。但子類對象和父類對象是完全獨立的惊橱,不存在修改子類對象的屬性值影響父對象的風(fēng)險。缺點是該模式某種意義上講箭昵,其實不是繼承税朴,無法從父類的prototype中獲得任何東西
模式三:借用和設(shè)置原型
本模式是上面兩個模式的結(jié)合體,借鑒了上面兩種模式的特點:
function Child(a, b, c, d) { //子構(gòu)造函數(shù)
Parent.apply(this, arguments); //參照模式二,借用父構(gòu)造函數(shù)
}
Child.prototype = new Parent(); //參照模式一正林,將原型對象指向父對象
這就是JavaScript繼承一文中推薦的繼承模式泡一。子對象既可獲得父對象本身的成員副本,又能獲得原型的引用觅廓。子對象能傳參數(shù)給父構(gòu)造函數(shù)鼻忠,也能安全地修改自身屬性。
例子:
function Parent(n) { //父構(gòu)造函數(shù)
this.name = n || 'Adam';
}
Parent.prototype.say = function() {
return this.name;
}
function Child(name) { //子構(gòu)造函數(shù)
Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var c4 = new Child("Patrick");
console.log(c4.name); //Patrick
console.log(c4.say()); //Patrick
delete c4.name;
console.log(c4.say()); //Adam
該模式通常用用就可以了哪亿,但不是完美的粥烁。缺點和模式二的缺點二一樣,多個子對象都會重復(fù)地創(chuàng)建父對象蝇棉,效率不高讨阻。另外從例子的結(jié)果和圖中都可以看出,有兩個name屬性篡殷,一個在父對象中钝吮,一個在子對象中。你delete子對象中的name后板辽,父對象的name會顯現(xiàn)出來奇瘦,這可能會出bug。而且對效率狂來說劲弦,冗余的屬性會看著不舒服耳标。
模式四:共享原型
為了克服模式三需要重復(fù)創(chuàng)建父對象的缺點,該模式不調(diào)用構(gòu)造函數(shù)邑跪,即任何需要繼承的成員都放到原型里次坡,而不是放置在父構(gòu)造函數(shù)的this中。等價于對象共享一個原型
function inherit(Child, Parent) {
Child.prototype = Parent.prototype;
}
例子:
function Parent(n) { //父構(gòu)造函數(shù)
this.name = n || 'Adam';
}
Parent.prototype.say = function() {
return this.name;
}
function Child(n) {
this.name = n;
}
function inherit(Child, Parent) {
Child.prototype = Parent.prototype;
}
inherit(Child, Parent); //繼承
var c4 = new Child("Patrick");
console.log(c4.name); //Patrick
console.log(c4.say()); //Patrick
delete c4.name;
console.log(c4.say()); //undefined
從結(jié)果可以看出画畅,該模式和模式三不同砸琅,現(xiàn)在你delete子對象的name屬性,就不會將父對象的name屬性顯現(xiàn)出來了轴踱。原型鏈圖:
該模式除了需要你仔細斟酌哪些屬性和方法需要被繼承症脂,抽出來放到父類原型里。而且由于父子對象共享原型淫僻,因此雙方修改時都要小心诱篷,如果子對象不小心修改了原型里的屬性和方法,會影響到父對象雳灵,反之亦然棕所。例如:
Child.prototype.setName = function(n) {
return this.name = n;
}
c4.setName("Jack");
console.log(c4.name); //Jack
console.log(c4.say()); //Jack
var c5 = new Parent();
c5.setName("Betty");
console.log(c5.name); //Betty
console.log(c5.say()); //Betty
給子類原型增加一個setName方法。由于父子類共享原型细办,因此父類對象也自動獲得了setName方法橙凳。
模式五:臨時構(gòu)造函數(shù)
為解決模式四中父子對象間耦合度較高的缺點蕾殴,該模式斷開父子對象間的原型的直接鏈接關(guān)系,但同時還能繼續(xù)受益于原型鏈的好處
function inherit(Child, Parent) {
var F = function() {}; //空的臨時構(gòu)造函數(shù)
F.prototype = Parent.prototype;
Child.prototype = new F();
}
例子:
function Parent(n) { //父構(gòu)造函數(shù)
this.name = n || 'Adam';
}
Parent.prototype.say = function() {
return this.name;
}
function Child(n) {
this.name = n;
}
function inherit(Child, Parent) {
var F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
}
inherit(Child, Parent); //繼承
var c6 = new Child("Patrick");
console.log(c6.name); //Patrick
console.log(c6.say()); //Patrick
delete c6.name;
console.log(c6.say()); //undefined
原型鏈圖:
與模式四的差別就是岛啸,新定義了個空的臨時構(gòu)造函數(shù)F()钓觉,子類的原型指向該臨時構(gòu)造函數(shù)。這樣修改子類原型時坚踩,實際修改的是修改到了臨時構(gòu)造函數(shù)F()荡灾,不會影響父類:
Child.prototype.setName = function(n) {
return this.name = n;
}
c6.setName("Jack");
console.log(c6.name); //Jack
console.log(c6.say()); //Jack
var c7 = new Parent();
c7.setName("Betty"); //error,未定義
上面的例子和模式四中相同,但結(jié)果不同瞬铸,子類原型上添加的新方法setName批幌,父類對象無法訪問。
該模式非常好嗓节,即有效率荧缘,還能實現(xiàn)父子解耦。本著精益求精的精神拦宣,再為該模式增加三個加分項:
加分項一:添加一個指向父類原型的引用截粗,例如其他語言里的super:
function inherit(Child, Parent) {
var F = function() {}; //空的臨時構(gòu)造函數(shù)
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype; //uber表示super,因為super是保留的關(guān)鍵字
}
這樣如果你為子類原型添加setName方法后鸵隧,希望父類對象也能獲得該方法绸罗,可以:
function inherit(Child, Parent) {
var F = function() {}; //空的臨時構(gòu)造函數(shù)
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype; //uber表示super,因為super是保留的關(guān)鍵字
}
inherit(Child, Parent); //繼承
Child.prototype.setName1 = function(n) {
return this.name = n;
}
Child.uber.setName2 = function(n) {
return this.name = n;
}
var c8 = new Child("Patrick");
c8.setName1("Jack");
console.log(c8.name); //Jack
console.log(c8.say()); //Jack
c8.setName2("Betty");
console.log(c8.name); //Betty
console.log(c8.say()); //Betty
var c9 = new Parent();
c9.setName1("Andy"); //error豆瘫,未定義
c9.setName2("Andy");
console.log(c9.name); //Andy
console.log(c9.say()); //Andy
子類給原型的新增方法setName1不會影響父類珊蟀,父類對象無法使用setName1。但父類對象可以使用子類通過uber給原型的新增方法setName2外驱。
加分項二:重置該構(gòu)造函數(shù)的指針育灸,以免在將來某個時候還需要該構(gòu)造函數(shù)。如果不重置構(gòu)造函數(shù)的指針略步,那么所有子對象會報告Parent()是它們的構(gòu)造函數(shù)描扯,這沒有任何用處:
var c10 = new Child();
console.log(c10.constructor.name); //Parent
console.log(c10.constructor === Parent); //true
雖然我們很少用constructor屬性定页,不改也不影響實際的使用趟薄,但作為完美主義者還是改一下吧:
function inherit(Child, Parent) {
var F = function() {}; //空的臨時構(gòu)造函數(shù)
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype; //uber表示super,因為super是保留的關(guān)鍵字
Child.prototype.constructor = Child; //修正constructor屬性
}
inherit(Child, Parent); //繼承
var c11 = new Child();
console.log(c11.constructor.name); //Child
console.log(c11.constructor === Parent); //false
加分項三:臨時構(gòu)造函數(shù)F()不必每次繼承時都創(chuàng)建典徊,僅創(chuàng)建一次以提高效率:
var inherit = (function() {
var F = function() {};
return function(Child, Parent) {
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype;
Child.prototype.constructor = Child;
}
}());
模式五杭煎,你可以在開源的YUI庫,或其他庫中看到類似模式五的身影卒落。例如Klass
Klass
Klass是一種代碼結(jié)構(gòu)羡铲,模擬傳統(tǒng)OO語言的Class。繼承時能像傳統(tǒng)OO語言的Class一樣儡毕,子類構(gòu)造函數(shù)調(diào)用父類的構(gòu)造函數(shù)也切。作為一種代碼結(jié)構(gòu)扑媚,它有一套命名公約,如initialize雷恃,_init等疆股,創(chuàng)建對象時這些方法會被自動調(diào)用。
例如:
var klass = function (Parent, props) {
var Child, F, i;
//1.新構(gòu)造函數(shù)
Child = function (Parent, props) {
if(Child.uber && Child.uber.hasOwnProperty("__construct")) {
Child.uber.__construct.apply(this, arguments);
}
if(Child.prototype.hasOwnProperty("__construct")) {
Child.prototype.__construct.apply(this, arguments);
}
};
//2.繼承
Parent = Parent || Object;
F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.uber = Parent.prototype;
Child.prototype.constructor = Child;
//3.添加實現(xiàn)方法
for(i in props) {
if(props.hasOwnProperty(i)) {
Child.prototype[i] = props[i];
}
}
return Child;
};
看一下上面的Klass代碼結(jié)構(gòu)倒槐。它有兩個參數(shù)旬痹,分別是父類和子類需要擴展的字面量形式的屬性。
第一部分是為子類生成構(gòu)造函數(shù):如果父類存在構(gòu)造函數(shù)讨越,先調(diào)用父類構(gòu)造函數(shù)两残。如果子類存在構(gòu)造函數(shù),再調(diào)用子類構(gòu)造函數(shù)把跨。(由于PHP的影響人弓,一個潛規(guī)則是,類的構(gòu)造函數(shù)最好命名為__construct)着逐。在最后return出生成的構(gòu)造函數(shù)
第二部分是繼承票从,參照模式五,不贅述滨嘱。
第三部分是為子類添加需要擴展的屬性峰鄙。
現(xiàn)在我們的代碼中就可以不再糾結(jié)于用哪種模式來實現(xiàn)繼承了,直接用Klass太雨。
例如創(chuàng)建一個不繼承自任何類的新類:
var Person = klass(null, {
__construct: function (n) {
this.name = n;
},
sayHi: function() {
console.log("hi " + this.name);
}
});
var p1 = new Person("Jack");
p1.sayHi(); //hi Jack
上面代碼用klass創(chuàng)建了一個Person的新類吟榴,沒有繼承自任何類,意味著繼承Object類(源碼中的Parent = Parent || Object;
語句)囊扳。構(gòu)造函數(shù)里創(chuàng)建name屬性吩翻,并提供了一個sayHi的方法
現(xiàn)在擴充一個Man類:
var Man = klass(Person, {
__construct: function (n) {
console.log("I am a man.");
},
sayHi: function() {
Man.uber.sayHi.call(this);
}
});
var m1 = new Man("JackZhang"); //I am a man.
m1.sayHi(); //hi JackZhang
console.log(m1 instanceof Person); //true
console.log(m1 instanceof Man); //true
用庫的klass的話,雖然比較方便锥咸,讓JavaScript無比接近傳統(tǒng)OO語言狭瞎,讓新手也能快速上手進行開發(fā)。但其實不建議用klass搏予,因為讓容易讓人產(chǎn)生一種JavaScript也有類的錯覺熊锭,其實它只是一種模擬類的代碼結(jié)構(gòu),如果你對JavaScript的原型鏈不害怕的話雪侥,還是避免用klass比較好
原型繼承
上面介紹的五種模式和klass都屬于基于類型的繼承碗殷,在JavaScript繼承一文中還介紹了用Object.create()基于對象的繼承,也叫原型繼承速缨。用法很簡單锌妻,這里看一下它的本質(zhì)。
原型繼承不涉及類旬牲,對象都是繼承自其它對象仿粹,即要繼承的話搁吓,先要有一個父類對象:
function create(o) {
function F() {}
F.prototype = o;
return new F();
}
var parent = {
name: "Papa"
};
var child = create(parent);
console.log(child.name); //Papa
原型鏈圖:
也不是必須使用字面量來創(chuàng)建父對象(雖然用字面量比較常見),也可以用構(gòu)造函數(shù)來創(chuàng)建父對象吭历,這樣的話擎浴,自身的屬性和構(gòu)造函數(shù)的原型的屬性都將被繼承:
function Parent() {
this.name = "Papa";
}
Parent.prototype.getName = function() {
return this.name;
};
var papa = new Parent();
var child = create(papa);
console.log(child.name); //Papa
console.log(child.getName()); //Papa
不論用字面量還是構(gòu)造函數(shù)方式創(chuàng)建父對象都可以,甚至你可以只繼承父類的原型對象:
var child = create(Parent.prototype);
console.log(typeof child.name); //undefined
console.log(typeof child.getName); //function
ES5里定義成Object.create()毒涧,基于對象的繼承我們直接用該方法就行了贮预。
總結(jié)
如果基于對象繼承用Object.create()。如果基于類型繼承契讲,平時一些快速應(yīng)用仿吞,或小應(yīng)用,用模式三實現(xiàn)繼承就夠了捡偏。復(fù)雜應(yīng)用或大型程序用模式五唤冈。如果你做代碼庫,可以用模式五定義inherit函數(shù)银伟,或定義Klass你虹。