1. 程式人生 > 其它 >JavaScript中的類、原型和建構函式

JavaScript中的類、原型和建構函式

JavaScript中的類、原型和建構函式

每個JavaScript物件都是一個屬性集合,相互之間沒有任何聯絡。在JavaScript中也可以定義物件的類,讓每個物件都共享某些屬性,這種 “共享”的特性是非常有用的。類的成員或例項都包含一些屬性,用以存放或定義它們的狀態,其中有些屬性定義了它們的行為(通常稱為方法)。這些行為通常是由類定義的,而且為所有例項所共享。例如,假設有一個名為Complex的類用來表示複數,同時還定義了一些複數運算。一個Complex例項應當包含複數的實部和虛部(狀態),同樣Complex類還會定義複數的加法和乘法操作(行為)。

在JavaScript中,類的實現是基於其原型繼承機制的。如果兩個例項都從同一個原型物件上繼承了屬性,我們說它們是同一個類的例項。

如果兩個物件繼承自同一個原型,往往意味著(但不是絕對)它們是由同一個建構函式建立並初始化的。

如果你對諸如Java和C++這種強型別(強/弱型別是指型別檢查的嚴格程度,為所有變數指定資料型別稱為“強型別”。)的面向物件程式設計比較熟悉,你會發現JavaScript中的類和Java以及C++中的類有很大不同。儘管在寫法上類似,而且在JavaScript中也能 “模擬”出很多經典的類的特性( 比如傳統類的封裝、繼承和多型。),但是最好要理解JavaScript的類和基於原型的繼承機制,以及和傳統的Java(當然還有類似Java的語言)的類和基於類的繼承機制的不同之處。

JavaScript中類的一個重要特性是“動態可繼承"(dynamically extendable)。

我們可以將類看做是型別。

“鴨式辯型”(duck-typing)的程式設計哲學,它弱化了物件的型別,強化了物件的功能。

定義類是模組開發和重用程式碼的有效方式之一。

類和原型

在JavaScript中,類的所有例項物件都從同一個原型物件上繼承屬性。因此,原型物件是類的核心。inherit( )這個函式返回一個新建立的物件,後者繼承自某個原型物件。如果定義一個原型物件,然後通過inherit( )函式建立一個繼承自它的物件,這樣就定義了一個JavaScript類。通常,類的例項還需要進一步的初始化,通常是通過定義一個函式來建立並初始化這個新物件。下例給一個表示“值的範圍”的類定義了原型物件,還定義了一個“工廠”函式用以建立並初始化類的例項。

例: 一個簡單的JavaScript類
// range.js: 實現一個能表示值的範圍的類

// 這個工廠方法返回一個新的“範圍物件”
function range(from, to) {
    // 使用inherit()函式來建立物件,這個物件繼承自在下面定義的原型物件
    // 原型物件作為函式的一個屬性儲存,並定義所有”範圍物件”所共享的方法(行為) 
    var r = inherit(range.methods);

    // 儲存新的”範圍物件”的起始位置和結束位置(狀態)
    // 這兩個屬性是不可繼承的,每個物件都擁有唯一的屬性
    r.from = from;
    r.to = to;

    // 返回這個新建立的物件

    return r;
}

// 原型物件定義方法,這些方法為每個範圍物件所繼承 
range.methods = {
    // 如果x在範畝內,則返回true,否則返回false
    // 這個方法可以比較數字範圍,也可以比較字串和日期範圍 
    includes: function (x) {
    return this.from <= x && x <= this.to; },
    
    // 對於範圍內的每個整數都呼叫一次十
    // 這個方法只可用做數字範圍
    foreach: function (f) {
        for (var x = Math.ceil(this.from); x <= this.to; x++) f(x); 
    },
    // 返回表示這個範圍的字串
    toString: function () {return "(" + this.from + "..." + this.to + ")";}
};

// 這裡是使用“範圍物件”的一些例子
var r = range(1, 3);        // 建立一個範圍物件
r.includes(2);              // => true: 2 在這個範圍內
r.foreach(console.log);     // 輸出 1 2 3
consol.log(r);              // 輸出 (1...3)

在上例中有一些程式碼是沒有用的。這段程式碼定義了一個工廠方法range( ),用來建立新的範圍物件。我們注意到,這裡給range( )函式定義了一個屬性range.methods,用以快捷地存放定義類的原型物件。把原型物件掛在函式上沒什麼大不了,但也不是慣用做法。再者,注意range( )函式給每個範圍物件都定義了from和to屬性,用以定義範圍的起始位置和結束位置,這兩個屬性是非共享的,當然也是不可繼承的。最後,注意在range.methods中定義的那些可共享、可繼承的方法都用到了from和to屬性,而且使用了this關鍵字,為了指代它們,二者使用this關鍵字來指代呼叫這個方法的物件。任何類的方法都可以通過this的這種基本用法來讀取物件的屬性。

類和建構函式

上例展示了在JavaScript中定義類的其中一種方法。但這種方法並不常用,畢竟它沒有定義建構函式,建構函式是用來初始化新建立的物件的。使用關鍵字new來呼叫建構函式。使用new呼叫建構函式會自動建立一個新物件,因此建構函式本身只需初始化這個新物件的狀態即可。呼叫建構函式的一個重要特徵是,建構函式的 prototye屬性被用做新物件的原型。這意味著通過同一個建構函式建立的所有物件都繼承自一個相同的物件,因此它們都是同一個類的成員。下例對例上中的“範圍類”做 了修改,使用建構函式代替工廠函式:

例:使用建構函式來定義“範圍類”
// rangez.js: 表示值的範圍的類的另一種實現

// 這是一個建構函式,用以初始化新建立的”範圍物件”
// 注意,這裡並沒有建立並返回一個物件,僅僅是初始化 
function Range(from, to) {
// 儲存”範圍物件”的起始位置和結束位置(狀態)
// 這兩個屬性是不可繼承的,每個物件都擁有唯一的屬性 
this.from = from;
this.to = to;
}

// 所有的"範圍物件”都繼承自這個物件
// 注意,屬性的名字必須是"prototype"
Range.prototype = {
// 如果x茬範圍內,則返回true;否則返回false
// 這個方法可以比較數字範圍,也可以比較字串和日期範圍
includes: function (x) { return this.from <= x && x <= this.to; }, 
// 對於範圍內的每個整數都呼叫一次f
// 這個方法只可用於數字範圍
foreach: function (f) {
  for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);
},
// 返回表示這個範圍的字串
toString: function () (return "(" + this.from + "..." + this.to + ")";}
};

// 這裡是使用“範圍物件”的一些例子
var r = range(1, 3);	    // 建立一個範圍物件
r.includes(2);	            // => true:	2	在這個範圍內
r.foreach(console.log);     // 輸出 1 2 3
console.log(r);	            // 輸出 (1...3)

將上面兩例中的程式碼做一個仔細的對比,可以發現兩種定義類的技術的差別。首先,注意當工廠函式range( )轉化為建構函式時被重新命名為Range( )。這裡遵循了一個常見的程式設計約定:從某種意義上講,定義建構函式既是定義類,並且類名首字母要大寫。 而普通的函式和方法都是首字母小寫。

再者,注意Range( )建構函式是通過new關鍵字呼叫的(在示例程式碼的末尾),而range( )工廠函式則不必使用new。上面兩例用兩種方法建立新物件,一是通過呼叫普通函式來建立新物件,另外一個就是使用建構函式呼叫來建立新物件。由於Range( )建構函式是通過new關鍵字呼叫的,因此不必呼叫inherit( )或其他什麼邏輯來建立新物件。在呼叫建構函式之前就已經建立了新物件,通過this關鍵字可以獲取這個新物件。Range( )建構函式只不過是初始化this而已。建構函式甚至不必返回這個新建立的物件,建構函式會自動建立物件, 然後將建構函式作為這個物件的方法來呼叫一次,最後返回這個新物件。事實上,建構函式的命名規則(首字母大寫)和普通函式是如此不同還有另外一個原因,建構函式呼叫和普通函式呼叫是不盡相同的。建構函式就是用來“構造新物件”的,它必須通過關鍵字new呼叫,如果將建構函式用做普通函式的話,往往不會正常工作。開發者可以通過命名約定來(建構函式首字母大寫,普通方法首字母小寫)判斷是否應當在函式之前冠以關鍵字mew。

上面兩例之間還有一個非常重要的區別,就是原型物件的命名。在第一段示例程式碼中的原型是range.methodso這種命名方式很方便同時具有很好的語義,但又過於隨意。

在第二段示例程式碼中的原型是Range.prototype,這是一個強制的命名。對Range( )建構函式的呼叫會自動使用Range.prototype作為新Range物件的原型。

最後,需要注意在上面兩例中兩種類定義方式的相同之處,兩者的範圍方法定義和呼叫方式是完全一樣的。

建構函式和類的標識

上文提到,原型物件是類的唯一標識:當且僅當兩個物件繼承自同一個原型物件時,它們才是屬於同一個類的例項。而初始化物件的狀態的建構函式則不能作為類的標識,兩個建構函式的prototype屬性可能指向同一個原型物件。那麼這兩個建構函式建立的例項是屬於同一個類的。

儘管建構函式不像原型那樣基礎,但建構函式是類的“外在表現”。很明顯的,建構函式的名字通常用做類名。比如,我們說Range( )建構函式建立Range物件。然而,更根本地講,當使用instanceof運算子來檢測物件是否屬於某個類時會用到建構函式。假設這裡有一個物件r,我們想知道r是否是Range物件,我們這樣寫:

r instanceof Range // 如果r繼承自Range.prototype,則返回true

實際上instanceof運算子並不會檢査工是否是由Range( )建構函式初始化而來,而會檢査r是否繼承自Range.prototype( )。不過,instanceof的語法則強化了“建構函式是類的公有標識”的概念。

constructor屬性

在上例中,將Range.prototype定義為一個新物件,這個物件包含類所需要的方法。其實沒有必要新建立一個物件,用單個物件直接量的屬性就可以方便地定義原型上的方法。任何JavaScript函式都可以用做建構函式,並且呼叫建構函式是需要用到一個prototye屬性的。因此,每個JavaScript函式(ECMAScript 5中的Function.bind( )方法返回的函式除外)都自動擁有一個prototype屬性。這個屬性的值是一個物件,這個物件包含唯一個不可列舉屬性constructoro constructor屬性的值是一個函式物件:

var F = function() {};	// 這是一個函式物件
var p = F.prototype;    // 這是F相關聯的原型物件
var c = p.constructor;   //這是與原型相關聯的函式
c === F                  // => true: 對於任意函式F.prototype.constructor==F

可以看到建構函式的原型中存在預先定義好的constructor屬性,這意味著物件通常繼承的constructor均指代它們的建構函式。由於建構函式是類的“公共標識”,因此這個constructor屬性為物件提供了類。

var o = new F();	// 建立類F的一個物件
o.constructor === F // => true, constructor屬性指代這個類

如下圖所示,下圖展示了建構函式和原型物件之間的關係,包括原型到建構函式的反向引用以及建構函式建立的例項。

需要注意的是,上圖用Range( )建構函式作為示例,但實際上,例9-2中定義的Range類 使用它自身的一個新物件重寫預定義的Range.prototype物件。這個新定義的原型物件不 含有constructor屬性。因此Range類的例項也不含有constructor屬性。我們可以通過補 救措施來修正這個問題,顯式給原型新增一個建構函式:

Range.prototype = { 
 constructor: Range, // 顯式設定建構函式反向引用 
 includes: function(x) { return this.from <= x && x <= this.to; }, 
 foreach: function(f) {
     for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
 },
 toString: function() { return "(" + this.from + "..." + this.to + ")"; } 
};

另一種常見的解決辦法是使用預定義的原型物件,預定義的原型物件包含constructor屬性,然後依次給原型物件新增方法:

// 擴充套件預定義的Range.prototype物件,而不重寫之
// 這樣就自動建立Range.prototype.constructor屬性

Range.prototype.includes = function (x) {return this.from <= x && x <= this.to;}; Range.prototype.foreach = function (f) {
    for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);

    };
    Range.prototype.toString = function () {
        return "(" + this.from + "..." + this.to + ")";
    };