JavaScript中的類繼承


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

  但首先,要搞清楚我們為什么如此關注繼承?主要有兩個原因。首先是方便類型的轉換。我們希望語言系統能夠對那些相似類的引用進行自動轉換。而對於一個要求對引用對象進行顯示轉換的類型系統來說只能獲得很少的類型安全性。這對於強類型語言來說很重要,但是在像JavaScript這樣的松散型語言中,永遠不需要對對象引用進行強制轉換。

  第二個原因是代碼的復用。代碼中存在大量擁有相同方法的對象是十分常見的。類可以通過一組定義來創建它們。另外存在很多相似的對象也很普遍,這些對象中只有少數有關添加和修改的方法存在區別。類的繼承可以很有效地解決這些問題,但原型繼承更有效。

  為了說明這一點,我們將介紹一點語法糖,它允許我們以類似於傳統的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)".

  現在我們創建另一個類繼承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方法類似。它允許一個方法調用父類中的方法(只是改了名稱以避開保留字的限制)。

  然后我們可以這樣寫:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM