JavaScript繼承詳解(Klass)

之前的JavaScript繼承一文中已經(jīng)介紹了繼承,但那篇只能算簡(jiǎn)介。本篇結(jié)合原型鏈詳細(xì)介紹一下JavaScript的繼承。

通常除非小應(yīng)用,那像JavaScript繼承一文中那樣直接寫寫代碼就行了。如果是大型應(yīng)用或者庫函數(shù),對(duì)于繼承這種稍顯復(fù)雜的代碼結(jié)構(gòu),通常會(huì)封裝成一個(gè)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)在我們來實(shí)現(xiàn)inherit。

模式一:默認(rèn)模式,將原型對(duì)象指向父對(duì)象

function inherit(Child, Parent) {
    Child.prototype = new Parent(); //原型對(duì)象指向父對(duì)象
}

例子:

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(); //原型對(duì)象指向父對(duì)象
}

inherit(Child, Parent); //繼承

var c1 = new Child("Jack");
console.log(c1.name);   //Adam
c1.say();               //Adam

原型鏈圖:

見上面的結(jié)果為Adam。這就是該模式的缺點(diǎn)之一,即無法將子構(gòu)造函數(shù)的參數(shù)給父構(gòu)造函數(shù)。這個(gè)缺點(diǎn)很致命,因此通常我們不用該模式。即使你能保證父子構(gòu)造函數(shù)都不需要參數(shù),那從結(jié)果上看是OK的,但效率是低下的,例如你再創(chuàng)建一個(gè)子對(duì)象:

var c2 = new Child("Betty");
console.log(c2.name);   //Adam
c2.say();               //Adam

兩個(gè)子對(duì)象c1和c2,都分別新建了一個(gè)父對(duì)象,因此存在兩個(gè)父對(duì)象。這是該模式的缺點(diǎn)之二,即每個(gè)子對(duì)象都會(huì)重復(fù)地創(chuàng)建父對(duì)象,效率不高。

模式二:借用構(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方法會(huì)報(bào)未定義的錯(cuò)。原因就是該模式并沒有將prototype指向Parent,只不過借用了一下Parent的實(shí)現(xiàn)。因此看似是繼承,其實(shí)不然,從原型鏈角度來看,兩者毫無關(guān)系。Child的實(shí)例對(duì)象里自然就沒有Parent原型中的say方法。圖示如下:


總結(jié)一下該模式:子類只是借用了父類構(gòu)造函數(shù)的實(shí)現(xiàn),從結(jié)果上看,獲得了一個(gè)父對(duì)象的副本。但子類對(duì)象和父類對(duì)象是完全獨(dú)立的,不存在修改子類對(duì)象的屬性值影響父對(duì)象的風(fēng)險(xiǎn)。缺點(diǎn)是該模式某種意義上講,其實(shí)不是繼承,無法從父類的prototype中獲得任何東西

模式三:借用和設(shè)置原型

本模式是上面兩個(gè)模式的結(jié)合體,借鑒了上面兩種模式的特點(diǎn):

function Child(a, b, c, d) {          //子構(gòu)造函數(shù)
    Parent.apply(this, arguments);  //參照模式二,借用父構(gòu)造函數(shù)
}
Child.prototype = new Parent();     //參照模式一,將原型對(duì)象指向父對(duì)象

這就是JavaScript繼承一文中推薦的繼承模式。子對(duì)象既可獲得父對(duì)象本身的成員副本,又能獲得原型的引用。子對(duì)象能傳參數(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

該模式通常用用就可以了,但不是完美的。缺點(diǎn)和模式二的缺點(diǎn)二一樣,多個(gè)子對(duì)象都會(huì)重復(fù)地創(chuàng)建父對(duì)象,效率不高。另外從例子的結(jié)果和圖中都可以看出,有兩個(gè)name屬性,一個(gè)在父對(duì)象中,一個(gè)在子對(duì)象中。你delete子對(duì)象中的name后,父對(duì)象的name會(huì)顯現(xiàn)出來,這可能會(huì)出bug。而且對(duì)效率狂來說,冗余的屬性會(huì)看著不舒服。

模式四:共享原型

為了克服模式三需要重復(fù)創(chuàng)建父對(duì)象的缺點(diǎn),該模式不調(diào)用構(gòu)造函數(shù),即任何需要繼承的成員都放到原型里,而不是放置在父構(gòu)造函數(shù)的this中。等價(jià)于對(duì)象共享一個(gè)原型

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子對(duì)象的name屬性,就不會(huì)將父對(duì)象的name屬性顯現(xiàn)出來了。原型鏈圖:


該模式除了需要你仔細(xì)斟酌哪些屬性和方法需要被繼承,抽出來放到父類原型里。而且由于父子對(duì)象共享原型,因此雙方修改時(shí)都要小心,如果子對(duì)象不小心修改了原型里的屬性和方法,會(huì)影響到父對(duì)象,反之亦然。例如:

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

給子類原型增加一個(gè)setName方法。由于父子類共享原型,因此父類對(duì)象也自動(dòng)獲得了setName方法。

模式五:臨時(shí)構(gòu)造函數(shù)

為解決模式四中父子對(duì)象間耦合度較高的缺點(diǎn),該模式斷開父子對(duì)象間的原型的直接鏈接關(guān)系,但同時(shí)還能繼續(xù)受益于原型鏈的好處

function inherit(Child, Parent) {
    var F = function() {};      //空的臨時(shí)構(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è)空的臨時(shí)構(gòu)造函數(shù)F(),子類的原型指向該臨時(shí)構(gòu)造函數(shù)。這樣修改子類原型時(shí),實(shí)際修改的是修改到了臨時(shí)構(gòu)造函數(shù)F(),不會(huì)影響父類:

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,父類對(duì)象無法訪問。

該模式非常好,即有效率,還能實(shí)現(xiàn)父子解耦。本著精益求精的精神,再為該模式增加三個(gè)加分項(xiàng):

加分項(xiàng)一:添加一個(gè)指向父類原型的引用,例如其他語言里的super:

function inherit(Child, Parent) {
    var F = function() {};       //空的臨時(shí)構(gòu)造函數(shù)
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.uber = Parent.prototype; //uber表示super,因?yàn)閟uper是保留的關(guān)鍵字
}

這樣如果你為子類原型添加setName方法后,希望父類對(duì)象也能獲得該方法,可以:

function inherit(Child, Parent) {
    var F = function() {};      //空的臨時(shí)構(gòu)造函數(shù)
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.uber = Parent.prototype;  //uber表示super,因?yàn)閟uper是保留的關(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不會(huì)影響父類,父類對(duì)象無法使用setName1。但父類對(duì)象可以使用子類通過uber給原型的新增方法setName2。

加分項(xiàng)二:重置該構(gòu)造函數(shù)的指針,以免在將來某個(gè)時(shí)候還需要該構(gòu)造函數(shù)。如果不重置構(gòu)造函數(shù)的指針,那么所有子對(duì)象會(huì)報(bào)告Parent()是它們的構(gòu)造函數(shù),這沒有任何用處:

var c10 = new Child();
console.log(c10.constructor.name);        //Parent
console.log(c10.constructor === Parent);    //true

雖然我們很少用constructor屬性,不改也不影響實(shí)際的使用,但作為完美主義者還是改一下吧:

function inherit(Child, Parent) {
    var F = function() {};          //空的臨時(shí)構(gòu)造函數(shù)
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.uber = Parent.prototype;  //uber表示super,因?yàn)閟uper是保留的關(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

加分項(xiàng)三:臨時(shí)構(gòu)造函數(shù)F()不必每次繼承時(shí)都創(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。繼承時(shí)能像傳統(tǒng)OO語言的Class一樣,子類構(gòu)造函數(shù)調(diào)用父類的構(gòu)造函數(shù)。作為一種代碼結(jié)構(gòu),它有一套命名公約,如initialize,_init等,創(chuàng)建對(duì)象時(shí)這些方法會(huì)被自動(dò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.添加實(shí)現(xiàn)方法
    for(i in props) {
        if(props.hasOwnProperty(i)) {
            Child.prototype[i] = props[i];
        }
    }

    return Child;
};

看一下上面的Klass代碼結(jié)構(gòu)。它有兩個(gè)參數(shù),分別是父類和子類需要擴(kuò)展的字面量形式的屬性。

第一部分是為子類生成構(gòu)造函數(shù):如果父類存在構(gòu)造函數(shù),先調(diào)用父類構(gòu)造函數(shù)。如果子類存在構(gòu)造函數(shù),再調(diào)用子類構(gòu)造函數(shù)。(由于PHP的影響,一個(gè)潛規(guī)則是,類的構(gòu)造函數(shù)最好命名為__construct)。在最后return出生成的構(gòu)造函數(shù)

第二部分是繼承,參照模式五,不贅述。

第三部分是為子類添加需要擴(kuò)展的屬性。

現(xiàn)在我們的代碼中就可以不再糾結(jié)于用哪種模式來實(shí)現(xiàn)繼承了,直接用Klass。

例如創(chuàng)建一個(gè)不繼承自任何類的新類:

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)建了一個(gè)Person的新類,沒有繼承自任何類,意味著繼承Object類(源碼中的Parent = Parent || Object;語句)。構(gòu)造函數(shù)里創(chuàng)建name屬性,并提供了一個(gè)sayHi的方法

現(xiàn)在擴(kuò)充一個(gè)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語言,讓新手也能快速上手進(jìn)行開發(fā)。但其實(shí)不建議用klass,因?yàn)樽屓菀鬃屓水a(chǎn)生一種JavaScript也有類的錯(cuò)覺,其實(shí)它只是一種模擬類的代碼結(jié)構(gòu),如果你對(duì)JavaScript的原型鏈不害怕的話,還是避免用klass比較好

原型繼承

上面介紹的五種模式和klass都屬于基于類型的繼承,在JavaScript繼承一文中還介紹了用Object.create()基于對(duì)象的繼承,也叫原型繼承。用法很簡(jiǎn)單,這里看一下它的本質(zhì)。

原型繼承不涉及類,對(duì)象都是繼承自其它對(duì)象,即要繼承的話,先要有一個(gè)父類對(duì)象:

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)建父對(duì)象(雖然用字面量比較常見),也可以用構(gòu)造函數(shù)來創(chuàng)建父對(duì)象,這樣的話,自身的屬性和構(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)建父對(duì)象都可以,甚至你可以只繼承父類的原型對(duì)象:

var child = create(Parent.prototype);
console.log(typeof child.name);    //undefined
console.log(typeof child.getName);  //function

ES5里定義成Object.create(),基于對(duì)象的繼承我們直接用該方法就行了。

總結(jié)

如果基于對(duì)象繼承用Object.create()。如果基于類型繼承,平時(shí)一些快速應(yīng)用,或小應(yīng)用,用模式三實(shí)現(xiàn)繼承就夠了。復(fù)雜應(yīng)用或大型程序用模式五。如果你做代碼庫,可以用模式五定義inherit函數(shù),或定義Klass。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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