javascript|編程|對象|繼承
我們將向你展示 JavaScript 如何?物件導向的語言中的: 繼承. 同時, 這些例子將向你展示如何?類的封裝. 在此, 我們不會討論多態實現.
雖然 JavaScript 是指令碼語言, 但它所支援的物件導向編程也是非常強大的. 雖然它沒有類和執行個體, 但它有對象, 原型和隱式的繼承. 我們將會解釋如何類比繼承及其超類與子類之間關係的形式. 原型是理解繼承概念的關鍵, 我們將會教你如何建立原型, 如何檢測一個對象是否是另外一個對象的原型, 及其 JavaScript 的模型與 Java 物件導向編程之間的區別. 我們同樣會向你展示如何檢測對象所包含的各種屬性的方法. 在另外一篇文章裡, 我還會詳細地講解有關 "原型鏈 (prototype chain)" 的知識.
本文大量地參考了 Webreference.com 中 "Object-Oriented Programming with JavaScript, Part I: Inheritance" 的內容, 許多內容我進行了詳細的測試和再探討, 以保證內容不會有太大的失誤.
原文地址: http://www.webreference.com/js/column79/
物件導向語言的特點
物件導向設計是基於以下 3 個主要原理的: 封裝, 繼承和多態. 說某種程式語言是支援 OO (物件導向) 設計的, 只有在它的文法中支援以上 3 個概念才可以這麼說. 這種語言應該為你提供某些方法, 以使你能很輕鬆地定義和使用這些範例. 封裝涉及到了將某個對象變成一個 "黑盒子"的概念. 當你使用某個對象時, 你不用知道它內部是如何工作的, 你也不必理解對象是如何工作的. 這個對象只需將它絕對有用的資訊以介面方式提供出來. 此對象應該給你提供友好的介面, 來讓你可以使用其有限的屬性集和方法集. 封裝還有一層意思, 那就是說某個對象包含了它需要的每一樣東西, 這包括資料和對於它的操作. 封裝的概念非常的強大, 因為它允許將一個大的軟體項目有效地分配給每個開發人員, 對於團隊中的每個人, 他們只需要關注自己所實現的對象, 而不需要太多地關注於別人的實現. 開發項目中的開銷使得Team Dev中成員與介面的數量按指數層級增長. 封裝是自 "軟體危機" 以來最受歡迎的 OO 設計理念.
軟體的複用是 OO 設計思想中另外一個重要的特點. 在軟體體系中實現此思想的主要方法就是繼承. 類就是定義對象的功能. 超類是某個新類, 或者說是子類被建立的來源類. 一個子類從它的超類中繼承了所的方法和屬性. 實際上, 所有的子類都是被自動地產生的, 因此節省了大量的工作. 你不需要一個一個地定義這些子類. 當然, 你可以重載那些繼承下來的方法和屬性. 事實上, 誰也沒有指出哪個子類要建立得和其超類一模一樣, 除非你沒有重載任何的屬性和方法.
多態可能是這個 3 個概念中最複雜的一個了. 其本質上是說, 每個對象都可以處理各種不同的資料類型. 你不必為處理不同的資料類型而建立不同的類. 其典型的例子就是畫圖的類, 你不必為實現畫圓, 畫矩形, 畫橢圓而編寫不同的類. 你可以建立一個足夠聰明的類來調用特定的方法來操作特定的形狀.
通過函數實現繼承
雖然 JavaScript 不支援顯示繼承操作符, 但你可以通過其實方式實現隱式繼承. 對於實作類別的繼承, 有 2 種比較常用的方式. 第一種將某個類定義成子類的方法是, 通過在負責定義子類函數的內部調用超類的建構函式. 看下面的樣本:
// 超類建構函式
function superClass() {
this.bye = superBye;
this.hello = superHello;
}
// 子類建構函式
function subClass() {
this.inheritFrom = superClass;
this.inheritFrom();
this.bye = subBye;
}
function superHello() {
return "Hello from superClass";
}
function superBye() {
return "Bye from superClass";
}
function subBye() {
return "Bye from subClass";
}
// 測試構造特性的函數
function printSub() {
var newClass = new subClass();
alert(newClass.bye());
alert(newClass.hello());
}
當你運行上面的 printSub 函數時, 它會依次執行 subBuy 和 superHello 函數. 我們可以看到, bye 和 hello 方法最先在 superClass 中被定義了. 然而, 在 subClass 中, bye 方法又被重載了, subClass 建構函式頭兩行的功能只是做了一個簡單的原始的繼承操作, 但它是通過顯示執行 inheritFrom 方法來完成的繼承操作. 繼承的過程先是將 superClass 的對象原型賦給 subClass 下的 inheritFrom 方法, 然後在執行完 superClass 的建構函式後, superClass 的屬性就被自動地加到了 subClass 的屬性列表中.這主要是由於在 subClass 中通過 this 來調用的 inheritFrom (也就是 superClass) 建構函式造成的, 通過此種方式調用 superClass 建構函式時, JavaScript 解譯器會把 superClass 中的 this 與 subClass 中的 this 理解成位於同一個範圍下的 this 關鍵字, 所以就產生了繼承的效果.
另外, 需要說明的是, 對於任何一個執行個體化的對象, 你任意地為它添加屬性或方法, 如下所示:
var newClass = new subClass();
newClass.addprop = "added property to instance object";
很明顯, 通過此種方式添加的屬性和方法只對當前執行個體化對象有效, 不會影響所有的同類型對象執行個體. 無疑, 它是你創造的一個獨一無二的對象執行個體.
通過原型實現繼承
第二種, 也是更強大的方法是通過建立一個超類對象, 然後將其賦值給子類對象的 prototype 屬性, 以此方式來建立子類的繼承. 假設我們的超類是 superClass, 子類是 subClass. 其 prototype 的賦值格式如下:
subClass.prototype = new superClass;
對於原型繼承的實現方式, 讓我們剛前面的代碼改寫一下, 樣本如下:
// 超類建構函式
function superClass() {
this.bye = superBye;
this.hello = superHello;
}
// 子類建構函式
function subClass() {
this.bye = subBye;
}
subClass.prototype = new superClass;
function superHello() {
return "Hello from superClass";
}
function superBye() {
return "Bye from superClass";
}
function subBye() {
return "Bye from subClass";
}
// 測試構造特性的函數
function printSub() {
var newClass = new subClass();
alert(newClass.bye());
alert(newClass.hello());
}
我們可以看到, 除了將前面第一種繼承方式中 subClass 中的前 2 行內容, 換成函數外的 prototype 指派陳述式之外, 沒有其它任何的變化, 但代碼的執行效果和前面是一樣的.
為已經建立的對象添加屬性
通過原型實現的繼承比通過函數實現的繼承更好, 因為它支援動態繼承. 你可以在建構函式已經完成之後, 再通過 prototype 屬性定義超類的其它方法和屬性, 並且其下的子類對象會自動地獲得新的方法和屬性. 下面是樣本, 你可以看到它的效果.
function superClass() {
this.bye = superBye;
this.hello = superHello;
}
function subClass() {
this.bye = subBye;
}
subClass.prototype = new superClass;
function superHello() {
return "Hello from superClass";
}
function superBye() {
return "Bye from superClass";
}
function subBye() {
return "Bye from subClass";
}
var newClass = new subClass();
/*****************************/
// 動態添加的 blessyou 屬性
superClass.prototype.blessyou = superBlessyou;
function superBlessyou() {
return "Bless You from SuperClass";
}
/*****************************/
function printSub() {
alert(newClass.bye());
alert(newClass.hello());
alert(newClass.blessyou());
}
這就是我們經常看到的為內部對象, 如 String, Math 等再添加其它屬性和方法的技巧. 對於任何的內部對象和自訂對象, 你都也可以通過 prototype 來重載其下的屬性和方法. 那麼在調用執行時, 它將調用你所定義的方法和屬性. 下面是樣本:
// 為內部 String 對象添加方法
String.prototype.myMethod = function(){
return "my define method";
}
// 為內部 String 對象重載方法
String.prototype.toString = function(){
return "my define toString method";
}
var myObj = new String("foo");
alert(myObj.myMethod());
alert(myObj);
alert("foo".toString());
另外需要注意的是, 所有 JavaScript 內部對的 prototype 屬性都是唯讀. 你可以像上面那樣為內部對象的原型添加或重載屬性和方法,但不能更改該內部對象的 prototype 原型. 然而, 自訂對象可以被賦給新的原型. 也就是說, 像下面這樣做是沒有意思的.
function Employee() {
this.dept = "HR";
this.manager = "John Johnson";
}
String.prototype = new Employee;
var myString = new String("foo");
上面的程式在運行之後不會報錯, 但顯然, 如果你調用 myString.dept 將會得到一個非定義的值.
另外, 一個經常使用的是 prototype 下的 isPrototypeOf() 方法, 它主要用來判斷指定對象是否存在於另一個對象的原型鏈中. 文法如下:
object1.prototype.isPrototypeOf(0bject2);
上面的格式是用來判斷 Object2 是否出現 Object1 的原型鏈中. 樣本如下:
function Person() {
this.name = "Rob Roberson";
this.age = 31;
}
function Employee() {
this.dept = "HR";
this.manager = "John Johnson";
}
Employee.prototype = new Person();
var Ken = new Employee();
當執行 Employee.prototype.isPrototypeOf(Ken), Person.prototype.isPrototypeOf(Ken) 和 Object.prototype.isPrototypeOf(Ken) 時, 結果都會返回 true.
用於 Netscape 下的特定繼承檢測
在 Netscape 瀏覽器 4.x 到 6, 及其 Mozilla 系列瀏覽中, JavaScript 將對象間的原型關係儲存在一個特殊的內部屬性對象中, __proto__ (前後是 2 個底線). 下面是一個樣本:
function Shape() {
this.borderWidth = 5;
}
function Square() {
this.edge = 12;
}
Square.prototype = new Shape;
myPicture = new Square;
alert(myPicture.__proto__);
alert(myPicture.borderWidth);
由於指令碼執行過 Square.prototype = new Shape 語句, 所以 myPicture 具有了一個指向 Shape 對象的內部屬性 __proto__. 在指令碼的執行過程中, 當要擷取對象的某個屬性值, 並且此對象是通過原型賦值而建立的某個對象, 在自身並沒有對某個屬性進行定義時, JavaScript 解析器會查看它的 __proto__ 屬性對象, 也就它的原型對象, 然後枚舉其原型中的所有屬性, 而得出的結果要麼是有這個屬性, 要麼是沒有這個屬性. 如果沒有此屬性, 再枚舉原型對象下面的原型對象, 直到此過程真正的結束. 而所有的這些 JavaScript 引擎內部的操作, 我們是不會知道的, 下面的內容就是對這個問題的解釋.
其實, 對於所有的自訂對象, 無論它有沒有使用過 prototype 賦值操作, 它都具有一個 __proto__ 內部對象. 而如果某個對象是通過多層 prototype "繼承" 來的, 所有的 "繼承" 而來的屬性卻可以通過簡單的一層迴圈遍曆出來, 而不需要使用什麼遞迴演算法, 因為 JavaScript 引擎自動給我們做了. 樣本如下:
function Shape() {
this.borderWidth = 5;
}
function Square() {
this.edge = 12;
}
function RoundSquare()
{
this.radio = 0.5;
}
Square.prototype = new Shape;
RoundSquare.prototype = new Square;
var myPicture = new RoundSquare;
for (property in myPicture.__proto__) {
alert(property);
}
我們或者還可以通過更改後面的迴圈, 來遍曆某個子類對象繼承來的所有屬性, 如下:
for (property in RoundSquare.prototype) {
alert(property);
}
如果你不怕麻煩, 我們甚至還可以通過級連的方式, 取出其建構函式中定義的原始屬性值.
alert(myPicture.__proto__.__proto__.borderWidth);
無論你是否修改過此屬性值, 通過上面語句所取出的屬性值都是原始定義值. 讓我們沿著這個思路再往下看, 下面的代碼涉及到另外一個問題, 這個問題和原型鏈 (prototype chain) 有關. 代碼如下:
function State() {
}
function City() {
}
City.prototype = new State;
function Street() {
}
Street.prototype = new City;
var UniversityAvenue = new Street();
function tryIt() {
alert(UniversityAvenue.__proto__== Street.prototype);
alert(UniversityAvenue.__proto__.__proto__==
City.prototype);
alert(UniversityAvenue.__proto__.__proto__.__proto__
== State.prototype);
alert(UniversityAvenue.__proto__.__proto__.__proto__.
__proto__== Object.prototype);
alert(UniversityAvenue.__proto__.__proto__.__proto__.
__proto__.__proto__== null);
}
當執行 tryIt 函數時, 所有的顯示均為 true. 也就是說, 子類對象的 prototype.__proto__ 總是等於超類對象的 prototype 屬性; 超類對象的 prototype.__proto__ 總是等於 Object.prototype; Object.prototype.__proto__ 總是為 null; 而執行個體對象的 __proto__ 總是等於其類對象的 prototype, 這就是為什麼任何自訂對象都具有 __proto__ 屬性的原因. 對於上面的敘述, 其對應的代碼如下:
Street.prototype.__proto__ == City.prototype // true
State.prototype.__proto__ == Object.prototype // true
Object.prototype.__proto__ == null // true
UniversityAvenue.__proto__ == Street.prototype // true
類比實現 instanceOf 函數
根據上一節的內容, 我們瞭解了有關 Netscape 所支援的 __proto__ 特性的內容. 這一節, 我們將利用此特性來建立自己的執行個體對象檢測函數.
許多時候, 我們都需要判斷某個對象是否是由某個類來定義的, 在其它的語言裡, 你可以通過 instanceOf 函數來實現此判斷. 在 JavaScript 中同樣提供了一個 instanceof 運行符, 而在 __proto__ 的基礎上, 我們完全可以自己定義一個同樣的函數, 雖然這看上去是在重複勞動, 但有助於我們更深刻地瞭解有關 __proto__ 的知識. 下面的代碼只是用來說明功能, 在實際的應用中, 你不需要重複定義 instanceOf 函數, 使用 instanceof 運算子即可.
function instanceOf(object, constructorFunction) {
while (object != null) {
if (object == constructorFunction.prototype)
{return true}
object = object.__proto__;
}
return false;
}
function State() {
}
function City() {
}
City.prototype = new State;
function Street() {
}
Street.prototype = new City;
var UniversityAvenue = new Street();
function demo() {
alert("instanceOf(UniversityAvenue, Street) is " +
instanceOf(UniversityAvenue, Street));
alert("instanceOf(UniversityAvenue, City) is " +
instanceOf(UniversityAvenue, City));
alert("instanceOf(UniversityAvenue, State) is " +
instanceOf(UniversityAvenue, State));
}
你會看到所有的運行結果全部為 true, 其原理和上一節的級連判斷相等如出一轍. 實際證明, 它的運行結果和 instanceof 運行符的運行結果是一致的.
你可以通過 constructor 屬性來檢測任意對象的超類, 此屬性返回通過 new 運算子建立新對象時所調用的建構函式, 傳回值是 Function 物件類型. 因為 Object 內部對象是支援 constructor 屬性的, 並且有的對象 (包括內部對象和自訂對象) 都是由 Object 繼承而來的, 所以所有的對象都支援此屬性. 讓我們再看一下下面的例子:
function Employee() {
this.dept = "HR";
this.manager = "John Johnson";
}
function printProp() {
var Ken = new Employee();
alert(Ken.constructor);
}
調用完 printProp 函數之後, 你會看到彈出框中顯示的是 Employee 函數的定義文本, 其實 Ken.constructor 的傳回值本身是 Function 物件類型, 而在 alert 時被隱含地調用了 toString 方法. 對於類對象本身, 你同樣可以調用 prototype.constructor 來取出其建構函式.
對象的分類和列印
JavaScript 支援 3 種主要類型的對象: 內部對象, 宿主對象, 自訂對象, 可能還有特殊的外部對象, 如: ActiveX 對象或 XPCOM 物件. 內部對象被 JavaScript 語言本身所支援, 如: Object, Math, Number 對象等. 所有的內部對象的共同特點是以大寫字母開頭, 並且它們是大小寫敏感的. 如果你想使用數學常量 PI, 必須寫成 Math.PI, 你如果寫成 math.PI, JavaScript 會顯示錯誤. 宿主對象是被瀏覽器支援的, 目的是為了能和被瀏覽的文檔可以互動, 如: document, window 和 frames. 宿主對象的特點是所有對象全部以小寫字母開頭. 因為 JavaScript 本身就是大小寫敏感的, 所以你同樣不能將大小寫搞混. 剩下要說的就只是自訂對象了, 你可以隨便將你的對象定義成小寫或大小寫, 但是一定要符合基本的命名規範. 如下所示, 這就是一個自訂對象:
function employee() {
this.dept = "HR";
this.manager = "John Johnson";
}
function printProp() {
var ken = new Employee();
for (property in ken) {
alert(property);
}
}
前面我們已經提到過, 所有的內部對象和自訂對象都是從 Object 對象繼承而來的, 它是所有對象的超類對象. 你可建立一個 Object 對象的執行個體. 如下:
var myObject = new Object();
Object 類型的對象有許多的屬性和方法, 你可以查看相關的手冊. 上面只是定義了一個最簡單的Null 物件, 你還可以為 Object 建構函式傳入參數, 它會返回相應類型值的執行個體化對象. 記住, 傳回值的類型是某種物件類型的 (如: String, Number 或 Object). 這種方式和直接通過賦值字串或數值常量不同, 主要表示在類型方面. 如下所示:
var myObject = new Object("foo"); // 傳回值類型為 object
var myObject = new String("foo"); // 傳回值類型為 object, 效果同上
與
var myObject = "foo"; // 傳回值類型為 string
你可以從調試器的 type 列中看出這個細微的差別, 它是簡單類型與物件類型之間的區別. 但是, 你通過 alert 調用是看出不這些內部差別的, 因為在調用 alert 的過程中, 所有的物件類型值都會被自動調用 toString 方法進行字串類型轉換, 轉換規則在 JavaScript 手冊中有說明. 如果你 alert 的是某個自訂對象, 並且它沒有定義 toString 方法, 那麼它的傳回值將為 "[object Object]". 對於 Math 對象, 當你查看其 Math.constructor 屬性時, 你會得到一個不同於其它內部對象的內容為 "function Object()..." 的物件建構函數, 這與其它對象返回 "function Function()..." 的建構函式很不相同. 原因很簡單, 因為 Math 對象是不能通過 new 運算子進行建立的.
另外, 如果傳入 Object 建構函式中的值是一個對象, 它將原封不動地將該對象返回. 記住, 此操作只是一個引用, 而不是複製.
請求對象的屬性
在前面的範例程式碼中, 已經出現過以迴圈方式枚舉對象屬性的樣本. 其實, 通過 for...in 語句, 無論是任何對象和數組, 其下的元素, 屬性和方法都可以遍曆出來. 樣本如下:
function employee() {
this.dept = "HR";
this.manager = "John Johnson";
}
function printProp() {
var ken = new employee();
for (property in ken) {
alert(property + " : " + ken[property]);
}
}
在遍曆測試過程中, 你會發現, 對於自訂對象和宿主對象一般都可以枚舉出其下的屬性, 而對於內部對象, 幾乎沒有什麼屬性可以遍曆出來, 為什麼要說幾乎呢? 因為對於 Mozilla 核心的瀏覽和 IE 核心的瀏覽器, 其 JavaScript 引擎有不同, Mozilla 下可以枚舉出部分內容, 而枚舉的原則不得而知.
對於每一個對象, 你還可以使用 hasOwnProperty 方法來檢測其是否具有某個屬性或方法. 由於 hasOwnProperty 是 Object 對象下的方法, 因此所有的對象都具有此方法. 但是, 需要注意的是, 此方法只能檢測通過 this 關鍵字定義的成員, 如果某個成員是通過原型鏈定義的, 那麼此方法將返回 false. 也就是說, 通過 prototype 繼承來的屬性和方法, 及其通過 prototype 定義的屬性和方法, 都是不能通過 hasOwnProperty 來進行檢測的. 由此, 我們可以看出, 通過 this關鍵字定義的屬性和方法是同對象本身處於同一個地址空間內的; 而通過 prototype 定義的屬性和方法, 是通過所謂的 "原型鏈" 進行管理的, 其下的的屬性和方法不位於同一個地址空間之間, 當其調用這種屬性或方法時, 必須通過 "鏈表" 才能索引到其下的某個屬性或方法. 也就說, 調用以原型方式定義的屬性和方法會有一個類似於鏈表的 "回溯" 操作.
和 hasOwnProperty 差不多, 對於對象中的每個屬性, 我們還可以通過 propertyIsEnumerable 來測試它是否可以被枚舉出來. 如下所示:
function Employee1() {
this.dept = "HR";
this.manager = "John Johnson";
this.month = new Array("jan", "feb", "mar");
}
var Ken = new Employee1();
Ken.month.propertyIsEnumerable(0);
我們可以看到, 其文法是 propertyIsEnumerable 後跟數組的元素索引或對象中的屬性名稱. 同樣, 對於原型鏈中的屬性或方法它是不予考慮的, 結果當然是返回 false.
對於 JavaScript 和 Java 的比較
與 Java 這種基於類的語言不同, JavaScript 是一種基於原型的語言. 這種特點影響著每一個方面. 如術語 instance 在基於類的語言中有著特殊的意義, 它表示某個執行個體是隸屬於某個特殊類的獨立個體, 是對類定義的真實實現; 而在 JavaScript 中, 術語 instance 沒有這個意思, 因為在它的文法裡面, 類和執行個體是沒有區別的. 雖然, 執行個體可以用來說明某個對象是使用某個特殊的建構函式產生的. 如下所示:
function superClass() {
this.bye = superBye;
this.hello = superHello;
}
function subClass() {
this.bye = subBye;
}
subClass.prototype = new superClass;
function superHello() {
return "Hello from superClass";
}
function superBye() {
return "Bye from superClass";
}
function subBye() {
return "Bye from subClass";
}
var newClass = new subClass();
newClass 是 subClass 的執行個體, 它是通過 subClass 的建構函式產生的. 而如果使用基於類的語言呢, 如下所示是 Java 的等價實現.
public class superClass {
public superClass () {
this.bye = superBye;
this.hello = superHello;
}
}
public class subClass extends superClass {
public subClass () {
this.bye = subBye;
}
}
結束語
我們在上面的幾節中, 詳細地說明了有關 JavaScipt 中的物件導向實現, 或者只能說是類比實現. 在此期間向你展示了實現方法. 並且闡述了有關如何檢測對象之間關係的方法, 如何列印屬性和測試某個特定的屬性, 還做了一個 JavaScript 和 Java 的簡單比較. 但這顯然是不夠的, 因而, JavaScript 的物件導向編程是非常多樣化的, 格式也非常繁雜. 我打算在後面的內容裡, 再總結一下有問封裝的格式問題, 著重說明與對象方法有關的內容和實現, 同時還有上面提到的原型鏈 (prototype chain) 問題.