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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.繼承(接口繼承和實現(xiàn)繼承) 繼承是 OO 語言中的一個最為人津津樂道的概念。許多 OO 語言都支持兩種繼承方式...
    believedream閱讀 1,061評論 0 3
  • 繼承 Javascript中繼承都基于兩種方式:1.通過原型鏈繼承,通過修改子類原型的指向,使得子類實例通過原型鏈...
    LeoCong閱讀 409評論 0 0
  • 1、構(gòu)造函數(shù)模式 [url=]file:///C:/Users/i037145/AppData/Local/Tem...
    橫沖直撞666閱讀 927評論 0 0
  • 生活中有這樣的一個現(xiàn)象:作為傾聽者,對于演講者表達的一個意思,不同的人,會有不同的理解,更有甚者會得出截然相反的觀...
    丁昆朋閱讀 736評論 6 2
  • 綠江彼岸 斷橋一端 悠悠白鷺 在水一方 平臺樓宇 鶯飛草長 牛羊三兩 樂得自在 華燈初上 歲月靜好 光陰似夢 只...
    橙澈_Cc閱讀 224評論 1 1

友情鏈接更多精彩內(nèi)容