2010年1月24日 星期日

正名運動:Java與JavaScript不是直系血親

有相當多人認為Java與JavaScript存在著某種關係,更有人認為JavaScript其實是Java的變形體,然而若對JavaScript的歷史稍有研究,你可以發現JavaScript與Java可以說是一點關係也沒有,全都是名字與刻意模仿的命名誤導了大家。
JavaScript的原名為LiveScript,由Brendan Eich所設計,1996年誕生於網景(Netscape)公司。當時的網景公司是瀏覽器的霸主,為了推行這種新穎的動態網頁技術而和昇陽(Sun Mirco Systems)公司達成協議,將LiveScript更名為JavaScript(因為昇陽的Java Applet在當時曾風光一時,藉由Java的名號推廣新技術比較容易。),網景公司則以幫助昇陽研發Navigator瀏覽器上的虛擬機器(VM)做為交換條件,也因此JavaScript的商標權目前是屬於昇陽。換句話說,JavaScript最多只是Java的乾兒子,何況在程式語言的系譜上,Java與JavaScript的關係也不在同一條血脈上。Java在特性上承繼了C++與SmallTalk,有著封裝、繼承與多型三種傳統物件導向程式語言的主要特徵,反觀JavaScript,嚴格來說沒有封裝與繼承,更不用說多型。JavaScript雖然有成員變數與成員函式的觀念,但與其說那是封裝,不如說是沒有存取權限控制的關聯式陣列(associative array),例如在Java中宣告一個類別:

class JavaClass{
private String name;
int id;
}

String型態的name變數只能在其宣告的類別內存取,整數型態的id變數則可以在所屬package內存取,然而在JavaScript,當你這樣宣告:

function JavaScriptClass(){
this.name = "Default";
this.id = 0;
}

※JavaScript的實體建構函式(建構子)與一般函式是沒有分別的,因此當下文提到建構子,即是指程式中專門用來建構實體(new)的函式。

name和id的存取是不用考慮權限的,甚至可以用以下兩種方式來存取實體內的成員變數:

  1. var instance = new JavaScriptClass();
    instance["name"]; // = "Default"
    instance["id"]; // = 0

  2. var instance = new JavaScriptClass();
    instance.name; // = "Default"
    instance.id; // = 0

換句話說,instance變數可被看成含有name與id成員變數的物件,也可以視為是一個以字串為鍵(key)的關聯式陣列,建構子的宣告只是幫助我們將陣列內的鍵與值先做初始化。
在JavaScript中,要製造出多個有相同成員變數與成員函式的物件,宣告一個function來作為實體的建構子會比較方便,但如果實體只會被使用一次,也可以用下列的方式來宣告:

  1. var instance = new Object();
    instance.name = "JavaScript";
    instance.id = 1;
  2. // 或者是 var instance = new Array();
    var instance = new Object();
    instance["name"] = "JavaScript";

  3. instance = {
    name : "JavaScript",
    id : 3
    }

習慣寫Java的人可能會覺得第一種寫法很弔詭,但在JavaScript裡確實是行得通的:即使Object的實體裡不存在name與id變數,只要有指派,變數就自動被配置了。也因此第二種寫法看起來較直覺,只要把實體全部看成關聯式陣列(或 Hash Table)就行了。第三種寫法是以JSON(JavaScript Object Notation)的方式寫成,但如果你用過Ruby的關聯式陣列,就會覺得這種寫法用來宣告關聯式陣列是再正常不過了。
到這裡,很明顯可以看出Java與JavaScript的不同之處,然而他們最大的不同是在函式的特性:JavaScript的函式本質上仍屬於關聯式陣列的值,而非任何實體的成員函式。例如,當你在一個建構函式內宣告一個成員函式:

function JavaScriptClass(){
this.name = "Hello";
this.displayName = function(){
alert(this.name);
}
}

var obj = new JavaScriptClass();
obj.displayName();

雖然以上的程式會確實會彈出一個警告視窗顯示"Hello",但如果你的程式是像這樣寫,也會有同樣的結果:
function func(){
alert(this.name);
}

function JavaScriptClass(){
this.name = "Hello";
}

var obj = new JavaScriptClass();
obj.displayName = func;
obj.displayName();

※注意obj.displayName = func的func後面沒有大括號,懂C的人可以把它視為函式指標。

知道原因嗎?obj實體的displayName函式並不是成員函式,因此displayName函式中的this並不是函式所屬的物件,而是函式的「呼叫者」。換句話說,obj.displayName()呼叫時,displayName函式是到呼叫者(obj)去尋找name變數。下列寫法應會比較容易理解:
function func(){
alert(this.name);
}

function JavaScriptClass(){
this.name = "Hello";
}

var obj = new JavaScriptClass();
func.call(obj);

在JavaScript中,當一個函式被呼叫,實際上是呼叫函式物件中的call函式。call函式的第一個參數為該函式物件的呼叫者,也就是說函式內的this關鍵字會關聯到call函式的第一個參數,因此當func函式執行this.name變數時,實際上是到obj實體尋找name變數。了解其中的機制後,就可以用JSON當作參數傳入函式而得到同樣的效果:
function func(){
alert(this.name);
}

func.call({name:"Hello"});

JavaScript就是一個原理如此簡單的語言!

翻開族譜,JavaScript真正的祖宗其實是Self與Scheme,才會有像prototype與第一級函式的特性。那麼,誰才是Java真正的兒子?除了源自於Java的Groovy外,最像Java的語言應是那位認錯祖宗的C#吧!

沒有留言:

張貼留言