JavaScript中的類繼承
DouglasCrockford
www.crockford.com
And you think you're so clever and classless and free
--John Lennon
JavaScript一種沒有類的,面向?qū)ο蟮恼Z言,它使用原型繼承來代替類繼承。這個(gè)可能對受過傳統(tǒng)的面向?qū)ο笳Z言(如C++和Java)訓(xùn)練的程序員來說有點(diǎn)迷惑。JavaScript的原型繼承比類繼承有更強(qiáng)大的表現(xiàn)力,現(xiàn)在就讓我們來看看。
Java |
JavaScript |
---|---|
強(qiáng)類型 |
弱類型 |
靜態(tài) |
動(dòng)態(tài) |
基于類 |
基于原型 |
類 |
函數(shù) |
構(gòu)造器 |
函數(shù) |
方法 |
函數(shù) |
但首先,為什么我們?nèi)绱岁P(guān)心繼承呢?主要有兩個(gè)原因。第一個(gè)是類型有利。我們希望語言系統(tǒng)可以自動(dòng)進(jìn)行類似類型引用的轉(zhuǎn)換cast。小類型安全可以從一個(gè)要求程序顯示地轉(zhuǎn)換對象引用的類型系統(tǒng)中獲得。這是強(qiáng)類型語言最關(guān)鍵的要點(diǎn),但是這對像JavaScript這樣的弱類型語言是無關(guān)的,JavaScript中的類引用無須強(qiáng)制轉(zhuǎn)換。
第二個(gè)原因是為了代碼的復(fù)用。在程序中常常會發(fā)現(xiàn)很多對象都會實(shí)現(xiàn)同一些方法。類讓建立單一的一個(gè)定義集中建立對象成為可能。在對象中包含其他對象也包含的對象也是很常見的,但是區(qū)別僅僅是一小部分方法的添加或者修改。類繼承對這個(gè)十分有用,但原型繼承甚至更有用。
要展示這一點(diǎn),我們要介紹一個(gè)小小的“甜點(diǎn)”可以主我們像一個(gè)常規(guī)的類語言一樣寫代碼。我們?nèi)缓髸故疽恍┰陬愓Z言中沒有的有用的模式。最后,我們會就會解釋這些“甜點(diǎn)”。
類繼承
首先,我們建立一個(gè)Parenizor類,它有成員 value的get和set方法,還有一個(gè)會將value包裝在括號內(nèi)的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() + ')';
});
這個(gè)語法可能沒什么用,但它很容易看出其中類的形式。method方法接受一個(gè)方法名和一個(gè)函數(shù),并把它們放入類中作為公共方法。
現(xiàn)在我們可以寫成
myParenizor = new Parenizor(0);
myString = myParenizor.toString();
正如期望的那樣,myString是 "(0)"。
現(xiàn)在我們要建立另一個(gè)繼承自Parenizor的類,它基本上是一樣的除了toString方法將會產(chǎn)生"-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。它令一個(gè)方法調(diào)用父類的方法(更改了名稱是為了避免和保留字沖突)。
我們可以寫成這樣
myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString();
這次, myString是 "-0-".
JavaScript 并沒有類,但我們可以編程達(dá)到這個(gè)目的。
多繼承
通過操作一個(gè)函數(shù)的prototype對象,我們可以實(shí)現(xiàn)多繼承。混合多繼承難以實(shí)現(xiàn)而且可能會遭到名稱沖突的危險(xiǎn)。我們可以在JavaScript中實(shí)現(xiàn)混合多繼承,但這個(gè)例子我們將使用一個(gè)較規(guī)范的形式稱為瑞士繼承SwissI nheritance.
假設(shè)有一個(gè)NumberValue類有一個(gè)setValue方法用來檢查 value是不是在一個(gè)指定范圍內(nèi)的一個(gè)數(shù),并在適當(dāng)?shù)臅r(shí)候拋出異常。我們只要它的setValue和 setRange方法給我們的ZParenizor。我們當(dāng)然不想要它的toString方法。這樣,我們寫到:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
這個(gè)將僅僅添加需要的方法。
寄生繼承
這是另一個(gè)書寫 ZParenizor類的方法。并不從 Parenizor繼承,而是寫了一個(gè)調(diào)用了Parenizor構(gòu)造器的構(gòu)造器,并對結(jié)果修改最后返回這個(gè)結(jié)果。這個(gè)構(gòu)造器添加的是特權(quán)方法而非公共方法。
function ZParenizor2(value) {
var self = new Parenizor(value);
self.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-"
};
return self;
}
類繼承是一種“是……”的關(guān)系,而寄生繼承是一個(gè)關(guān)于“原是……而現(xiàn)在是……”的關(guān)系。構(gòu)造器在對象的構(gòu)造中扮演了大量的角色。注意uber (代替super關(guān)鍵字)對特權(quán)方法仍有效。
類擴(kuò)展
JavaScript的動(dòng)態(tài)性讓我們可以對一個(gè)已有的類添加或替換方法。我們可以在任何時(shí)候調(diào)用方法。我們可以隨時(shí)地?cái)U(kuò)展一個(gè)類。繼承不是這個(gè)方式。所以我們把這種情況稱為“類擴(kuò)展”來避免和Java的extends──也叫擴(kuò)展,但不是一回事──相混淆。
對象擴(kuò)展
在靜態(tài)面向?qū)ο笳Z言中,如果你想要一個(gè)對象和另一個(gè)對象有所區(qū)別,你必須新建立一個(gè)類。但在JavaScript中,你可以向單獨(dú)的對象添加方法而不用新建類。這會有巨大的能量因?yàn)槟憔涂梢詴鴮懕M量少的類,類也可以寫得更簡單。想想JavaScript的對象就像哈希表一樣。你可以在任何時(shí)候添加新的值。如果這個(gè)值是一個(gè)函數(shù),那他就會成為一個(gè)方法。
這樣在上面的例子中,我完全不需要 ZParenizor類。我只要簡單修改一下我的實(shí)例就行了。
myParenizor = new Parenizor(0);
myParenizor.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
};
myString = myParenizor.toString();
我們給 myParenizor實(shí)例添加了一個(gè) toString方法而沒有使用任何繼承。我們可以演化單獨(dú)的實(shí)例因?yàn)檫@個(gè)語言是無類型的。
小甜點(diǎn)
要讓上面的例子運(yùn)行起來,我寫了四個(gè)“甜點(diǎn)”方法。首先,method方法,可以把一個(gè)實(shí)例方法添加到一個(gè)類中。
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
這個(gè)將會添加一個(gè)公共方法到 Function.prototype中,這樣通過類擴(kuò)展所有的函數(shù)都可以用它了。它要一個(gè)名稱和一個(gè)函數(shù)作為參數(shù)。
它返回 this。當(dāng)我寫一個(gè)沒有返回值的方法時(shí),我通常都會讓它返回this。這樣可以形成鏈?zhǔn)秸Z句。
下面是 inherits方法,它會指出一個(gè)類是繼承自另一個(gè)類的。它必須在兩個(gè)類都定義完了之后才能定義,但要在方法繼承之前調(diào)用。
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;
});
再來,我們擴(kuò)展 Function類。我們加入一個(gè) parent類的實(shí)例并將它做為新的prototype。我們也必須修正constructor字段,同時(shí)我們加入uber方法。
uber方法將會在自己的prototype中查找某個(gè)方法。這個(gè)是寄生繼承或類擴(kuò)展的一種情況。如果我們是類繼承,那么我們要找到parent的prototype中的函數(shù)。return語句調(diào)用了函數(shù)的apply方法來調(diào)用該函數(shù),同時(shí)顯示地設(shè)置this并傳遞參數(shù)。參數(shù)(如果有的話)可以從arguments數(shù)組中獲得。不幸的是,arguments數(shù)組并不是一個(gè)真正的數(shù)組,所以我們又要用到apply來調(diào)用數(shù)組中的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方法對每個(gè)參數(shù)進(jìn)行循環(huán)。每個(gè)名稱,它都將parent的原型中的成員復(fù)制下來到新的類的prototype中。
總結(jié)
JavaScript可以像類語言那樣使用,但它也有一種十分獨(dú)特的表現(xiàn)層次。我們已經(jīng)看過了類繼承、瑞士繼承、寄生繼承、類擴(kuò)展和對象擴(kuò)展。這一等系列代碼復(fù)用的模式都能來自這個(gè)一直被認(rèn)為是很小、很簡單的JavaScript語言。
類對象屬于“硬的”。給一個(gè)“硬的”對象添加成員的唯一的方法是建立一個(gè)新的類。在JavaScript中,對象是“軟的”。要給一個(gè)“軟”對象添加成員只要簡單的賦值就行了。
因?yàn)镴avaScript中的類是這樣地靈活,你可能會還想到更復(fù)雜的類繼承。但深度繼承并不合適。淺繼承則較有效而且更易表達(dá)。
相關(guān)文章
JavaScript加入收藏夾功能(兼容IE、firefox、chrome)
這篇文章主要介紹了JavaScript加入收藏夾功能,兼容IE、firefox、chrome,并解決了window.sidebar.addPanel is not a function問題,需要的朋友可以參考下2014-05-05JavaScript實(shí)現(xiàn)像雪花一樣的Hexaflake分形
這篇文章主要介紹了JavaScript實(shí)現(xiàn)像雪花一樣的Hexaflake分形,文中示例代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07JS如何通過FileReader獲取.txt文件內(nèi)容
今天小編就為大家分享一篇JS如何通過FileReader獲取.txt文件內(nèi)容,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12