JavaScript繼承詳解(Klass)

之前的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你虹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市彤避,隨后出現(xiàn)的幾起案子傅物,更是在濱河造成了極大的恐慌,老刑警劉巖琉预,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件董饰,死亡現(xiàn)場離奇詭異,居然都是意外死亡圆米,警方通過查閱死者的電腦和手機卒暂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來娄帖,“玉大人也祠,你說我怎么就攤上這事〗伲” “怎么了诈嘿?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長数焊。 經(jīng)常有香客問我永淌,道長崎场,這世上最難降的妖魔是什么佩耳? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮谭跨,結(jié)果婚禮上干厚,老公的妹妹穿的比我還像新娘李滴。我一直安慰自己,他們只是感情好蛮瞄,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布所坯。 她就那樣靜靜地躺著,像睡著了一般挂捅。 火紅的嫁衣襯著肌膚如雪芹助。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天闲先,我揣著相機與錄音状土,去河邊找鬼。 笑死伺糠,一個胖子當著我的面吹牛蒙谓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播训桶,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼累驮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了舵揭?” 一聲冷哼從身側(cè)響起谤专,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎午绳,沒想到半個月后毒租,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡箱叁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年墅垮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耕漱。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡算色,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出螟够,到底是詐尸還是另有隱情灾梦,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布妓笙,位于F島的核電站若河,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寞宫。R本人自食惡果不足惜萧福,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望辈赋。 院中可真熱鬧鲫忍,春花似錦膏燕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弱恒。三九已至箩做,卻和暖如春田晚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背智润。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工银锻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人做鹰。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓击纬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钾麸。 傳聞我的和親對象是個殘疾皇子更振,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1.繼承(接口繼承和實現(xiàn)繼承) 繼承是 OO 語言中的一個最為人津津樂道的概念。許多 OO 語言都支持兩種繼承方式...
    believedream閱讀 939評論 0 3
  • 繼承 Javascript中繼承都基于兩種方式:1.通過原型鏈繼承饭尝,通過修改子類原型的指向肯腕,使得子類實例通過原型鏈...
    LeoCong閱讀 293評論 0 0
  • 1、構(gòu)造函數(shù)模式 [url=]file:///C:/Users/i037145/AppData/Local/Tem...
    橫沖直撞666閱讀 843評論 0 0
  • 在今天我還在做直播的時候钥平,有水友來到我的直播間問了我一個問題实撒,主播,你感覺在明天we會贏嗎涉瘾? 在這里我只是想這樣回...
    黃銅刀閱讀 175評論 0 0
  • 生活中有這樣的一個現(xiàn)象:作為傾聽者知态,對于演講者表達的一個意思,不同的人立叛,會有不同的理解负敏,更有甚者會得出截然相反的觀...
    丁昆朋閱讀 609評論 6 2