:::

JavaScript物件導向繼承:以prototype方式繼承中需要呼叫constructor的問題

布丁布丁吃布丁

JavaScript物件導向繼承:以prototype方式繼承中需要呼叫constructor的問題

image

Dean Edwards認為更好的JavaScript物件導向應該有的其中一個條件是「I want to avoid calling a class’ constructor function during the prototyping phase」(在原形宣告期間,要避免去呼叫類別的建構子),這是由於JavaScript傳統的prototype繼承方式中,總是一直去呼叫父類別的建構子,才能讓子類別進行繼承。也就是說,父類別建構子裡面做過的事情,在該類別「真正派得上用場」之前,就得先做過一次,白白消耗客戶端的記憶體及運算時間。

這一篇是為了記錄究竟什麼是「原形宣告期間(prototyping phase)就呼叫建構子(constructor)」的問題,以及父類別的建構子裡面盡量不要使用太多動作的消極建議,最後簡單地介紹使用Dean Edwards的Base類別改善方法的優缺點。

JavaScript的prototype繼承方式

以下我舉一個簡單的JavaScript以prototype方式繼承的兩個類別「A」跟「B」,說明一併寫在註解裡面:

//建立類別A,以下是建構子(constructor)
function A() {
    //在畫面中輸出[Call A's constructor],表示執行A的建構子
    document.write("[Call A's constructor]");
}

//A的方法,稍後會給B繼承
A.prototype.method = function () {
    //表示執行A的方法
    document.write("[Hello, world!]");
};

//建立類別B,以下是建構子
function B() {
    //在畫面中輸出[Call B's constructor],表示執行B的建構子
    document.write("[Call B's constructor]");
}

//prototype方式的繼承。注意,此處呼叫了A類別的建構子
B.prototype = new A();    //畫面輸出[Call A's constructor]

//實體化類別B到變數b中
var b = new B();    //畫面輸出[Call B's constructor]

//呼叫由類別A繼承來的方法
b.method();    //畫面輸出[Hello, world!]

執行到最後,畫面上會輸出「[Call A's constructor][Call B's constructor][Hello, world!]」。然而理想上,程式的最後只有實體化類別B、沒有實體化類別A,因此應該只要呼叫到類別B的建構子,而不需要呼叫到類別A的建構子。但是實際上這種prototype繼承方式,卻是會在繼承時強制地呼叫類別A的建構子,造成資源的消耗。

設計建構子的心得

其實不僅僅是上述的問題,就連類別中的「物件類別(Object)」屬性也會受到JavaScript屬於傳址運作而可能遭受子類別影響的缺點(詳細請看我另一篇的討論),而必須在建構子中加入初始化屬性、並且呼叫父類別建構子。這即使在Dean Edwards繼承庫中,也會有一樣的問題。

換句話說,重複呼叫父類別建構子,似乎是難以避免的一個難關。在此前提之下,就是盡量不要在建構子中寫入太多程式、執行太多動作,最簡單的只要將物件類別的屬性初始化就好。

有些JavaScript程式設計師會在類別的建構子中宣告許多動作,但在建構子會被重複呼叫的前提下,這顯然地並不是很好的作法。舉例來說,以下就是在類別的建構子中宣告屬性與方法的JavaScript類別寫法,但是這會在每次呼叫建構子時重複地被執行、佔用記憶體,因此並不是很好的寫法。

function test() {
    var private1 = "I'm private variable 'private1'!";//私有成員變數
    this.public1 = "I'm public valiable 'public1'!";//公開成員變數
    function getPrivateFriend() {//私有成員函數
        alert(private1);
    }
    this.getPrivatePublic = function() {//公開成員函數
        getPrivateFriend();
    }
}

儘管如此,唯一令人欣慰的是,在繼承階層中最底層的子類別則就沒有上述的限制,因為他們不需要被別人所繼承,而他們通常也是系統中最常在建構子寫入大量動作的類別。

改用Dean Edwards的Base類別如何?

Dean Edwards針對這個問題提出了改良後的JavaScript物件導向繼承類別:Base (SkyDrive備份),這種方式可以避免呼叫到上層類別的建構子。

以下是上述範例中使用Dean Edwards的Base改良後的程式碼,請注意必須先引用Base.js喔。

<script type="text/javascript" src="Base.js"></script>
<script type="text/javascript">
//建立類別A,繼承自Base類別
A = Base.extend({
    //建構子
    constructor: function () {
        //在畫面中輸出[Call A's constructor],表示執行A的建構子
        document.write("[Call A's constructor]");    
    },
    method: function () {
        //表示執行A的方法
        document.write("[Hello, world!]");
    }
});

B = A.extend({
    //建構子
    constructor: function () {
        //在畫面中輸出[Call B's constructor],表示執行B的建構子
        document.write("[Call B's constructor]");
    }
});

//實體化類別B到變數b中
var b = new B();    //畫面輸出[Call B's constructor]

//呼叫由類別A繼承來的方法
b.method();    //畫面輸出[Hello, world!]
</script>

最後畫面上會輸出「[Call B's constructor][Hello, world!]」 ,由此可知他並不會呼叫到類別A的建構子,而且整體程式的排版更為直觀、易讀。

image

但是這種方法的缺點在於JavaScript IDE不支援,即使是我使用過最聰明的Aptana Studio也看不懂Dean Edwards繼承類別的運作方式,所以上圖的自動提示中是找不到類別B所繼承的方法method。

結語

其實一開始看到Dean Edwards提出這個問題時,我還看不懂這是什麼意思。直到實作了一段時間之後才發現原來建構子真的一直被重複呼叫,特別是我在系統中使用了多層次的類別繼承,導致呼叫次數多到一種難以想像的地步。

我試著用prototype的方式來挑戰嘗試迴避這個缺點,但是並沒有成功地找出解法。看來還是得用Dean Edwards的Base類別之類的迂迴手法才能解決這個問題吧?但是Base類別會讓Aptana看不懂,會影響到開發時候的效率,各有優劣,就看大家怎麼考量了。不過為了解決這個問題,我也想到一種「偽裝繼承」的方式來讓Base能夠用在Aptana Studio當中,下次有機會再來講講。

(more...)

NetBeans的JavaScript編輯中呼叫自動完成功能(Code Completing)

布丁布丁吃布丁

NetBeans的JavaScript編輯中呼叫自動完成功能(Code Completing)

image

NetBeans是用來撰寫網頁程式語言的整合開發環境(IDE)。他的特色之一就是程式碼的自動完成提示(Smart Code Completion),這也是我認為IDE應該要有的基本功能之一。然而NetBeans在撰寫JavaScript的時候,除非手動打開自動完成功能,不然只有在輸入觸發關鍵字「.」的時候才會顯示自動完成提示。也就是說,像我輸入常用的字詞,例如「var」、「new」、「function」等關鍵字的時候,他根本就不會出現提示,然後我就會很手殘地老是把「function」打成「funciton」,程式出錯跑不出來。

為了要在輸入一般文字時就能呼叫自動完成功能,我需要使用NetBeans的快捷鍵Ctrl+Space,然而他卻很不幸地跟Windows的切換輸入法相衝。因此又得修改快捷鍵設定,才能順利地呼叫NetBeans的自動完成功能。

以下介紹中,我使用的是NetBeans 6.9版本,作業系統是Windows 7 64-bit。但不管哪個版本,應該大致上都大同小異。

預設呼叫自動完成的快捷鍵 Ctrl + Space

image

要呼叫自動完成的快捷鍵(Hotkey),預設是使用「Ctrl + Space」。同樣身為中文Windows使用者的您看到這邊一定是滿臉陰影,因為Ctrl + Space是Windows用來切換輸入法的快捷鍵,在NetBeans中根本就沒辦法用Ctrl + Space來呼叫。

NetBeans的FAQ裡面也有提到這個問題,而他建議去設定Keymap來修改這個快捷建設定。接下來我們也照著他的方法來修改NetBeans呼叫自動完成的快捷鍵吧。

修改快捷鍵設定為 Alt + Z

基於Aptana StudioSpket IDE等我常用的IDE的習慣,通常呼叫自動完成的快捷鍵是設定在Alt + Z,這個按鍵也非常容易使用,因此以下我介紹如何將NetBeans的自動完成快捷鍵設定為Alt + Z。

  1. 開啟Tools中的Options。
    image 
  2. 切換到Keymap分頁,並在Search in Shortcuts欄位中輸入空白鍵,你就可以找到「Show Code Completion Popup」,而他後面的Shortcut(快捷鍵設定)則是「Ctrl+SPACE」
    image
  3. 雙擊要修改的快捷鍵設定,並按下「Alt」跟「Z」,並按下「Enter」確定。按下Enter之後列表會跳掉,但是再回去Searhc Alt+Z的時候,就會看到修改已經完成:
    image 
  4. 按「Ok」按鈕完成設定,然後在寫JavaScript的時候就開心地使用Alt + Z來呼叫自動完成吧。
    image


結語

在尚未得知這個方法之前,我一直以為NetBeans的自動完成非常地彆腳。我喜歡Aptana那種輸入第一個字就會立刻帶出自動完成的設定,而至今我還沒有在其他JavaScript IDE看到能在這點與他相比的特性。然而退而求其次,在這種按「Alt + Z」才能呼叫自動完成的IDE中雖然需要多一個這樣的步驟,但習慣了也勉強可以接受。

NetBeans的自動完成提示最貼心的地方在於會將重要的屬性與方法往上提,而不是像Aptana把所有的名稱都依照字母排序,只是幫你找出符合開頭的文字。

image

然而遺憾的是,NetBeans的JavaScript分析器不如Aptana來得聰明。而且他說是支援JSDoc,可是又不是很標準的JSDoc語法。像是JSDoc網站上認定的繼承語法「@extends」在Aptana跟Spket IDE都可以使用,到了NetBeans當中就只能認得「@inherits」。

因此到了最後,我還是繼續用Aptana來開發JavaScript。真有種繞了一大圈之後又回來原地的感覺。

(more...)

談JavaScript使用prototype實作物件導向的探討

布丁布丁吃布丁

談JavaScript使用prototype實作物件導向的探討

image

JavaScript雖然是物件導向的程式語言,但是他的物件導向方法並不統一,跟我們在學校所學的C、Java或甚至是PHP等都有很大的差異。

最知名的JavaScript繼承方法之一是使用「prototype」(此處並不是在講Prototype的JavaScript Framework喔),Fillano(馮旭平)談論物件導向Javascript - 實作繼承的效果為prototype的繼承方法作了很多說明,網路上也有很多教學是以「prototype」方式進行,例如JSDoc在介紹使用方法中就有示範一段以prototype作為繼承的例子。

我在論文系統發展初期也是使用prototype來實作繼承,但是做到一半的時候,我發現prototype其實是有很多問題的。除了老是要撰寫「this.prototype.」的宣告開頭讓程式大小居高不下,prototype還有使用「傳址」(連結位址) 而非「傳值」(複製資料) 來初始化變數的缺點,這會導致繼承時上層類別的變數會因為傳址而被變更的問題。

這一篇就是要來談談JavaScript中以prototype實作繼承的這個問題,並且一一探討解決的方法。


prototype實作繼承的方法與問題

prototype是JavaScript物件中特殊的一種屬性,透過指定prototype屬性,便可以指定要繼承的目標。然而當類別的屬性為「物件」的資料型態(例如有個屬性為birthday物件,而該物件包含了year、month、day這三種屬性),而方法本身又是修改該屬性中的屬性(例如修改birthday中的year這個屬性)時,就會影響到上層物件的屬性內容。

以下我舉一個簡單的範例:「Ancestor」上層類別跟「Child」子類別。首先下圖是他的UML類別圖:

image

將上面的類別圖寫成JavaScript物件導向的繼承,就會變成下列的程式碼:

//上層類別
function Ancestor() {
    this.setBirthdayYear(1889);
}

//屬性
Ancestor.prototype.birthday = {
    year: null,
    month: null,
    day: null
};

//方法
Ancestor.prototype.setBirthdayYear = function (year)
{
    this.birthday.year = year;
}; 

//子類別
function Child() {
    //即使Child沒有宣告setBirthdayYear的方法,
    //也可以使用上層類別Ancestor的setBirthdayYear
    this.setBirthdayYear(1915);  
}

//Child繼承Ancestor
Child.prototype = new Ancestor();   

最重要的繼承方法是最後一行「Child.prototype = new Ancestor();」,透過此宣告,Child類別就能使用上層類別Ancestor的屬性birthday跟方法setBirthdayYear()。

乍看之下這樣是沒有問題的,但是實際上Child在執行setBirthdayYear時,卻會連帶地影響到Ancestor的birthday屬性。舉例來說:

var ancestor = new Ancestor;
var child = new Child;
document.write(ancestor.birthday.year);    //應該要顯示1889,但卻顯示1915
document.write(child.birthday.year);    //顯示1915

很遺憾地,Ancestor的birthday屬性的確被Child修改了。由此可知,這種JavaScript物件導向的實作方式有著傳址運作的缺陷。

改進prototype的實作方式:在constructor指定參數

要改善上述實作方式的方法之一,是在constructor(建構子)的時候指定參數,讓每次類別在實體化成為物件的時候,都去重新指定參數的內容。以下是修改之後的Ancestor類別:

//上層類別
function Ancestor() {
    //初始化屬性
    this.birthday = {
        year: null,
        month: null,
        day: null
    };
    
    this.setBirthdayYear(1889);
}

在建構子當中加入屬性初始化的設定之後,就算之後Child修改了Ancestor的屬性,因為Ancestor在實體化的時候每次都會將屬性初始化,所以Ancestor看起來就像是不會受到Child影響一樣。

值得注意的是,這只有在屬性為物件時才可能會受到影響,請對可能受到影響的屬性物件進行初始化,如果屬性的資料型態為字串、數字、布林值,那麼即使不進行初始化也是無所謂的。相關的探討請見JavaScript中參數的傳值與傳址探討

這樣做是正確的,但在多重繼承的時候,仍會有一些問題發生。請繼續看以下的探討。

多層繼承時將會導致問題發生

儘管上一節中為類別的建構子加入初始化屬性的設定,就可以暫時解決上層類別受到影響的問題,但是如果要實作多層繼承的時候,卻仍因為最下層類別不會去執行最上層類別的建構子,導致最後仍沒有進行初始化屬性的錯誤。

接下來我再舉一個例子作為說明,以下是多層繼承的UML類別圖:(雖然這並不是一個很標準的物件導向的例子,大家請不要見怪。)

image

現在我們有4個類別,各別是Grandfather爺爺、Father爸爸、Son兒子、Daughter女兒。他們用上述的prototype實作方式來寫成JavaScript程式碼之後,會如以下:

//最上層類別
function Grandfather() {
    this.birthday = {
        year: null,
        month: null,
        day: null
    };
    this.setBirthdayYear(1889);
}

//屬性
Grandfather.prototype.birthday = {
    year: null,
    month: null,
    day: null
};

//方法
Grandfather.prototype.setBirthdayYear = function (year)
{
    this.birthday.year = year;
}; 

//上層類別
function Father() {
    this.setBirthdayYear(1915);    
}

//Father繼承Grandfather
Father.prototype = new Grandfather();    

//子類別之一
function Son() {
    this.setBirthdayYear(1943);    
}

//Son繼承Father
Son.prototype = new Father();    

//子類別之二
function Daughter() {
    this.setBirthdayYear(1945);    
}

//Daughter繼承Father
Daughter.prototype = new Father();

類別名稱修改,並且加入了Son跟Daughter這兩個類別之外,基本上跟前面舉例中的Ancestor與Child沒有太大差別。但是這樣子執行的時候,卻會發生很大的問題:

var grandfather = new Grandfather();
var father = new Father();
var son = new Son();
var daughter = new Daughter();

document.write(grandfather.birthday.year);    //顯示1889
document.write(father.birthday.year);    //應該要顯示1915,但卻顯示1945
document.write(son.birthday.year);    //應該要顯示1943,但卻顯示1945
document.write(daughter.birthday.year);    //顯示1945

你可以注意到,Daughter修改了屬性的資料,連帶影響到了Father跟Son,也就是說,除了最上層Grandfather因為有在建構子時初始化屬性之外,其他沒有在建構子中初始化屬性的子類別仍會受到影響。

你可能想到說:那麼同樣地也在Father、Son、Daughter時初始化屬性不就得了?但這樣的作法是與物件導向中為了不要重複撰寫程式碼的原則相違背,因此並不推薦這麼做。

解決方式:在建構子呼叫上層類別的建構子

為了解決上述的問題,就必須在建構子呼叫上層類別的建構子,確保子類別每次實體話的時候也能夠初始化屬性。

在上述例子中,只要修改Father類別,讓他在建構子當中初始化就可以了。讓我們用JavaScript中經典的prototype base繼承法來取得並執行上層物件的建構子,修改之後的Father類別如下:

//上層類別
function Father() {
    this.base = Grandfather;
    this.base();
    this.setBirthdayYear(1915);    
}

加入了中間那兩行便可以呼叫上層類別的建構子,透過這種指定方式,this.base會被當做是Grandfather這個function(也就是Grandfather的建構子),然後在下面呼叫this.base()的時候,就會把Grandfather做過的事情再做一次,也就可以達到呼叫上層建構子的這個目的了。

除了這個方法之外,也可以使用call()函式來取得上層建構子。以call()來修改的Father類別如下:

//上層類別
function Father() {
    Grandfather.call(this);
    this.setBirthdayYear(1915);    
}

call()會將Grandfather裡面的this以輸入的參數取代並執行一遍,而現在輸入的參數是Father的this,因此就等於Father中去執行Grandfather的建構子一樣的意思。call()方法可以說是比base更為簡潔且直覺的實作方法,詳細的內容可以參考ECMA-262的13.2.1 [[Call]]方法的定義

最後讓我們重新看一下以call()修改之後的四個類別多層繼承的程式碼:

//最上層類別
function Grandfather() {
    this.birthday = {
        year: null,
        month: null,
        day: null
    };
    this.setBirthdayYear(1889);
}

//屬性
Grandfather.prototype.birthday = {
    year: null,
    month: null,
    day: null
};

//方法
Grandfather.prototype.setBirthdayYear = function (year)
{
    this.birthday.year = year;
}; 

//上層類別
function Father() {
    Grandfather.call(this);
    this.setBirthdayYear(1915);    
}

//Father繼承Grandfather
Father.prototype = new Grandfather();    

//子類別之一
function Son() {
    Father.call(this);
    this.setBirthdayYear(1943);    
}

//Son繼承Father
Son.prototype = new Father();    

//子類別之二
function Daughter() {
    Father.call(this);
    this.setBirthdayYear(1945);    
}

//Daughter繼承Father
Daughter.prototype = new Father();

//輸出檢驗
var grandfather = new Grandfather();
var father = new Father();
var son = new Son();
var daughter = new Daughter();

document.write(grandfather.birthday.year);    //顯示1889
document.write(father.birthday.year);    //顯示1915
document.write(son.birthday.year);    //顯示1943
document.write(daughter.birthday.year);    //顯示1945

結語

由於JavaScript是傳址的方式運作,所以在物件導向上會發生很多出乎意料之外的問題,讓我在這兩個禮拜一直在找解決的方法。這一篇寫了又改、改了又寫,反反覆覆地好多次,總算是把JavaScript用prototype的繼承方法有個比較完整的整理。網路上的資料零零散散地看了很多,大致上把比較有印象的列在下方供大家參考。寫這篇也是對自己這些日子學習JavaScript的繼承方法有一個交代,作為學習的一個筆記。

原本我用prototype的方式來寫JavaScript的繼承,但因為發現了這個問題,於是又想改用Dean Edwards的Base工具來實作繼承,但是這卻導致Aptana這個JavaScript IDE看不懂,讓IDE整個武功盡失。在找尋更為理想的解決方式時,又回來整理這一篇。

image

到目前為止,我發現Aptana IDE可以完全理解這種prototype繼承方法,甚至不需要用JSDoc的@extends標註繼承的對象,Aptana也可以理解並帶出上層類別的方法或屬性。

接下來我想要繼續研究一個可以支援我需要的物件導向功能,並且又能讓Aptana這種JavaScript IDE讀取分析的理想實作方式。待我程式實作到一定程度時,確認都沒有問題了,我再把我的方法整理之後寫上來吧。


參考資源

(more...)

JavaScript中參數的傳值與傳址心得

布丁布丁吃布丁

JavaScript中參數的傳值與傳址心得

image
JavaScript中參數傳遞到底是「傳值」(passing by value) 還是「傳址」(passing by refernce) ,似乎常常造成許多人混亂。我自己原本也一直以為JavaScript只是傳值而非傳址,而為此混亂了好一陣子。特別是在撰寫物件導向的JavaScript程式時更容易造成混亂。
有人認為JavaScript的函數(function)會依據你輸入參數的資料類型來區分傳值還是傳址,物件(object)的話會使用傳址,非物件的則是使用傳值。但實際上,我認為JavaScript的本質是傳址而非傳值。而端看你在函數裡面是否修改參數的記憶體位址(也就是建立新的資料給參數),來決定是否影響函數之外的參數資料。
以下我們就來一一地檢視一下JavaScript究竟是傳值或是傳址的參數傳遞運作方式吧。

2016/1/14更新:根據底下留言網友討論指出了我原文的錯誤,可以發現其實JavaScript本質仍是「傳值而非傳址」。雖然下面文章仍放著給大家參考,但下面的討論更有看頭。希望觀看這篇的讀者能夠連下面的討論一併閱讀,方能深入瞭解JavaScript的奧妙。

參數傳值:Number、String、Boolean等非Object類型

當輸入給函數的參數類型為非物件(object)的資料型態,例如Number(數字)、String(字串)或是Boolean(布林邏輯值)的時候,JavaScript的函數會以傳值的方式來處理。傳值的意思是說,輸入到函數裡面的參數會被複製一份,而在函數裡面對參數的操作,不會影響到外在參數的影響。
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為字串
var paramA = 'original';

//確認變數資料
document.write(paramA + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamA(paramA)
{
    //修改參數
    paramA = 'changed';
    
    //確認被修改之後的參數資料
    document.write(paramA + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamA(paramA);    //在函數中運作,輸出 changed

//被函數修改之後,仍然維持原本的資料
document.write(paramA + '<br />');    //輸出 original
</script>

上面的例子的輸出結果將會是:
original
changed
original

由此可知,JavaScript函數中使用參數為字串資料類型時,將會是以傳值的方式運作。同樣的道理也適用於數字、布林值上。

參數傳址:Object

當輸入給函數的參數資料類型為Object(物件)時,JavaScript會以傳址的方式來處理。傳址的意思是說,輸入的函數裡面的參數只是「記憶體中的位置」,而在函數裡面對參數的操作,其實是對記憶體位置中的該物件進行修改,因此函數之外的參數也會受到影響。
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為物件
var paramB = {
    attr: 'original'
};

//確認變數資料
document.write(paramB.attr + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamB(paramB)
{
    //修改參數。注意修改的方式,是直接指定物件的屬性進行修改,而非建立新的物件。    
paramB.attr = "changed";
    
    //確認被修改之後的參數資料
    document.write(paramB.attr + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamB(paramB);    //在函數中運作,輸出 changed

//被函數修改之後,物件的屬性也跟著改變了
document.write(paramB.attr + '<br />');    //輸出 changed
</script>

上面例子中輸出的結果將會是:
original
changed
changed

在上面的例子中,你可以發現到以物件資料型態輸入函數中的參數,在函數中被修改之後,在函數之外也會跟著受到影響,一般來說這會被視為傳址的參數傳遞方式。

再探Object的參數傳遞

在上一節介紹傳址的例子中,函數修改參數的方式是指定物件的屬性進行修改。但是如果函數修改參數的方式是直接指定新的物件、或是輸入其他的資料類型,那麼函數之外的參數就不會受到修改,也就是傳值的參數傳遞。
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為物件
var paramC = {
    attr: 'original'
};

//確認變數資料
document.write(paramC.attr + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamC(paramC)
{
    //修改參數。注意修改的方式,是建立新的物件,而新的物件裡面也包含attr屬性。
    paramC = {
        attr: 'changed'
    };
    
    //確認被修改之後的參數資料
    document.write(paramC.attr + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamC(paramC);    //在函數中運作,輸出 changed

//被函數修改之後,仍然維持原本的資料
document.write(paramC.attr + '<br />');    //輸出 original
</script>

上面例子中輸出的結果將會是:
original
changed
original

你會發現到,即使輸入參數的資料類型為物件,但是當你在函數中為參數指定新的資料的時候,函數之外的參數並不會受到影響,是為傳值參數傳遞的運作方式。

推測JavaScript是傳址方式運作

因此由以上的例子中,我可以得到一個推測的結論:JavaScript一直都是用傳址的參數傳遞。只是根據函數對於參數是否變更參數的記憶體位址,而會有看起來像是傳值或傳址的運作差異。
在以物件傳遞、修改物件屬性的paramB例子中,函數本身並沒有修改參數的記憶體位址,也就是沒有給予paramB新建立的資料(新的資料就是一個新的記憶體位址),因此在函數中被修改的paramB,函數之外也會受到影響。
而paramA跟paramC的例子裡都是建立了新的資料給參數,修改了參數本身的記憶體位址,讓函數裡面運作的參數跟函數之外兩者互不相干,因此乍看之下就類似傳值的運作方式。

參數傳值或是傳址運作:Array

那麼我們再回頭來看看JavaScript中一種特殊的資料型態:Array(陣列)。陣列本質上屬於「Object」,那麼把陣列作為參數輸入函數時,究竟是傳值還是傳址呢?由前面的推論中可知,這是根據函數裡面對於參數的操作是否參數為建立新的資料來判斷
以下是一段示範用的程式碼:
<script type="text/javascript">
//宣告變數,資料類型為陣列
var paramAry = ['original'];

//確認變數資料
document.write(paramAry[0] + '<br />');    //輸出 original

//定義函數,在函數中修改參數資料,並且輸出做確認
function changeParamAry1(paramAry)
{
    //修改參數。注意修改的方式,是建立新的陣列。
    paramAry = ['changed'];
    
    //確認被修改之後的參數資料
    document.write(paramAry[0] + '<br />');    //輸出 changed
}

function changeParamAry2(paramAry)
{
    //修改參數。注意修改的方式,是修改陣列裡面的索引,而非建立新的陣列。
    paramAry[0] = 'changed';
    
    //確認被修改之後的參數資料
    document.write(paramAry[0] + '<br />');    //輸出 changed
}

//在函數中修改參數
changeParamAry1(paramAry);    //在函數中運作,輸出 changed

//被函數修改之後,仍然維持原本的資料
document.write(paramAry[0] + '<br />');    //輸出 original

//在函數中修改參數
changeParamAry2(paramAry);    //在函數中運作,輸出 changed

//被函數修改之後,參數的資料已經被修改
document.write(paramAry[0] + '<br />');    //輸出 changed
</script>

上面例子中輸出的結果將會是:
original
changed
original
changed
changed

因此你可以發現到,根據函數對於參數的操作,就會造成傳值或傳址的運作差異。

結語

當JavaScript越寫越複雜之後,對於參數的傳值或傳址就會越來越注重。也許是我讀的JavaScript的書不夠多,像這種基礎的概念居然都不是從書上得來,而是在網路上找尋各個程式設計師的心得與探討之後,再整理出來的。
題外話,因為這篇主要是整理理論的東西,沒什麼圖片。秉持著一篇文章一定要有一張圖的精神,就去ICON FINDER找了張JavaScript的圖示來貼在開頭XD 而最近都在談JavaScript的東西,因此我也為這個Blog加入了JavaScript的分類標籤。之前的發文就待有空時再來整理歸類到JavaScript標籤去吧。

參考資源

(more...)

Spket IDE使用jQuery自動提示

Spket IDE使用jQuery自動提示

image

Spket IDE是網路上知名的JavaScriptXML編輯器,有許採用jQueryExtJS工具庫的開發者都很喜歡使用Spket,而我在找尋理想的JavaScript IDE的時候,也來安裝Spket IDE來使用看看。

自動提示(Code Assist)是IDE中很重要的功能之一,效果就如上圖一樣,讓程式設計師在撰寫程式的時候,自動帶出相關的字詞,不僅減少程式設計師打字的工作量,也能夠避免輸入錯誤語法的人為失誤。然而似乎不少Spket IDE的使用者不清楚如何使用jQuery或ExtJS的自動提示功能,不僅Skpet IDE官方網站上有文字影片(簡體字?)的說明之外,Google中搜尋Spket IDE的介紹中也幾乎都是在教怎麼配置Profile

然而我照著教學上面的方法來做,卻始終帶不出jQuery的自動提示。找尋許多資料之後,終於在Spket IDE的論壇中發現了解決方法,因此想說在此整理一下,給有需要的朋友們一個引路。

環境敘述

image

Spket IDE可以作為plugin安裝在eclipse系列 3.2.x的IDE當中,而我是使用Aptana Studio 2,這是使用eclipse 3.5.2為基礎的IDE,也一樣可以安裝Spket IDE。而作業系統是Windows 7 64位元。

用網站安裝無效

image

原本我是使用eclipse的Install Software,連到Spket IDE的網站 http://www.spket.com/update 去下載1.6.18版本來安裝,但是這個版本似乎有問題,所以安裝之後仍然無法順利使用。

手動下載Plugin並安裝

根據論壇的說法,我將我的Skpet IDE安裝以及jQuery Profile配置的方法整理如下:

  1. 先安裝eclipse系列的IDE編輯器,例如Aptana Studio 2
  2. 下載com.spket.js_1.6.18.jar (SkyDrive備份)。
  3. 將com.spket.js_1.6.18.jar放置到eclipse安裝目錄底下的「plugins」資料夾。以Aptana Studio 2來說,就是「D:\Program Files\Aptana Studio\plugins\com.spket.js_1.6.18.jar」(因為我把Aptana安裝到D磁碟分割去了)
  4. 開啟eclipse,沒有錯誤訊息的話,應該就是安裝完成了。

接著是配置jQuery Profile:

  1. 打開「Window > Preferences」:
    image
  2. 左邊導覽列中,找到「Spket > JavaScript Profiles」,然後點選右上角的「New」以建立新的Profile:
    image
  3. 輸入Profile名稱,你看得懂就好,那就設定為「jQuery」吧。
    image
  4. 現在Profiles裡面多了一個剛剛建立的jQuery Profile,選擇他並點選右邊的「Add Library」:
    image
  5. 在選擇Library的下拉式選單中,選擇「jQuery」,然後按下ok完成:
    image
  6. 接著要加入jQuery的檔案,請點下右方的「Add File」:
    image
  7. jQuery的檔案最好是附加上說明的詳細版本,而不是一般使用的min版本,這樣在自動提示的時候才會把方法的說明也一併帶出來。在此推薦使用阿良翻譯的jQuery 1.3.2文件 (SkyDrive備份),請解壓縮之後選擇裡面的「jquery-1.3.2-jsdoc-Spket-profile.js」即可。
    image
  8. 完成後,選擇左方jQuery的Profile,並按下右邊的「Default」,讓Spket IDE把這個Profile作為預設的環境。
    image

儘管他們的版本號都是1.6.18,可是論壇上面的jar似乎比較正確。安裝正確的版本之後,不僅可以顯示jQuery的自動完成提示,也可以開啟Profile Explorer來查詢可用JavaScript語法。如下圖:

image


結語

儘管總算讓Spket IDE可以讀取jQuery的自動提示了,但是對於自己寫的程式,Spket IDE依然是不會弄出自動提示。接著我要繼續研究怎麼讓我寫的程式加入Spket IDE的自動提示中,如果這點可以做到的話,那麼Spket IDE就幾乎稱得上是理想的JavaScript IDE了。

(more...)

eclipse開啟時發生無法載入「jvm.dll」問題的解決方法

布丁布丁吃布丁

eclipse開啟時發生無法載入「jvm.dll」問題的解決方法

image

eclipse是知名的自由(也可以免費取用)的跨平台整合開發環境(Integreated Development Environment,簡稱IDE),主要用來開發Java,但他擁有強大的自訂能力,而使得eclipse也可以用來開發C/C++PHP或甚至是JavaScript。而許多IDE也是基於eclipse再擴充、發展而成,像是我目前主要用來開發JavaScript的Aptana Studio 2

問題敘述

eclipse能夠跨平台是基於Java運作環境的功勞,而他本身也是一個免安裝的檔案,理論上在良好設定的前提下,下載之後、解壓縮並直接開啟就能夠啟動。

最近為了再次找尋更完善的JavaScript IDE,所以我下載了Eclipse IDE for JavaScript Web Developers來使用。但沒想到下載完、解壓縮並開啟之後,出現了「Failed to load the JNI shared libray “D:\Program Files\Java\jre6\bin\client\jvm.dll”」的錯誤訊息,讓eclipse無法順利啟動。

這個問題顯然是我的Java環境參數哪裡弄錯了。我的電腦是Windows 7 64位元,安裝了Java的JDK 6.0.21,除了安裝路徑移至D磁碟分割之外,其他都跟預設一樣。

修改eclipse.ini,失敗

image

網路上可以找到兩種解決方法,一個是修改位於eclipse目錄底下的「eclipse.ini」設定檔。這個作法在「eclipse打开出现JVM terminated.Exit Code=-1错误的解决办法」這篇的後半部有提到,但我試著做卻很遺憾地沒能解決這個問題,而是需要用另一種方法來解決。

開啟eclipse時指定Java機器,成功

image

另一個方法是在開啟eclipse的時候,同時指定正確的Java虛擬機器路徑作為參數。作法如下:

  1. 找到你Java虛擬機器的路徑。以我的電腦為例子,因為Java安裝到D:\Program Files\裡面去了,所以路徑為「D:\Program Files\Java\jdk1.6.0_21\bin\javaw.exe
  2. 為「eclipse.exe」建立捷徑「eclipse.exe - 捷徑」,名稱可以隨意修改。
  3. 在捷徑上按右鍵,進入「內容」。
  4. 在「捷徑」分頁中找到「目標」欄位。
    image
  5. 在目標欄位資料後面加上「-vm "D:\Program Files\Java\jdk1.6.0_21\bin\javaw.exe"」,參數即是第一步中查詢的Java虛擬機器的路徑。舉例來說,原本的資料為「"D:\Program Files\eclipse\eclipse.exe"」,現在改為「"D:\Program Files\eclipse\eclipse.exe" -vm "D:\Program Files\Java\jdk1.6.0_21\bin\javaw.exe"」。
  6. 點擊捷徑,開啟eclipse。

image

開始使用eclipse吧!


結語

這其實是我第三次還是第四次遇到同樣的問題,而我卻每次都還是上網去找解決方法,找得灰頭土臉的才得到每次都一樣的解決方法。所以在此特地把這個方法寫在Blog,希望自己下次不要再這麼辛苦了。

題外話,Eclipse IDE for JavaScript Web Developers的JavaScript Editor在Aptana當中已有內建,而且也沒有Aptana JS Editor好用,著實讓人失望。下次有機會我在來分析一下JavaScript IDE的優缺點吧。

(more...)

論文進度報告(2010/9/26):進度延後至10/29

布丁布丁吃布丁

論文進度報告(2010/9/26):進度延後至10/29

image

9月中meeting時,我跟老師報告了一下系統狀況與現在的進度。當然就如你所知的,系統開發並不是很順利。前端的JavaScript程式完成了估計數量的1/3不到而已,之前說想要在9/21完成的希望,當然也是只能摸摸鼻子地再來重新估算日期。

系統現況與進度報告投影片

這是9/15論文進度報告的投影片(SkyDrive下載)。

前端的View部分,系統目前可以分成六大區塊:Toolkit工具箱、Core核心元件、Window視窗元件、Toolbar工具列、Text標註、Search搜尋。而目前我正在做到Toolbar這一塊,比報告之後又多推了一些進度,但跟上次報告時比起來也不過是多作了7隻程式而已,中間相隔約2個禮拜。

以下是進度的詳細內容:

元件 預估程式數量 已完成 UML類別圖
Toolkit 15 15 1
Core 15 13 1
Window 4 4 1
Toolbar 36 3 2
Text 38+? 0 3
Search 6+? 0 1
統計 114+? 35 (30.7%) 9

專案進度延後,設定10/29作為查核點

image

系統延後的狀況實在是太嚴重,嚴重到我都快對不起KALS Wiki左上角的倒數計時器。但是正視自己的弱點才能夠前進,所以還是要忍辱地再次調整專案規劃。

如果以兩個禮拜可以增加7隻程式來計算,大概還需要5個月的時間……當然不可能會到這麼久。而且這兩個禮拜中,又是寫書校稿、又是假期回家,所以進度才比慢到這麼誇張。不過都已經9月底的現在,說什麼理由也沒啥意義了。

總之,接下來兩個禮拜應該可以專心在系統上,因此要用這兩個禮拜來預估系統進度,然後設定10/29(五)作為查核點,再給自己一個月,看看情況如何吧。

image

由於整個進度都往後順延,影響到最後畢業的時程,延遲至3月初。由於我仍欠國中圖一篇論文,所以千萬得在3/24之前完成口試才行。

寫書一校完畢

image

之前提到的寫書工作,終於完成一校的作業了。

一校作業是由我跟老師分擔負責,我主要是檢查技術部分是否有問題。而不出所料地,我又作了很多修改。讓人有種「校稿怎麼校都校不完」的感覺。後來編輯又來告知說我圖檔dpi不足300,印刷時會讓圖片花掉,讓我又花了好多時間再來重新截圖。儘管現在都做完了,但二校時可能也會再來一次這種大混亂吧。

雖然老師希望該書在9月底能夠出版,但看現在的情況,能在10月底之前出版也許就該偷笑了?

與外國作者討論程式的感想

image

我在撰寫論文程式時,使用了Max Wheeler的PlaceHeld的外掛,並改進了裡面的一些小bug。這次心血來潮,上github將我的建議寫給作者Max,而他也在數天之後回了我。但他並不知道這樣改的意義,所以我又寫了一些範例回給他,現在在等他的回應。

以往我改了很多人寫的程式(特別是DSpace),有不少會在這個Blogger中寫出我改過的地方,但卻很少跟原作者交流、互動。我想,如果要成長的話,就一定要積極地與人交流知識才行。光是閉門造車,是很難有所成就的。

嗯,加油!


以往我總是將許多主題混在論文進度報告裡面一起寫,讓閒聊的事情與可能比較有教學價值的文章內容混淆。現在我打算把一些具有獨立探討價值的議題從論文進度報告中拆開來,進度報告歸進度報告(還有很多閒聊)、其他議題歸其他議題。也許這樣會對想要一次看完的讀者來說比較辛苦,但我相信將議題獨立探討,應該是更方便讀者找尋資料。

也因此,最近我發了很多篇文章,而談論議題都比較獨立。事實上,也還有好幾篇想寫議題列在待辦事項中,等待我一篇一篇地將他們完成。

(more...)