js類式繼承的實現 非常重要

瓜瓜東西發表於2014-04-09

在開始擺弄程式碼之前,應該搞清楚使用繼承的目的和能帶來什麼好處。一般來說,在設計類的時候,我們希望能減少重複性的程式碼,並且儘量弱化類之間的耦合。而要做到這兩者都兼顧是很難的,我們需要根據具體的條件和環境下決定我們應該採取什麼方法。根據我們對物件導向語言中繼承的瞭解,繼承會帶類直接的強耦合,但js由於其特有的靈活性,可以設計出強耦合和弱耦合,高效率和低效率的程式碼。而具體用什麼,看情況。

下面提供js實現繼承的三種方法:類式繼承,原型繼承,摻元類。這裡先簡述類式繼承,後兩種在往後的隨便中簡述,請多多關注、指導,謝謝。

類式繼承。

js類式繼承的實現依靠原型鏈來實現的。什麼是原型鏈?js中物件有個屬性prototy,這個屬性返回物件型別的引用,用於提供物件的類的一組基本功能。

貌似對prototype有印象,對了,我們經常這樣用程式碼。

1
2
3
4
5
6
7
8
9
var Person = function(){   
    this.name = "liyatang";
};
Person.prototype = {
    //可以在這裡提供Person的基本功能
    getName : function(){
        return this.name;
    }
}

我們把類的基本功能放在prototype屬性裡,表示Person這個物件的引用有XXX功能。

在理解原型後,需要理解下什麼是原型鏈。在訪問物件的某個成員(屬性或方法)時,如果這個成員未見於當前物件,那麼js會在prototype屬性所指的那個物件中查詢它,如果還沒有找到,就繼續到下一級的prototype所指的物件中查詢,直至找到。如果沒有找到就會返回undifined。

那麼原型鏈給我們什麼提示呢?很容易聯想到,原型鏈意味著讓一個類繼承另一個類,只需將子類的prototype設定為指向父類的一個例項即可。這就把父類的成員繫結到子類上了,因為在子類上查詢不到某個成員時會往父類中查詢。(以上這兩段用詞不嚴謹,只在用通俗易懂的言語描述)

下面我們需要個Chinese類,需要繼承Person類的name和getName成員。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Chinese = function(name, nation){
        //繼承,需要呼叫父類的建構函式,可以用call呼叫,this指向Chinese
    //使Person在此作用域上,才可以呼叫Person的成員
    Person.call(this,name);
    this.nation = nation;
};
Chinese.prototype = Person.prototype;
//這裡不可和以前一樣,因為覆蓋掉了prototype屬性
//Chinese.prototype = {
//  getNation : function(){
//      return this.nation;
//  }
//};
//以後的方法都需要這樣加
Chinese.prototype.getNation = function(){
        return this.nation;
};

繼承關係就建立了,我們這樣呼叫它

1
2
var c = new Chinese("liyatang","China");
alert(c.getName());// liyatang

於是類式繼承就這樣完成了。難道真的完成了嘛,用firebug在alert那裡設斷點,會發現原來的Person.prototype被修改了,新增了getNation方法。

1

這是因為在上面的程式碼Chinese.prototype = Person.prototype; 這是引用型別,修改Chinese同時也修改了Person。這本身就是不能容忍的,且使類之間形成強耦合性,這不是我們要的效果。

我們可以另起一個物件或例項化一個例項來弱化耦合性。

1
2
3
4
5
6
//第一種
//Chinese.prototype = new Person();
//第二種
//var F = function(){};
//F.prototype = Person.prototype;
//Chinese.prototype = F.prototype;

這兩種方法有什麼區別呢。在第二種中新增了一個空函式F,這樣做可以避免建立父類的一個例項,因為有可能父類會比較龐大,而且父類的建構函式會有一些副作用,或者說會執行大量的計算任務。所以力薦第二種方法。

到此,完了嘛,還沒有!在物件的屬性prototype下面有個屬性constructor,它儲存了對構造特定物件例項的函式的引用。根據這個說法Chiese.prototype.constructor應該等於Chinese,實際上不是。

回憶之前在設定Chiese的原型鏈時,我們把Person.prototype 覆蓋掉了Chiese.prototype。所以此時的Chiese.prototype.constructor是Person。我們還需要新增以下程式碼

1
2
3
4
//對這裡的if條件不需要細究,知道Chinese.prototype.constructor = Chinese就行
if(Chinese.prototype.constructor == Object.prototype.constructor){
    Chinese.prototype.constructor = Chinese;
}

整理全部程式碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var Person = function(name){
    this.name = name;
};
Person.prototype = {
    getName : function(){
        return this.name;
    }
};
 
var Chinese = function(name, nation){
    Person.call(this,name);
    this.nation = nation;
};
var F = function(){};
F.prototype = Person.prototype;
Chinese.prototype = F.prototype;
if(Chinese.prototype.constructor == Object.prototype.constructor){
    Chinese.prototype.constructor = Chinese;
}
Chinese.prototype.getNation = function(){
        return this.nation;
};
 
var c = new Chinese("liyatang","China");
alert(c.getName());

如果可以把繼承的程式碼放在一個函式裡,方便程式碼複用,最後整理程式碼如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function extend(subClass,superClass){
    var F = function(){};
    F.prototype = superClass.prototype;
    subClass.prototype = new F();
    subClass.prototype.constructor = subClass;
    subClass.superclass = superClass.prototype; //加多了個屬性指向父類本身以便呼叫父類函式
    if(superClass.prototype.constructor == Object.prototype.constructor){
        superClass.prototype.constructor = superClass;
    }
}
 
var Person = function(name){
    this.name = name;
};
Person.prototype = {
    getName : function(){
        return this.name;
    }
};
 
var Chinese = function(name, nation){
    Person.call(this,name);
    this.nation = nation;
};
extend(Chinese, Person);
Chinese.prototype.getNation = function(){
        return this.nation;
};
 
var c = new Chinese("liyatang","China");
alert(c.getName());

發表後修改:

在一樓的評論下,我對那個extend函式又有新的看法。之前在討論如何設定原型鏈時提出了兩種方法

1
2
3
4
5
6
//第一種
//Chinese.prototype = new Person();
//第二種
//var F = function(){};
//F.prototype = Person.prototype;
//Chinese.prototype = F.prototype;

雖然第二種減少了呼叫父類的建構函式這條路,但在設計Chinese類時用了Person.call(this,name);這裡也相當於呼叫了父類的建構函式。

然而用第一種方法的話可以減少在Chinese中再寫Person.call(this,name);,這部分程式碼在子類中往往會遺忘。不妨把這種功能程式碼放在了extend裡。就只寫

Chinese.prototype = new Person();也達到同樣的目的:耦合不強。

但遺忘的一點是,Chinese.prototype = new Person();這樣寫對嘛。答案是不對!很明顯 new Person()需要傳一個name引數的。我們不可能在extend函式裡做這部分工作,只好在Chinese類裡呼叫父類的建構函式了。這樣也符合物件導向的思路。

所以,還是力薦用第二種方法。

第一次這樣寫有關技術類的文章,基本是按自己的思路鋪展開來,難免會有一些沒有考慮到的地方和解釋的不清楚的地方,望留言反饋,謝謝。

相關文章