JavaScript Inheritance
DouglasCrockford
www.crockford.com
And you think you're so clever and classless and free
--John Lennon
JavaScript一種沒有類的,物件導向的語言,它使用原型繼承來代替類繼承。這個可能對受過傳統的物件導向語言(如C++和Java)訓練的程式員來說有點迷惑。JavaScript的原型繼承比類繼承有更強大的表現力,現在就讓我們來看看。
Java |
JavaScript |
強型別 |
弱類型 |
靜態 |
動態 |
基於類 |
基於原型 |
類 |
函數 |
構造器 |
函數 |
方法 |
函數 |
但首先,為什麼我們如此關心繼承呢?主要有兩個原因。第一個是類型有利。我們希望語言系統可以自動進行類似類型引用的轉換cast。小型別安全可以從一個要求程式顯示地轉換對象引用的類型系統中獲得。這是強型別語言最關鍵的要點,但是這對像JavaScript這樣的弱類型語言是無關的,JavaScript中的類引用無須強制轉換。
第二個原因是為了代碼的複用。在程式中常常會發現很多個物件都會實現同一些方法。類讓建立單一的一個定義集中建立對象成為可能。在對象中包含其他對象也包含的對象也是很常見的,但是區別僅僅是一小部分方法的添加或者修改。類繼承對這個十分有用,但原型繼承甚至更有用。
要展示這一點,我們要介紹一個小小的“甜點”可以主我們像一個常規的類語言一樣寫代碼。我們然後會展示一些在類語言中沒有的有用的模式。最後,我們會就會解釋這些“甜點”。
類繼承
首先,我們建立一個Parenizor類,它有成員 value的get和set方法,還有一個會將value封裝在括弧內的toString方法。
複製代碼 代碼如下:function Parenizor(value) {
this.setValue(value);
}
Parenizor.method('setValue', function (value) {
this.value = value;
return this;
});
Parenizor.method('getValue', function () {
return this.value;
});
Parenizor.method('toString', function () {
return '(' + this.getValue() + ')';
});
這個文法可能沒什麼用,但它很容易看出其中類的形式。method方法接受一個方法名和一個函數,並把它們放入類中作為公用方法。
現在我們可以寫成 複製代碼 代碼如下:myParenizor = new Parenizor(0);
myString = myParenizor.toString();
正如期望的那樣,myString是 "(0)"。
現在我們要建立另一個繼承自Parenizor的類,它基本上是一樣的除了toString方法將會產生"-0-"如果value是零或者空。 複製代碼 代碼如下:function ZParenizor(value) {
this.setValue(value);
}
ZParenizor.inherits(Parenizor);
ZParenizor.method("e;toString"e;, function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
});
inherits方法類似於Java的extends 。uber方法類似於Java的super。它令一個方法調用父類的方法(更改了名稱是為了避免和保留字衝突)。
我們可以寫成這樣 複製代碼 代碼如下:myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString();
這次, myString是 "-0-".
JavaScript 並沒有類,但我們可以編程達到這個目的。
多繼承
通過操作一個函數的prototype對象,我們可以實現多繼承。混合多繼承難以實現而且可能會遭到名稱衝突的危險。我們可以在JavaScript中實現混合多繼承,但這個例子我們將使用一個較規範的形式稱為瑞士繼承SwissI nheritance.
假設有一個NumberValue類有一個setValue方法用來檢查 value是不是在一個指定範圍內的一個數,並在適當的時候拋出異常。我們只要它的setValue和 setRange方法給我們的ZParenizor。我們當然不想要它的toString方法。這樣,我們寫到: 複製代碼 代碼如下:ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
這個將僅僅添加需要的方法。
寄生繼承
這是另一個書寫 ZParenizor類的方法。並不從 Parenizor繼承,而是寫了一個調用了Parenizor構造器的構造器,並對結果修改最後返回這個結果。這個構造器添加的是特權方法而非公用方法。 複製代碼 代碼如下:function ZParenizor2(value) {
var self = new Parenizor(value);
self.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-"
};
return self;
}
類繼承是一種“是……”的關係,而寄生繼承是一個關於“原是……而現在是……”的關係。構造器在對象的構造中扮演了大量的角色。注意uber (代替super關鍵字)對特權方法仍有效。
類擴充
JavaScript的動態性讓我們可以對一個已有的類添加或替換方法。我們可以在任何時候調用方法。我們可以隨時地擴充一個類。繼承不是這個方式。所以我們把這種情況稱為“類擴充”來避免和Java的extends──也叫擴充,但不是一回事──相混淆。
對象擴充
在靜態物件導向語言中,如果你想要一個對象和另一個對象有所區別,你必須建立立一個類。但在JavaScript中,你可以向單獨的對象添加方法而不用建立類。這會有巨大的能量因為你就可以書寫盡量少的類,類也可以寫得更簡單。想想JavaScript的對象就像雜湊表一樣。你可以在任何時候添加新的值。如果這個值是一個函數,那他就會成為一個方法。
這樣在上面的例子中,我完全不需要 ZParenizor類。我只要簡單修改一下我的執行個體就行了。 複製代碼 代碼如下:myParenizor = new Parenizor(0);
myParenizor.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
};
myString = myParenizor.toString();
我們給 myParenizor執行個體添加了一個 toString方法而沒有使用任何繼承。我們可以演化單獨的執行個體因為這個語言是無類型的。
小甜點
要讓上面的例子運行起來,我寫了四個“甜點”方法。首先,method方法,可以把一個執行個體方法添加到一個類中。 複製代碼 代碼如下:Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
這個將會添加一個公用方法到 Function.prototype中,這樣通過類擴充所有的函數都可以用它了。它要一個名稱和一個函數作為參數。
它返回 this。當我寫一個沒有傳回值的方法時,我通常都會讓它返回this。這樣可以形成鏈式語句。
下面是 inherits方法,它會指出一個類是繼承自另一個類的。它必須在兩個類都定義完了之後才能定義,但要在方法繼承之前調用。 複製代碼 代碼如下:Function.method('inherits', function (parent) {
var d = 0, p = (this.prototype = new parent());
this.method('uber', function uber(name) {
var f, r, t = d, v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d -= 1;
return r;
});
return this;
});
再來,我們擴充 Function類。我們加入一個 parent類的執行個體並將它做為新的prototype。我們也必須修正constructor欄位,同時我們加入uber方法。
uber方法將會在自己的prototype中尋找某個方法。這個是寄生繼承或類擴充的一種情況。如果我們是類繼承,那麼我們要找到parent的prototype中的函數。return語句調用了函數的apply方法來調用該函數,同時顯示地設定this並傳遞參數。參數(如果有的話)可以從arguments數組中獲得。不幸的是,arguments數組並不是一個真正的數組,所以我們又要用到apply來調用數組中的slice方法。
最後,swiss方法 複製代碼 代碼如下:Function.method('swiss', function (parent) {
for (var i = 1; i < arguments.length; i += 1) {
var name = arguments[i];
this.prototype[name] = parent.prototype[name];
}
return this;
});
The swiss方法對每個參數進行迴圈。每個名稱,它都將parent的原型中的成員複製下來到新的類的prototype中。
總結
JavaScript可以像類語言那樣使用,但它也有一種十分獨特的表現層次。我們已經看過了類繼承、瑞士繼承、寄生繼承、類擴充和對象擴充。這一等系列代碼複用的模式都能來自這個一直被認為是很小、很簡單的JavaScript語言。
類對象屬於“硬的”。給一個“硬的”對象新增成員的唯一的方法是建立一個新的類。在JavaScript中,對象是“軟的”。要給一個“軟”對象新增成員只要簡單的賦值就行了。
因為JavaScript中的類是這樣地靈活,你可能會還想到更複雜的類繼承。但深度繼承並不合適。淺繼承則較有效而且更易表達。