要用 ECMAScript 實現繼承機制,您能夠從要繼承的基類入手。全部開發者定義的類均可做爲基類。出於安全緣由,本地類和宿主類不能做爲基類,這樣能夠防止公用訪問編譯過的瀏覽器級的代碼,由於這些代碼能夠被用於惡意攻擊。數組
選定基類後,就能夠建立它的子類了。是否使用基類徹底由你決定。有時,你可能想建立一個不能直接使用的基類,它只是用於給子類提供通用的函數。在這種狀況下,基類被看做抽象類。瀏覽器
儘管 ECMAScript 並無像其餘語言那樣嚴格地定義抽象類,但有時它的確會建立一些不容許使用的類。一般,咱們稱這種類爲抽象類。安全
建立的子類將繼承超類的全部屬性和方法,包括構造函數及方法的實現。記住,全部屬性和方法都是公用的,所以子類可直接訪問這些方法。子類還可添加超類中沒有的新屬性和方法,也能夠覆蓋超類的屬性和方法。app
和其餘功能同樣,ECMAScript 實現繼承的方式不止一種。這是由於 JavaScript 中的繼承機制並非明確規定的,而是經過模仿實現的。這意味着全部的繼承細節並不是徹底由解釋程序處理。函數
構想原始的 ECMAScript 時,根本沒打算設計對象冒充(object masquerading)。它是在開發者開始理解函數的工做方式,尤爲是如何在函數環境中使用 this 關鍵字後才發展出來。工具
其原理以下:構造函數使用 this 關鍵字給全部屬性和方法賦值(即採用類聲明的構造函數方式)。由於構造函數只是一個函數,因此可以使 ClassA 構造函數成爲 ClassB 的方法,而後調用它。ClassB 就會收到 ClassA 的構造函數中定義的屬性和方法。例如,用下面的方式定義 ClassA 和 ClassB:測試
function ClassA(sColor) { this.color = sColor; this.sayColor = function () { alert(this.color); }; } function ClassB(sColor) { }
還記得嗎?關鍵字 this 引用的是構造函數當前建立的對象。不過在這個方法中,this 指向的所屬的對象。這個原理是把 ClassA 做爲常規函數來創建繼承機制,而不是做爲構造函數。以下使用構造函數 ClassB 能夠實現繼承機制:this
function ClassB(sColor) { this.newMethod = ClassA; this.newMethod(sColor); delete this.newMethod; }
在這段代碼中,爲 ClassA 賦予了方法 newMethod(請記住,函數名只是指向它的指針)。而後調用該方法,傳遞給它的是 ClassB 構造函數的參數 sColor。最後一行代碼刪除了對 ClassA 的引用,這樣之後就不能再調用它。prototype
全部新屬性和新方法都必須在刪除了新方法的代碼行後定義。不然,可能會覆蓋超類的相關屬性和方法:設計
function ClassB(sColor, sName) { this.newMethod = ClassA; this.newMethod(sColor); delete this.newMethod; this.name = sName; this.sayName = function () { alert(this.name); }; }
爲證實前面的代碼有效,能夠運行下面的例子:
var objA = new ClassA("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //輸出 "blue" objB.sayColor(); //輸出 "red" objB.sayName(); //輸出 "John"
有趣的是,對象冒充能夠支持多重繼承。也就是說,一個類能夠繼承多個超類。用 UML 表示的多重繼承機制以下圖所示:
例如,若是存在兩個類 ClassX 和 ClassY,ClassZ 想繼承這兩個類,可使用下面的代碼:
function ClassZ() { this.newMethod = ClassX; this.newMethod(); delete this.newMethod; this.newMethod = ClassY; this.newMethod(); delete this.newMethod; }
這裏存在一個弊端,若是存在兩個類 ClassX 和 ClassY 具備同名的屬性或方法,ClassY 具備高優先級。由於它從後面的類繼承。除這點小問題以外,用對象冒充實現多重繼承機制垂手可得。
因爲這種繼承方法的流行,ECMAScript 的第三版爲 Function 對象加入了兩個方法,即 call() 和 apply()。
call() 方法是與經典的對象冒充方法最類似的方法。它的第一個參數用做 this 的對象。其餘參數都直接傳遞給函數自身。例如:
function sayColor(sPrefix,sSuffix) { alert(sPrefix + this.color + sSuffix); }; var obj = new Object(); obj.color = "blue"; sayColor.call(obj, "The color is ", "a very nice color indeed.");
在這個例子中,函數 sayColor() 在對象外定義,即便它不屬於任何對象,也能夠引用關鍵字 this。對象 obj 的 color 屬性等於 blue。調用 call() 方法時,第一個參數是 obj,說明應該賦予 sayColor() 函數中的 this 關鍵字值是 obj。第二個和第三個參數是字符串。它們與 sayColor() 函數中的參數 sPrefix 和 sSuffix 匹配,最後生成的消息 "The color is blue, a very nice color indeed." 將被顯示出來。
要與繼承機制的對象冒充方法一塊兒使用該方法,只需將前三行的賦值、調用和刪除代碼替換便可:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(color); //delete this.newMethod; ClassA.call(this, sColor); this.name = sName; this.sayName = function () { alert(this.name); }; }
這裏,咱們須要讓 ClassA 中的關鍵字 this 等於新建立的 ClassB 對象,所以 this 是第一個參數。第二個參數 sColor 對兩個類來講都是惟一的參數。
apply() 方法有兩個參數,用做 this 的對象和要傳遞給函數的參數的數組。例如:
function sayColor(sPrefix,sSuffix) { alert(sPrefix + this.color + sSuffix); }; var obj = new Object(); obj.color = "blue"; sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));
這個例子與前面的例子相同,只是如今調用的是 apply() 方法。調用 apply() 方法時,第一個參數還是 obj,說明應該賦予 sayColor() 函數中的 this 關鍵字值是 obj。第二個參數是由兩個字符串構成的數組,與 sayColor() 函數中的參數 sPrefix 和 sSuffix 匹配,最後生成的消息還是 "The color is blue, a very nice color indeed.",將被顯示出來。
該方法也用於替換前三行的賦值、調用和刪除新方法的代碼:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(color); //delete this.newMethod; ClassA.apply(this, new Array(sColor)); this.name = sName; this.sayName = function () { alert(this.name); }; }
一樣的,第一個參數還是 this,第二個參數是隻有一個值 color 的數組。能夠把 ClassB 的整個 arguments 對象做爲第二個參數傳遞給 apply() 方法:
function ClassB(sColor, sName) { //this.newMethod = ClassA; //this.newMethod(color); //delete this.newMethod; ClassA.apply(this, arguments); this.name = sName; this.sayName = function () { alert(this.name); }; }
固然,只有超類中的參數順序與子類中的參數順序徹底一致時才能夠傳遞參數對象。若是不是,就必須建立一個單獨的數組,按照正確的順序放置參數。此外,還可以使用 call() 方法。
繼承這種形式在 ECMAScript 中本來是用於原型鏈的。上一章介紹了定義類的原型方式。原型鏈擴展了這種方式,以一種有趣的方式實現繼承機制。
在上一章學過,prototype 對象是個模板,要實例化的對象都以這個模板爲基礎。總而言之,prototype 對象的任何屬性和方法都被傳遞給那個類的全部實例。原型鏈利用這種功能來實現繼承機制。
若是用原型方式重定義前面例子中的類,它們將變爲下列形式:
function ClassA() { } ClassA.prototype.color = "blue"; ClassA.prototype.sayColor = function () { alert(this.color); }; function ClassB() { } ClassB.prototype = new ClassA();
原型方式的神奇之處在於突出顯示的藍色代碼行。這裏,把 ClassB 的 prototype 屬性設置成 ClassA 的實例。這頗有意思,由於想要 ClassA 的全部屬性和方法,但又不想逐個將它們 ClassB 的 prototype 屬性。還有比把 ClassA 的實例賦予 prototype 屬性更好的方法嗎?
注意:調用 ClassA 的構造函數,沒有給它傳遞參數。這在原型鏈中是標準作法。要確保構造函數沒有任何參數。
與對象冒充類似,子類的全部屬性和方法都必須出如今 prototype 屬性被賦值後,由於在它以前賦值的全部方法都會被刪除。爲何?由於 prototype 屬性被替換成了新對象,添加了新方法的原始對象將被銷燬。因此,爲 ClassB 類添加 name 屬性和 sayName() 方法的代碼以下:
function ClassB() { } ClassB.prototype = new ClassA(); ClassB.prototype.name = ""; ClassB.prototype.sayName = function () { alert(this.name); };
可經過運行下面的例子測試這段代碼:
var objA = new ClassA(); var objB = new ClassB(); objA.color = "blue"; objB.color = "red"; objB.name = "John"; objA.sayColor(); objB.sayColor(); objB.sayName();
此外,在原型鏈中,instanceof 運算符的運行方式也很獨特。對 ClassB 的全部實例,instanceof 爲 ClassA 和 ClassB 都返回 true。例如:
var objB = new ClassB(); alert(objB instanceof ClassA); //輸出 "true" alert(objB instanceof ClassB); //輸出 "true"
在 ECMAScript 的弱類型世界中,這是極其有用的工具,不過使用對象冒充時不能使用它。
原型鏈的弊端是不支持多重繼承。記住,原型鏈會用另外一類型的對象重寫類的 prototype 屬性。
這種繼承方式使用構造函數定義類,並不是使用任何原型。對象冒充的主要問題是必須使用構造函數方式,這不是最好的選擇。不過若是使用原型鏈,就沒法使用帶參數的構造函數了。開發者如何選擇呢?答案很簡單,二者都用。
在前一章,咱們曾經講解過建立類的最好方式是用構造函數定義屬性,用原型定義方法。這種方式一樣適用於繼承機制,用對象冒充繼承構造函數的屬性,用原型鏈繼承 prototype 對象的方法。用這兩種方式重寫前面的例子,代碼以下:
function ClassA(sColor) { this.color = sColor; } ClassA.prototype.sayColor = function () { alert(this.color); }; function ClassB(sColor, sName) { this.name = sName; } ClassB.prototype.sayName = function () { alert(this.name); }; ClassA.call(this, sColor);ClassB.prototype = new ClassA();
在此例子中,繼承機制由兩行突出顯示的藍色代碼實現。在第一行突出顯示的代碼中,在 ClassB 構造函數中,用對象冒充繼承 ClassA 類的 sColor 屬性。在第二行突出顯示的代碼中,用原型鏈繼承 ClassA 類的方法。因爲這種混合方式使用了原型鏈,因此 instanceof 運算符仍能正確運行。
下面的例子測試了這段代碼:
var objA = new ClassA("blue"); var objB = new ClassB("red", "John"); objA.sayColor(); //輸出 "blue" objB.sayColor(); //輸出 "red" objB.sayName(); //輸出 "John"