JavaScript中的類繼承

  JavaScript是一個無class的面嚮對象語言,它使用原型繼承而非類繼承。這會讓那些使用傳統面嚮對象語言如C++和Java的程序員們感到困惑。正如咱們所看到的,JavaScript的原型繼承比類繼承具備更強的表現力。javascript

  但首先,要搞清楚咱們爲何如此關注繼承?主要有兩個緣由。首先是方便類型的轉換。咱們但願語言系統可以對那些類似類的引用進行自動轉換。而對於一個要求對引用對象進行顯示轉換的類型系統來講只能得到不多的類型安全性。這對於強類型語言來講很重要,可是在像JavaScript這樣的鬆散型語言中,永遠不須要對對象引用進行強制轉換。html

  第二個緣由是代碼的複用。代碼中存在大量擁有相同方法的對象是十分常見的。類能夠經過一組定義來建立它們。另外存在不少類似的對象也很廣泛,這些對象中只有少數有關添加和修改的方法存在區別。類的繼承能夠頗有效地解決這些問題,但原型繼承更有效。java

  爲了說明這一點,咱們將介紹一點語法糖,它容許咱們以相似於傳統的class的語言來編寫代碼。而後咱們將介紹一些有用的模式,這些模式不適用於傳統的class語言。最後,咱們將對語法糖進行解釋。程序員

類繼承

  首先,咱們添加了一個Parenizor類,包含set和get兩個方法,分別用來設置和獲取value,以及一個toString方法,用來對parens中的value進行包裝。編程

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接受方法的名稱和一個function,並將這個function做爲公共方法添加到類中。數組

  而後咱們能夠這樣寫:安全

myParenizor = new Parenizor(0);
myString = myParenizor.toString();

  正如你所指望的,myString的值爲"(0)".app

  如今咱們建立另外一個類繼承Parenizor,除了toString方法中對於value爲空或0的狀況會輸出"-0-"外其他都和Parenizor相同。函數

function ZParenizor(value) {
    this.setValue(value);
}

ZParenizor.inherits(Parenizor);

ZParenizor.method('toString', function () {
    if (this.getValue()) {
        return this.uber('toString');
    }
    return "-0-";
});

  這裏的inherits方法與Java中的extends方法相似,uber方法也與Java中的super方法相似。它容許一個方法調用父類中的方法(只是改了名稱以避開保留字的限制)。this

  而後咱們能夠這樣寫:

myZParenizor = new ZParenizor(0);
myString = myZParenizor.toString();

  這一次,myString的值爲"-0-".

  JavaScript沒有類,可是咱們能夠經過編程來實現它。

多重繼承

  經過操做一個函數的原型對象,咱們能夠實現多重繼承,從而使咱們能夠用多個類的方法來構建一個類。混合多重繼承可能難以實現,並可能存在方法名稱的衝突。咱們能夠在JavaScript中實現混合多重繼承,可是在本例中咱們將使用一個更嚴格的被稱之爲Swiss繼承的形式。

  假設有一個NumberValue類,包含一個方法setValue,該方法檢查value是否爲某個特定範圍內的數字,必要的時候會拋出異常。咱們只須要ZParenizorsetValuesetRange方法,而不須要toString方法。那麼咱們能夠這樣寫:

ZParenizor.swiss(NumberValue, 'setValue', 'setRange');

  這樣只會將咱們須要的方法添加到類中。

寄生繼承

  ZParenizor還有另一種寫法。除了從Parenizor類繼承,咱們還能夠在構造函數中調用Parenizor的構造函數,並傳遞返回的結果。經過這種方式,咱們給構造函數添加特權方法,而不用再去爲其添加公共方法。

function ZParenizor2(value) {
    var that = new Parenizor(value);
    that.toString = function () {
        if (this.getValue()) {
            return this.uber('toString');
        }
        return "-0-"
    };
    return that;
}

  類的繼承是is-a關係(公有繼承),而寄生繼承是was-a-but-now's-a關係(私有繼承與公有繼承)。構造函數在對象的構造中發揮了很大的做用。注意ubersuper方法仍然可用於特權方法。

類的擴充

  JavaScript的動態性容許咱們添加或替換現有類的方法,method方法能夠隨時被調用,這樣類的全部實例在如今和未來都會有這個方法。咱們能夠在任什麼時候候對一個類進行擴展。繼承具備追溯性,咱們把這個叫作類的擴充(Class Augmentation),以免與Java的extends產生混淆。

對象的擴充

  在靜態面嚮對象語言中,若是你想要一個對象與另外一個對象略微不一樣,就須要定義一個新的類。在JavaScript中,你能夠將方法添加到單個的對象中,而不須要在定義額外的類。這個很是強大,由於你只須要寫不多的類,而且類均可以很簡單。回想一下,JavaScript對象就像哈希表,你能夠隨時添加新的值,若是值是function,那麼它就成了一個方法。

  所以在上面的示例中,我根本不須要ZParenizor類。我能夠簡單地修改個人實例。

myParenizor = new Parenizor(0);
myParenizor.toString = function () {
    if (this.getValue()) {
        return this.uber('toString');
    }
    return "-0-";
};
myString = myParenizor.toString();

  我將toString方法添加到個人myParenizor實例中,而沒有使用任何形式的繼承。咱們能夠修改單個的實例,由於語言是無class的。

Sugar(語法糖)

  爲了使上面的示例能正常工做,我寫了四個sugar方法。首先是method方法,它將一個實例方法添加到類中。

Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    return this;
};

  它在Function.prototype上添加了一個公共方法,所以全部的函數都經過Class Augmentation(類的擴充)得到了該方法。它接受一個名稱和一個函數,並將它們添加到函數的原型對象中。

  它返回this. 當我編寫一個不須要返回值的方法時,我一般都會返回this,這樣就具備了一個級聯式的編程風格。

  接下來是inherits方法,它用來表示一個類從另外一個類繼承。應該在兩個類都被定義以後再調用這個方法,而且在繼承類的方法以前添加該方法。

Function.method('inherits', function (parent) {
    this.prototype = new parent();
    var d = {}, 
        p = this.prototype;
    this.prototype.constructor = parent; 
    this.method('uber', function uber(name) {
        if (!(name in d)) {
            d[name] = 0;
        }        
        var f, r, t = d[name], 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[name] += 1;
        r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
        d[name] -= 1;
        return r;
    });
    return this;
});

  咱們繼續對Function進行擴充。咱們建立了一個父類的實例,並將其做爲新的原型。咱們還修改了構造函數的字段,並將uber方法添加到原型中。

  Uber方法在本身的原型中查找指定的方法。這是在寄生繼承或對象擴充的狀況下調用的函數。若是咱們進行類的繼承,那麼咱們就須要在父類的原型中找到這個函數。Return語句使用函數的apply方法來調用function,顯示地設置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;
});

  Swiss方法對arguments進行遍歷。對每個name,它都從父類的原型中複製一個成員到新類的原型中。

結論

  JavaScript能夠像class語言同樣來使用,但它也具備至關獨特的表現力。咱們研究了類的繼承,Swiss繼承,寄生繼承,類的擴充以及對象的擴充。這種大量代碼的複用模式來自於一種被認爲比Java更小,更簡單的語言。

  類的對象很是嚴格,要將一個新成員添加到對象中,惟一的方法就是建立一個新類。而在JavaScript中,對象是鬆散的,能夠經過簡單的賦值操做將一個新成員添加到對象中。

  因爲JavaScript中的對象很是靈活,因此你須要對類的層次結構進行不一樣的考慮。深層次的結構並不太適用,相反,淺層次的結構更高效,更具備表現力。

 

我從事編寫JavaScript代碼已經有14年了,並且我歷來沒有發現須要使用uber函數。Super在class模式中十分重要,可是在原型和函數式模式中不是必須的。如今看來我早期嘗試在JavaScript中支持class模式是一個錯誤。

 

原文地址:Classical Inheritance in JavaScript

相關連接:http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html

相關文章
相關標籤/搜索