重新認識物件導向
為了說明 JavaScript 是一門徹底的物件導向的語言,首先有必要從物件導向的概念著手 , 探討一下物件導向中的幾個概念:
以這三點做為依據,C 是半物件導向半面向過程語言,因為,雖然他實作了類別的封裝、繼承和多態,但存在非物件性質的全域函數和變數。 Java、C# 是完全的物件導向語言,它們透過類別的形式組織函數和變量,使其無法脫離物件存在。但這裡函數本身就是一個過程,只是依附在某個類別上。
然而,物件導向只是一個概念或程式設計思想而已,它不應該依賴某個語言存在。例如 Java 採用物件導向思想建構其語言,它實作了類別、繼承、衍生、多型、介面等機制。但是這些機制,只是實現物件導向程式設計的一種手段,而非必須。換言之,語言可以根據其自身特性選擇合適的方式來實現物件導向。所以,由於大多數程式設計師首先學習或使用的是類似Java、C 等高階編譯型語言(Java 雖然是半編譯半解釋,但一般做為編譯型來講解),因而先入為主地接受了「類別」這個物件導向實作方式,從而在學習腳本語言的時候,習慣性地用類式物件導向語言中的概念來判斷該語言是否為物件導向語言,或者是否具備物件導向特性。這也是阻礙程式設計師深入學習並掌握 JavaScript 的重要原因之一。
實際上,JavaScript 語言是透過一種稱為 原型(prototype)的方式來實現物件導向程式設計的。以下就來討論 基於類別的(class-based)物件導向和 基於原型的 (prototype-based) 物件導向這兩種方式在建構客觀世界的方式上的差異。
基於類別的物件導向和基於原型的物件導向方式比較
在基於類別的物件導向方式中,物件(object)依靠 類別(class)來產生。而在基於原型的物件導向方式中,物件(object)則是依賴 構造器(constructor)利用 原型(prototype)建構出來的。舉個客觀世界的例子來說明二種方式認知的差異。例如工廠造一輛車,一方面,工人必須參考一張工程圖紙,設計規定這輛車應該如何製造。這裡的工程圖就好比是語言中的類別(class),而車子就是按照這個類別(class)製造出來的;另一方面,工人和機器( 相當於constructor) 利用各種零部件如發動機,輪胎,方向盤( 相當於prototype 的各個屬性) 將汽車構造出來。
事實上關於這兩種方式誰更為徹底地表達了物件導向的思想,目前仍有爭論。但筆者認為原型式物件導向是一種更徹底的物件導向方式,理由如下:
首先,客觀世界中的物件的產生都是其它實體物件建構的結果,而抽象的「圖紙」是不能產生「汽車」的,也就是說,類別是一個抽象概念而非實體,而物件的產生是一個實體的產生;
其次,按照一切事物皆對象這個最基本的物件導向的法則來看,類別(class) 本身並不是一個對象,然而原型方式中的建構器(constructor) 和原型(prototype) 本身也是其他物件透過原型方式構造出來的對象。
再次,在類別物件導向語言中,物件的狀態(state) 由物件實例(instance) 所持有,而物件的行為方法(method) 則由宣告物件的類別所持有,並且只有物件的結構和方法能夠被繼承;而在原型式物件導向語言中,物件的行為、狀態都屬於物件本身,並且能夠一起被繼承(參考資源),這也更貼近客觀實際。
最後,類別物件導向語言例如 Java,為了彌補無法使用過程導向語言中全域函數和變數的不便,允許在類別中宣告靜態 (static) 屬性和靜態方法。而實際上,客觀世界不存在所謂靜態概念,因為一切事物皆物件!而在原型式物件導向語言中,除內建物件 (build-in object) 外,不允許全域物件、方法或屬性的存在,也沒有靜態概念。所有語言元素 (primitive) 必須依賴物件存在。但由於函數式語言的特點,語言元素所依賴的物件是隨著執行時間 (runtime) 上下文 (context) 變化而變化的,具體體現在 this 指標的變化。正是這種特徵更貼近 「萬物皆有所屬,宇宙乃萬物生存之根本」的自然觀點。
JavaScript 物件導向基礎知識
雖然 JavaScript 本身是沒有類別的概念,但它仍然有物件導向的特性,雖然和一般常見的物件導向語言有所差異。
簡單的建立一個物件的方法如下:
function myObject() { }; JavaScript 中创建对象的方法一般来说有两种:函数构造法和字面量法,上面这种属函数构造法。下面是一个字面量法的例子: var myObject = { };
如果僅僅需要一個對象,而不需要對象的其它實例的情況下,推薦用字面量法。如果需要物件的多個實例,則建議函數建構法。
定義屬性和方法
函數構造法:
function myObject() { this.iAm = 'an object'; this.whatAmI = function() { console.log('I am ' + this.iAm); }; };
字面量法:
var myObject = { iAm : 'an object', whatAmI : function() { console.log('I am ' + this.iAm); } };
以上兩種方法所建立的物件中,都有一個名為 “iAm” 的屬性,還有一個名為 “whatAmI” 的方法。屬性是物件中的變量,方法則是物件中的函數。
如何取得屬性及呼叫方法:
var w = myObject.iAm; myObject.whatAmI();
呼叫方法的時候後面一定要加上括號,如果不加括號,那麼它只是傳回方法的參考而已。
兩種創建物件方法的區別
對於字面量法所建立的對象,可以直接用對象的參考呼叫其屬性或方法:
myObject.whatAmI();
而對於函式建構法而言,需要建立物件的實例,才能呼叫其屬性或方法:
var myNewObject = new myObject(); myNewObject.whatAmI();
使用建構子
現在再來迴歸一下之前的函數構造法:
function myObject() { this.iAm = 'an object'; this.whatAmI = function() { console.log('I am ' + this.iAm); }; };
其實它看起來就是個函數,既然是函數,能不能傳參數給它呢?將程式碼再稍作修改:
function myObject(what) { this.iAm = what; this.whatAmI = function(language) { console.log('I am ' + this.iAm + ' of the ' + language + ' language'); }; };
再將物件實例化,並傳入參數:
var myNewObject = new myObject('an object'); myNewObject.whatAmI('JavaScript');
程式最終輸出 I am an object of the JavaScript language。
兩種建立物件的方法,我該用哪一種?
對於字面量方法而言,因為它不需要實例化,所以如果修改了某物件的值,那麼這個物件的值就永久地被修改了,其它任何地方再訪問,都是修改後的值。而對於函數建構法而言,修改值的時候是修改其實例的值,它可以實例化 N 個物件出來,每個物件都可以擁有自己不同的值,而且互不干擾。比較以下幾段程式碼。
先看字面量法:
var myObjectLiteral = { myProperty : 'this is a property' }; console.log(myObjectLiteral.myProperty); // log 'this is a property' myObjectLiteral.myProperty = 'this is a new property'; console.log(myObjectLiteral.myProperty); // log 'this is a new property'
即便創建了一個新的變數指向這個對象,結果還是一樣的:
var myObjectLiteral = { myProperty : 'this is a property' }; console.log(myObjectLiteral.myProperty); // log 'this is a property' var sameObject = myObjectLiteral; myObjectLiteral.myProperty = 'this is a new property'; console.log(sameObject.myProperty); // log 'this is a new property'
再看函數構造法:
// 用函数构造法 var myObjectConstructor = function() { this.myProperty = 'this is a property' }; // 实例化一个对象 var constructorOne = new myObjectConstructor(); // 实例化第二个对象 var constructorTwo = new myObjectConstructor(); // 输出 console.log(constructorOne.myProperty); // log 'this is a property' // 输出 console.log(constructorTwo.myProperty); // log 'this is a property' 和预期一样,两个对象的属性值是一样的。如果修个其中一个对象的值呢? // 用函数构造法 var myObjectConstructor = function() { this.myProperty = 'this is a property'; }; // 实例化一个对象 var constructorOne = new myObjectConstructor(); // 修改对象的属性 constructorOne.myProperty = 'this is a new property'; // 实例化第二个对象 var constructorTwo = new myObjectConstructor(); // 输出 alert(constructorOne.myProperty); // log 'this is a new property' // 输出 alert(constructorTwo.myProperty); // log 'this is a property'
可以看到,用函數構造法實例化出來的不同對象,相互是獨立的,可以各自擁有不同的值。所以說,到底用哪一種方法來創建對象,需取決於各自實際情況。