:::

我的論文備份策略:Dropbox

布丁布丁吃布丁

我的論文備份策略:Dropbox

image

Dropbox是一個知名的免費雲端檔案同步工具,他能滿足我對於檔案備份的多個需求,包括全自動、雲端、版本備份。一開始帳戶提供了2GB的空間,可以再透過完成開始教學動作(+250MB)、註冊學校信箱(+512MB)、邀請其他使用者(+256MB每人/+512MB每學生),最大可以到18.3GB的免費空間。

其實我自己也是希望能夠推薦這個好工具給大家使用,並且藉此提昇自己的Dropbox容量XD 如果你看完這篇介紹之後也想開始使用Dropbox,請多多使用我的邀請連結喔!

 

布丁邀請註冊Dropbox連結

備份的需求

在介紹Dropbox之前,有一個問題是需要先思考一下:究竟你認為理想的備份策略是什麼呢?一般來說,備份的目的在「一旦資料遺失或損毀時,還可以很快地取得原來或最近備份的資料,讓工作可以繼續正常地進行」。長時間與電腦共處,以電腦作為主要生產工具的人都知道,電腦其實比想像中的更容易故障!重要的資料不僅要備份,還要透過雲端服務異地備份,更要自動化進行。

我撰寫論文期間很長,為了避免資料遺失,備份是必要的工作。我的論文資料不僅包含單純的Microsoft Word檔案,還有為了論文撰寫的大量網頁程式、圖片、各種規劃資料、還有寫這個Blog使用的Windows Live Wrtier程式與草稿。資料夾不僅分散各處,還會時常更改。如果要以手動方式進行備份,每次都會是相當浩大的工程。

基於以上情況,我將備份的需求歸納為以下四點:

備份要自動進行

我撰寫論文的工作日日夜夜都不斷進行,常常做到興頭上就忘記還需要進行備份作。儘管聽說有人會利用Gmail寄信來備份各版本的文件,但是像我論文的資料這麼多,每次要壓縮、上傳來進行手動備份,就會花去相當多的時間。而且由於資料眾多,手動備份顯然不是個好方法。

能夠以排程或是自動偵測的方式進行的備份功能,才能真正地讓你「幾乎忘了他的存在」,也是我選擇備份策略時第一個考量的重點。

透過雲端服務達到異地備份

有人會將資料複製到隨身硬碟中保存,可是日前就有人的筆記型電腦跟備份硬碟一起被偷的新聞,這也告訴我們異地備份的重要性,換句話說,就是「雞蛋(電腦)不要放在同一個籃子(地方)」的道理。

雲端服務是達成異地備份的方法之一。透過網際網路來將資料保存在不同地方的電腦中,即使資料在其中一台電腦的毀損或遺失,另一台電腦仍能保有最新的資料。使用雲端的先決條件就是要時常連上網際網路,而現在撰寫論文、程式時通常都需要開啟網際網路來查詢資料,也就是說,進行工作與備份檔案都會需要利用網際網路進行。沒有網路就不想工作啦 。(誤)

多資料夾備份

我的論文相關重要資料分散在電腦的各處:論文本文會放在桌面的資料夾中、程式會擺在伺服器的運作資料夾、Firefox的Zotero書目資料放在Firefox的資料夾底下。因為需要備份的資料太過零散,手動備份會非常地麻煩,因此需要工具來協助。

其實大部分的檔案同步服務都可以設定多資料夾來進行備份,而Dropbox搭配Dropbox Folder Sync之後,也能提供多資料夾備份的功能。

多版本備份

一般檔案同步的功能通常是做到更改、刪除時即時地變更各地的檔案。然而,這個特色也有個盲點:當你一不小心把檔案刪除或甚至是把檔案內文清空並儲存時,各個同步的資料也跟著一併刪除或清空,造成無法挽回的悲劇。這是自動化備份先天的限制,使用的時候必須要有所認知。

然而為了避免發生這個窘境,Dropbox的一大特色在於多版本的備份。每次Dropbox在備份資料時,都會在遠端空間上保存舊版本的資料,30天內的變更資料都會完整保存,方便你將資料隨時還原到之前儲存的任一版本。連異塵行者也是因為Dropbox的版本備份救了他的一命而對此讚揚不已

本篇介紹的Dropbox幾乎可以滿足四點備份的需求,操作也相當地簡單。因為Dropbox全自動的關係,所以安裝完、設定完之後就可以放著讓他自動運作,非常方便!

介紹Dropbox的相關文章

網路上介紹Dropbox的文章非常地多,因為大家介紹都很詳細了,所以這一篇我也就不做太多圖文細節介紹。以下大概介紹一下各篇所寫的重點:

入門篇
空間加大篇
其他應用篇

Dropbox檔案同步運作介紹

Dropbox最重要的仍然是檔案同步的功能,然而並不是每個人都有「檔案同步」的概念,所以在此先從整體的運作架構來介紹。

image

Dropbox的運作大致上可以分成三個部分:遠端空間Dropbox本地端儲存資料夾My Dropbox、以及連結兩端的Dropbox工具(需要安裝在本地端電腦上)。其中本地端就是指你的電腦主機、筆記型電腦、或是智慧型手機。基本的檔案同步運作步驟如下:

  1. 使用者將要備份的資料存在本地端My Dropbox資料夾中。
  2. Dropbox工具會透過網路自動地確認My Dropbox的資料更新狀況。
  3. 本地端較新的資料會上傳到遠端空間Dropbox。
  4. 本地端將較舊的資料從遠端空間下載回來更新。

透過Dropbox工具時常地確認本地端與遠端空間的資料,不論你在哪一台電腦/手機/筆電編輯,只要他們時常跟Dropbox保持連線,就能你的資料一直保持在最新的狀態。當然,這個前提是要必須時常保持網際網路連線,或是在要更新時進行連線才行。

Dropbox工具

不同的作業系統或環境都有各自的Dropbox工具。目前Dropbox提供了電腦的Windows、Mac、Linux(包括Ubuntu、Fedora兩種發行版)、手機(iPhone、iPad、Android、Blackberry),支援範圍非常廣泛。Dropbox的安裝方式非常簡單,你也可以看免費資源網路社群的圖文介紹來了解個大概。

image 

安裝Dropbox工具的同時,也會協助你上網註冊Dropbox帳號、建立遠端空間,並且配置本地端儲存資料夾My Dropbox。

image

我習慣將My Dropbox放在桌面上,方便配置與找尋資料。

image

Dropbox工具安裝完成之後會常駐在系統當中,以Windows來說就是會放在右下角的通知列。點選右鍵可以看到快速功能以及目前空間的使用狀況。

安裝完Dropbox工具、配置好My Dropbox資料夾之後,基本上就可以讓他放著自動去進行同步備份工作而不需要理會了。

本地儲存資料夾My Dropbox

image

當資料夾被設為My Dropbox資料夾之後,裡面的檔案在左下角都會有特殊的標誌。綠色打勾表示已經同步、藍色箭頭表示同步中,此外還有一些其他顏色的標示,但只要確認這些檔案都有綠色打勾,就表示同步狀況正常。

你可以將任意的檔案放入My Dropbox資料夾,讓Dropbox幫你進行檔案同步的動作。但是Dropbox只會備份My Dropbox一個資料夾的限制實在是很不方便,這時候就要借助Dropbox Folder Sync這個強大的小工具來達到多資料夾備份的目的,讓你不需要變更原本電腦中的資料架構也能進行備份。

上圖在我的My Dropbox中,「kals」是位於網頁伺服器中的程式檔案資料夾、WindowsLiveWriterPotable是位於Program Files的資料夾,這些都是透過Dropbox Folder Sync來進行連結的資料夾。關於Dropbox Folder Sync詳細的安裝與配置方式,就請看電腦玩物的介紹吧。

Dropbox Folder Sync功能非常重要,事實上我也是發現它之後才真正使用Dropbox來進行備份。畢竟像是程式之類的檔案都有既定的架構配置,不可能隨意把它改名叫做「My Dropbox」,而各種需要備份的資料混在同一個My Dropbox資料夾裡也是不方便管理。所以多資料夾備份也是我認為檔案備份的需求之一。

遠端空間Dropbox

image

當你擁有Dropbox帳號之後,你就可以到Dropbox的網站上去瀏覽、取用你同步的所有資料。遠端空間的資料會跟你本地端My Dropbox中的資料完全相同,一般情況下是不需要開啟遠端空間,只要在My Dropbox管理檔案即可。不過,就算你不使用檔案同步功能(沒有My Dropbox),你還是可以把它當成一個2GB以上的免費空間使用。

image

在Dropbox網站的遠端空間網頁介面中可以進行檔案分享、版本控制、回溯已刪除資料等動作。上圖即是我的備份檔案之一「KALS UML規劃.uml」的各個版本,每次我存檔的版本Dropbox都鉅細靡遺地把它備份下來,最短甚至一分鐘內就是一個版本。每當我檔案誤刪、誤改時,我都可以在此輕易地將檔案還原到之前版本,非常方便。

桌上型電腦、筆記型電腦同步備份工作

5070597991_83eba78b9c_b

我大部分時候是在桌上型電腦工作,但是有時候需要出遠門去處理事情,這時就會帶著筆記型電腦出門。我在桌上型電腦跟筆記型電腦都配置了Dropbox,甚至利用Dropbox Folder Sync將多個資料夾都配置完畢。這樣子我在桌上型電腦撰寫的程式可以利用Dropbox輕易地同步到筆記型電腦中,讓我即使出門也可以用桌上型電腦的資料來繼續撰寫;而要切換回原本的桌上型電腦時,也一樣透過Dropbox來將筆記型電腦中更新後的資料存回桌上型電腦。

更重要的是,檔案同步都是全自動進行,我不用考慮現在使用哪一台電腦才是最新的資料,我需要的只是花點時間讓Dropbox連上網路進行同步作業而已,這才是理想的檔案同步策略啊。

結語

這篇的目的是在介紹我如何使用Dropbox來備份我的論文資料,許多操作上的細節或是Dropbox的其他功能,就請你參考上述整理的文章連結吧。

最後如果你覺得Dropbox很棒,也想跟我一樣用Dropbox備份你電腦中的資料的話,也請多多利用我的邀請連結來註冊一個Dropbox玩玩吧XD

 

布丁邀請註冊Dropbox連結

(more...)

圖片上傳與Plurk貼圖的好工具:ZScreen與ImageShack

布丁布丁吃布丁

圖片上傳與Plurk貼圖的好工具:ZScreen與ImageShack

image

我習慣於用Plurk來寫個圖文兼具的噗,而Plurk中要貼圖必須要先上傳、取得網址才能完成這個任務,操作手續繁雜。再者Plurk內建的圖片伺服器並沒有提供像ImageShackFlickr這種專門為圖片、影片典藏網站一樣的管理介面,上傳之後就無法再管理,要取得舊有圖片也是不方便。

後來我發現可以使用ZSCreen這個強大的工具來解決上述的問題,讓我可以快速地截圖、上傳圖片到指定的網站(如ImageShack)、並且方便地將圖片連結放到剪貼簿中。

Plurk貼圖之不便

你有在使用Plurk寫微網誌嗎?我非常習慣於使用Plurk來記錄事物,像是一邊寫論文的程式,一邊記錄一下現在做到哪裡、狀況如何、下一步該做些什麼。這樣能夠讓我方便日後將這些零碎的資訊彙整起來撰寫成一篇較為完整的Blog。

image

Plurk的特色在於可以在簡短的140字中提供圖文兼具的內容,只要貼上圖片的網址,最後即能顯示圖片的縮圖,別人也能點選縮圖看到更大的圖片。上圖即是早上我分享Astrid小章魚待辦事項工具的更新消息,貼個小章魚圖片可以讓同樣使用這個軟體的同好更容易發現這消息。

image

說實在的,Plurk的貼圖功能實在是不好用。首先要先從「分享 > 圖片」中打開「分享圖片」的對話視窗,選擇檔案,按下「上傳圖檔」,等待圖片上傳,然後再繼續寫你想要寫的內容。

其實這個動作最終目的也不過就是要取得一個圖片上傳的網址,但是這些等待動作卻可能會打斷寫作的思緒。

另一個問題在於利用原本的上傳功能上傳到Plurk的圖片伺服器之後,使用者就沒辦法管理那些已經上傳的圖片,而圖片也會被強制縮圖、無法展現原本的大小。他不像ImageShack或Flickr這種圖片或影片典藏網站一樣具備豐富的管理功能,使用者更無法回溯以前自己上傳的圖片。這對像我這種利用Plurk記錄生活點滴的使用者來說是非常不方便的缺點。

因此,我需要透過第三方的服務來協助我典藏圖片與上傳圖片。我使用的圖片典藏網站是ImageShack,而圖片上傳工具就是本篇的主角ZScreen了。

ZScreen簡介

由於重灌狂人介紹了ZUploader這個好用的圖片、檔案、文字上傳軟體,連帶地也讓我發現了ZScreen這個比ZUploader更為強大的工具。

ZScreen功能不僅具備原本ZUploader可以做到的上傳功能,還有各種豐富的功能可以使用。第一次開啟來使用時,我在SCreen的複雜介面中摸了好久才知道個大概,甚至有部分功能是看了網路上簡睿隨筆的ZScreen:一氣呵成的單鍵「抓圖與傳檔」工具才知道原來還有短網址功能。

image

ZScreen還有一個很重要的功能,除了可以用匿名身分上傳之外,還可以用已註冊會員的身分上傳到目標位置,包含FTP、本地端主機、RapidShareSendSpaceFlickrImageShackTinyPicTwitterImageBarnMindTouch等你可能聽過也可能沒聽過的空間。這個功能讓我們可以方便地將圖片上傳到自己慣用的圖片典藏網站中,已方便進行圖片的管理。

免費圖片、影片典藏網站:ImageShack

image

這隻黃色樹蛙是老牌多媒體典藏網站ImageShack的招牌圖示。ImageShack的特色在於免費、穩定、上傳無限空間、提供圖片直接連結(這樣子才能在Plurk上顯示,而不是一定得連到該網站才能開啟圖片),他也有很多第三方工具來協助上傳,像ZScreen就是這類型的工具之一。

image

註冊免費會員之後,使用者可以利用「My Images」功能方便地管理上傳的圖片、影片,而沒有像Flickr免費會員只能看200張的限制。根據使用條款的說明,註冊會員的圖片將會永久保存而不會被刪除。當然,前提是不要觸犯暴力及色情的使用條款限制。

我現在多使用ImageShack來保存圖片,而在以下ZScreen介紹中也是使用ImageShack作為上傳圖片的目的地。如果看到這邊你也覺得很有興趣的話,歡迎點此註冊一個免費會員來玩玩!

設定ZScreen上傳到ImageShack

安裝ZScreen、註冊ImageShack的操作不難,但是要在ZScreen介面中設定上傳目的為ImageShack,則需要摸索一陣子。我假設正在看這篇文章的你已經安裝了ZScreen,也已經註冊了ImageShack,那麼接下來就是教你怎麼在ZScreen裡面進行設定。

取得ImageShack的Registration code

image

ZScreen必須設定你在Image Shack的User Name(使用者名稱)跟Registration Code(註冊碼)。User Name在註冊的時候你應該已經設定了,不過Registration Code則是要註冊完畢之後到My Images –> Preferences(偏好設定)裡面才能查詢。其中「My ImageShack Registration Code」底下的表單則是Registration Code,看起來就像是一串很長的亂碼。

選擇ZScreen上傳目的為ImageShack

image

請打開ZScreen的主介面「Main」,並在「Destinations」的「Images:」中選擇「ImageShack – www.imageshack.us」。

設定ZScreen中的ImageShack

image

一樣是在ZScreen的視窗中,從上排導覽頁中選擇「Destination」,再從次導覽列中選擇「ImageShack」。你會看到ZScreen需要你填入Registration Code跟User Name,請你將ImageShack中的資料填入即可。

如此設定完畢之後,透過ZScreen上傳圖片資料就會自動傳到ImageShack囉。

使用ZScreen擷取、上傳圖片

接下來我來利用ZScreen的Screenshots(圖片擷取)功能,來一氣呵成地擷取網頁圖片、直接上傳到ImageShack、取得圖片網址等一連串的動作。

開啟圖片擷取功能

image

ZScreen可以利用快速鍵、在通知列中的圖示按右鍵並進入「Quick Actions」、或是開啟Actions Toolbar來啟用擷取功能。擷取螢幕的功能有:

  • Entrie Screen:擷取全螢幕,預設快速鍵為PrintScreen鍵。
  • Selected Window:選擇部分視窗,預設快速鍵為Shift + PrintScreen,他會提示你選擇要擷取的範圍,可以是整個視窗、視窗中的一個元件的部份等,相當方便。
  • Crop Shot:擷取自訂範圍,預設快速鍵為Ctrl + PrintScreen,他會提示你選擇要擷取的範圍,畫面上會有尺規方便你度量。
  • Last Crop Shot:擷取上一次的擷取範圍,沒有預設快速鍵。
  • Auto Capture:詳細的擷取功能,可選擇延遲擷取的秒數,如下圖。沒有預設快速鍵。
    image
利用Selected Window選擇要擷取的範圍

image

利用「Selected Window」功能可以準確地選擇要擷取的網頁範圍,請用上述的方法來啟用「Selected Window」擷取功能吧。

上圖是正在使用Selected Window的擷取功能,你可以看到ZScreen會用紫色的尺規框線來提示你要選取的範圍。移動滑鼠的同時,他也會隨之改變選取範圍。他會很聰明地辨識視窗的元件與範圍,讓你快速抓到要擷取的網頁內容。確定要擷取的範圍之後,按下滑鼠左鍵即可完成擷取。

ZScreen自動上傳

image

擷取完成之後,通知列的ZScreen圖示會變成橘字,這表示ZScreen正在處理上傳圖片的動作,請稍待片刻。上傳完成之後,ZScreen就會顯示上傳完成的提示,如上圖。而上傳的圖片網址也自動地放到你的剪貼簿去了,你可以用貼上快速鍵「Ctrl + v」來將網址貼到你正在書寫文字的地方:

http://img63.imageshack.us/img63/2171/ss20101010235921o.png

在Histroy查詢已上傳的圖片

image

透過ZScreen上傳的資料會全部記錄在History功能中,你可以開啟ZScreen的視窗,並切換到「Histroy」頁籤,就可以輕易地取得之前上傳的各種資料。

ZScreen從剪貼簿中上傳圖片

雖然他的螢幕擷取功能很豐富,但我個人仍偏愛使用FastStone Capture來截圖、編輯,然後將圖片複製到「剪貼簿」,再請ZScreen「上傳剪貼簿資料」來進行貼圖。

利用各種編輯工具完成圖片,並且複製到剪貼簿

image

上圖是利用FastStone Capture編輯完成後再FastStone Editor編輯的畫面,當我確定我圖片可以上傳之後,只要按下「Ctrl + c」複製此圖片,就可以準備利用ZScreen進行上傳了。

ZScreen上傳剪貼簿資料

跟擷取螢幕的功能一樣,你可以從通知列的ZScreen圖示上按右鍵並進入「Quick Actions」、或是開啟Actions Toolbar來啟用Clipboard Upload(上傳剪貼簿資料)功能。

  • Clipboard Upload:上傳剪貼簿中的資料。預設快速鍵是Ctrl + F6

我個人習慣利用快速鍵Ctrl + F6來啟用上傳剪貼簿的功能。啟用此功能之後,跟擷取螢幕一樣地,通知列的ZScreen圖示會變成橘色字,再稍等一下就會顯示上傳完成的通知囉。

結語

我一直認為「工欲善其事,必先利其器」是一句至理名言。利用ZScreen來幫助我上傳圖片,我就可以一邊寫Plurk,一邊擷取並上傳自己要的圖片(或是備份原本已經在網路上的圖片),最後整合上傳圖片的網址與文字資料,寫出一則圖文並茂的訊息。日後我也能回去ImageShark來查看、整理之前上傳的圖片,而不必擔心當初寫在噗浪中的圖片在不知不覺中被人拿掉。

其實ZScreen還有很多功能可以使用,就等其他人繼續挖寶囉!

(more...)

論文進度報告(2010/10/10):幾乎完成工具列

布丁布丁吃布丁

論文進度報告(2010/10/10):幾乎完成工具列

image

上次的進度報告之後又隔了兩個多禮拜,最近也是一直在研究JavaScript的基礎技術,然後一邊在Blog把整理好的東西寫出來,希望能給一同鑽研JavaScript的程式設計師作為參考。評估各種寫作方式並訂定之後,就是繼續兩個禮拜之前停頓的進度,而繼續把工具列的部分完成。現在工具列只差「通知」功能尚未實作,但這是要等標註功能實作之後才會有通知,接著才是繼續完成這部份的功能。接下來就是要整理標註功能的UML了呢,又是一項大工程。在這之前,先來整理目前的論文進度吧。

目前的WebApp端的進度

元件 預估程式數量 已完成 UML類別圖
toolkit 17 17 1
core 15 13 1
kals_window 6 6 1
kals_toolbar 30 24 2
kals_text 38+? 0 3
search 6+? 0 1
統計 112+? 60 (53.5%) 9

整體來看,目前完成了大約一半的程式。由於使用物件導向架構開發,每個細節元件都是取用於toolkit或core中的上層元件,在開發同時也會不斷地改良這些元件,因此系統開發的速度會越寫越快。當然,這只是理論上而已。

也許會有人注意到類別程式的數量跟之前比起來有所減少,從之前的114降低到了112。我嘗試簡化系統的功能跟開發的方式來讓整個專案能更快完成,目前是將「自訂外觀」、「個人相片」的功能取消,暫緩適應小螢幕的功能調整(toolkit設計時有考慮小螢幕,但不一定每個功能都能適用就是);而開發中也只對關鍵功能設計單元測試,並且緊對Firefox進行測試,Chrome、IE、Android則在未來有機會時再進行統一的檢查。

navigation

上圖是kals_toolbar中navigation的UML類別圖,這是工具列上各個視窗的功能設定。其中灰字的類別則是暫停開發的類別。詳細的UML類別圖,請參考KALS Wiki

至於專案時程仍暫時維持在19天後完成,也就是10月29日。能不能完成我也沒把握,只能說盡力而為而已。

一改再改的JavaScript物件導向繼承寫作方式

一開始我的JavaScript是用最原始的prototype繼承法,但是由於這種方式在使用上會有些問題,而且我需要使用上層類別的方法,所以又花了一段時間回頭找尋相關資料學習。

首先學習到的是call跟apply的用法,但是發現到JavaScript仍有傳址問題存在,所以又把所有的屬性與方法寫在類別的建構子裡面,不過那將會造成系統資源大量消耗的問題,不得不再找尋新的方法。

接著找到的是Dean Edwards的Base繼承類別,他的確是很好用,寫法簡潔明瞭、又有呼叫上層類別方法的this.base()可使用,但是我使用的Aptana Studio看不懂,而我另外又試用了五六種JavaScript IDE,一樣是看不懂。儘管我後來找到讓Aptana Studio能夠理解的Base繼承法,但是最後仍毅然決然地使用最原始的prototype繼承法。

這是因為我在找尋這些方法的期間,學習到了prototype利用call、或是經典的base方法來呼叫上層或甚至是其他類別的方法來繼承並覆寫,讓prototype的寫法有了更多彈性,而能夠滿足系統的需求。儘管寫起來是比Base繼承法繁雜許多,但是由於prototype繼承法容易分析,許多JavaScript IDE都能理解,也是JavaScript程式設計師應該要知道的基本功課,這是我最後選用prototype繼承法來開發的原因。

這段期間不僅是一直翻閱相關資料、測試方法的優劣,還把系統的所有程式改寫了三次之多,著實非常地花時間,但也因此學到了不少東西。

重新檢視單元測試的必要性

core

在撰寫toolkit跟core的時候,我會要求自己盡量撰寫單元測試,並且最好是能夠測試到所以功能,也就是要求高「測試覆蓋率(Code coverage)」。上圖是core的UML類別圖,其中橘色的小註解標示著這部份的系統有設計單元測試的意思。小小的15個類別裡面,就有7個單元測試,力求核心元件要穩定。

過於繁雜的單元測試

上述在核心的工具時還可以做,到後面設計UI時,設計單元測試就越來越麻煩。我是用setTimeout來設計成機器人的方式,利用jQuery去選擇物件來點選元件或輸入資料,用以測試功能是否正常運作。但是這個機器人也常常做出一般人沒辦法做到的功能,導致測試的水準是比實際上更為高。舉例來說,原本使用者不可能點到某些隱藏起來的按鈕,可是用jQuery去點就可以點到、使用者必須等待視窗關閉並開啟之後才能執行下一個動作,可是jQuery就是無視視窗開閉來進行動作,導致他找不到下一個視窗的功能。也許是我設計單元測試的方法不對,但目前這樣做的確是很累,也顯得不切實際。

總結以上,簡單來說有兩個問題:

  1. 設計單元測試比設計元件本身還花時間,頗有本末倒置的情況發生。
  2. 單元測試不見得切合真實手動操作的情況。畢竟UI設計不像之前的計算用或存取用的程式這麼單純。

因此有必要重新檢討單元測試的必要性。

選擇重要的功能設計單元測試

仔細想一想,不需要全部功能都進行單元測試,去拼那個「測試覆蓋率」實在是很浪費時間與精力,只需要針對重要的功能設計單元測試即可。具體來說,針對具有以下兩種條件的功能來設計單元測試最為實用:

  1. 關鍵功能:千萬不能出錯的功能,當然要用單元測試確保他的正確性。
  2. 多道手續:與其說是在意其正確性,不如說是檢查時因為手續太多很麻煩,不如設計成單元測試的機器人讓他自己跑出的目標功能,再看看哪裡有問題,可以省下許多手動操作的時間。

也就是說,即使很重要的功能,但是如果很簡單就可以手動進行測試,或是其他測試中已經會使用到的功能,那麼就不需要刻意地撰寫單元測試。

程式要經過測試才算是完成。我仍然堅持這個最低底線,但現在並不強求一定要設計成自動的單元測試,即使是手動測試也可以算是勉強及格。

缺乏多人合作的遺憾

寫著寫著來講一個額外的小故事。

有天晚上回到宿舍的時候,發現室友學弟的位置旁坐著另一位學生。

學弟是資科所的學生,研究題目是跟雲端相關,當然也是寫程式的工作。看起來他應該是跟他同學兩個人正在趕一個系統,我偷偷地瞄到了phpMyAdmin的介面,兩台Mac筆電外加一個外接螢幕通通擺在桌上,旁邊還有喝到一半的五十嵐珍奶,看來晚上是要熬夜拼進度了。

老實說,我很少與人一起分工合作地寫程式。教人寫程式的經驗很多,但是可以像學弟他們那樣,「那個帳號登入的部份就拜託你了。」「OK!」地將一個系統分成幾個部分、再各自完成的經驗則是相當地少。不過架伺服器的各功能分工倒是有的,教育部計畫的時候,但那畢竟是功能調整,不太像是寫程式的這種「創作」。

我知道這種分工合作經驗的重要性對於程式開發來說並不亞於技術知識,大型的、有價值的系統是無法、也沒有時間讓我自己一個人完成,而我知道這正是我的弱點。至今我仍然是一個人在寫著程式,就像是現在正在做的碩士論文一樣。

儘管如此,我也在學著讓自己的程式「更好讀」、「更容易讓人使用」。像是建立嚴謹的物件導向架構程式讓人方便使用、遵守PHPDoc、JSDoc、cssdoc這種具有標準規範的註解讓人容易閱讀、利用UML跟網站草圖讓人理解整個系統,使用甘特圖與Wiki共筆工具來模擬多人合作時的專案互動。

我希望我能夠有資格與未來的夥伴一起合作開發一個大型的系統,所以我要加油,嗯。

結語

現在最令人擔心的是,19天後到底能不能完成系統呢?即使系統完成,後面還有好多後續的動作要做,多到讓人現在不想要面對的事情。能在這學期畢業嗎?能在明年3月之前如期把論文交給國中圖嗎?問題實在是太多了。現在還會有人問我要不要繼續念書,還是去當兵,還是要去當資訊替代役,但是現在能不能畢業都不知道啊XD

總之,每天都努力地繼續寫程式、寫Blog記錄,努力地向前進吧。

題外話,國慶日當天寫進度報告真是格外值得紀念?

(more...)

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...)