RSS

2011/06/30

令人又愛又恨的 JavaScript function

JavaScript 的其中一個特別之處, 就是 function 本身是一種 object 這個設計. 習慣寫 procedural 及 OOP 的人一時間未必能夠完全消化箇中用處. 寫 C 的高手, 或者是學過 LISP 的人, 也許會比較容易明白. 不過同一時間, 當 function 跟 object 一齊使用時, 又帶出了更加麻煩的 binding 問題.

JavaScript function 簡介

要定義 JavaScript function, 最基本的方法就是直接用 function keyword 來定義


function someFunc() {
// ...
}


這樣做的話, 其實等同於定義一個叫 someFunc 的 variable, 並指定一個 function object 作 value:


var someFunc = function () {
// ...
};


就因為 function 的名稱其實就是一個 variable, 我們可以把它用作另一個 function 的 parameter 甚至是從一個 function return 出去. 細想一下, 在 JavaScript 設定 event handler 時就正正是利用到這一點:


window.onload = function() { alert("Hi!"); };


當然, 除了用作 event handler 之外還有很多用途, 例如我可以寫一個這樣的 function :


function each(inputArray, inputFunction) {
var i;
for(i = 0; i<inputArray.length; ++i) {
inputFunction(inputArray[i]);
}
};


這個 function 就可以指定一個 function 去處理一個 array 的每一個項目, 例如我可以這樣用:


each([ 1, 2, 3 ], function(item) { document.write(item); });


類似的工具在不同的 JavaScript library 都有提供, 只要能夠充份的運用它們, 就能快速地完成想做的工作.
例如在 prototype library 之中我可以這樣寫:


var squares = [ 1, 2, 3 ].collect(function(item) { return item * item; });
// squares = [ 1, 4, 9 ]


當 function 遇上 object

寫 JavaScript 的其中一個良好習慣, 就是把要寫的東西都盡量歸納到自定的 object 之內. 順理成章的, 就會把 function 都定義到 object 之內了. 舉個例子:


var myPopup = {
message: "Hello!",
pop: function () { alert(this.message); }
};


假設當我們希望把它用作某些 event 的 event handler 的話, 很直接的想法就是這樣做:


window.onload = myPopup.pop;


不過這樣做的話不會出現我們所希望的結果. 為甚麼呢? 這是因為當我們把 function 當作 value 使用的時候, 它跟原本 object 之間的關聯就消失了. 想像一下, 以上例子其實跟以下這個一樣的:


window.onload = function () { alert(this.message); };


這樣看的話, 就即時發現 this 在這裡並不是指 myPopup 這個 object 了. (而事實上, 這個 this 在這裡是 JavaScript 的 window object)

要解決這個問題, 最容易的做法是定義一個 wrapper function:


window.onload = function() { return myPopup.pop(); };


這樣做就可以肯定 pop function 是經 myPopup 執行的. 要是不喜歡這種 wrapper 的話, 也可以借用一下 object template 的 lexical closure (類似 local scoping):


var PopupClass = function (message){
this.message = message;
var thisObject = this;
this.pop = function () { alert(thisObject.message); }
};
var myPopup = new PopupClass("Hello");
window.onload = myPopup.pop;


很複雜呢! 這裡做的, 是令到 pop function 可以用 thisObject 這個內部的 variable 去使用原來用不到的 this. 這樣做就可以用回 window.onload = myPopup.pop; 這種比較好看的寫法了.

再看看另一個情況, 如果這次是把 function 用作另一個 function 的 parameter 呢?


var myPopup = {
message: "Hello ",
pop: function (name) { alert(this.message + name); }
};
function popname(popFunc, name) {
popFunc(name);
}
popname(myPopup.pop, "Mary"); // undefinedMary
popname(myPopup.pop, "Tom"); // undefinedTom



同樣道理, 我們可以用 wrapper 解決:


popname(function() { myPopup.pop("Mary"); });
popname(function() { myPopup.pop("Tom"); });


但你會發現, 每次用到這個 function 的時候都要加一個 wrapper, 很麻煩又容易出錯. 我們可以預先定義一個工具, 再利用 function object 本身的 apply function:


function bind(func, object) {
return function() { return func.apply(object, arguments); };
}
popname(bind(myPopup.pop, myPopup), "Mary");
popname(bind(myPopup.pop, myPopup), "Tom");


同一個 function 更可以在程式中重覆使用. 除了 apply 之外, 還有類似的 call, 但在這個例子不適用.

JavaScript library

大部份的 JavaScript library 都提供了相關的工具, 相當方便. 例如 prototype 就直接定義了一個 bind function:


popname(myPopup.pop.bind(myPopup), "Mary");


而 jQuery 就有 proxy function 可以用:


popname($.proxy(myPopup.pop, myPopup), "Mary");


function binding 在 JavaScript 之中是個很重要的概念, 不過同一時間, 亦是一個很容易過份使用的工具. 對於初學者來說, 最重要的是先弄清楚甚麼時候需要用到. 熟習之後才研究甚麼時候需要/不需要用吧.

(BTW, 在這裡還要說清一點, 在 OOP 的世界, 我應該說 "method" 而不是 "function". 不過為免令到一些對 OOP 認識比較少的讀者看得一頭霧水, 還是決定用 "function" 一字)

2011/06/29

借用 Closure Compiler 來 debug JavaScript

JavaScript 令人頭痛的其中一個地方, 就是 browser compatibility 的問題. 有很多時候, 在一個 browser 運作正常的 script, 在另一個 browser 會出現問題.

很多時進行開發的人都只專注於其中一個運行環境, 待完成後才去作較大規模的測試. 現時很多網站都大量使用 JavaScript, 很容易就會出現過千行的code. 這時候問題一出現就會很頭痛, 因為很多時候這類 browser compatibility 的問題並不會產生甚麼有用的錯誤訊息.

舉個例子, 寫 PHP 的人很容易就習慣在建立 array 時在每個 item 後面都加上逗號, 例如是這樣:


<?php

$myarr = array(
'item1',
'item2',
'item3',
);
?>


這樣做的原因, 是可以很方便的加、減或移動項目. 但是 JSON 就不可以這樣做了:


var myarr = [
'item1',
'item2',
'item3'
];


最後的逗號一定要移走, 因為在某些 browser 會以為逗號之後有一個空的項目, 這樣會令整條 script 不能正常運作. 在加減項目, 或是移動項目時就變得要留意這個逗號了.

由於這個問題只會在某些 browser 出現, 若果埋沒在 code 海之中就很難去找出來了. 這個時候, 可以借用一下 Google 的 Closure Compiler 來幫忙了.

Closure Compiler 本身的作用, 是把 JavaScript 重新編譯, 以達到壓縮的效果. 不過所謂的"編譯", 有很大的部份只是把 variable, function, 或 object 以較短的名稱來取代. 由於要做到這效果, 必須先分析 script 的結構, 所以它可以找出 JavaScript 之中所潛在的 syntax error. 借用一下, 就可以用來 debug JavaScript.

就用以上的例子來說用一下吧. 首先到這個 Closure Compiler UI 界面. 在左手邊的 textarea 內把 // ADD YOUR CODE HERE 以下的 code 換成這個:


var myarr = [
'item1',
'item2',
'item3',
];


要留意這裡我故意在最後一個 item 加上逗號.

輸入好之後, 就按一下 "Compile", 然後就會看到右手邊顯示了有一個 Error. 按一下那個 tab 就會看到錯誤訊息:

JSC_TRAILING_COMMA: Parse error. Internet Explorer has a non-standard intepretation of trailing commas. Arrays will have the wrong length and objects will not parse at all. at line 6 character 1

找到錯誤的地方, 就能對症下藥了.

我個人認為, 在這個網絡速度那麼快的時代, Closure Compiler 本身的作為壓縮 JavaScript 的用途實在不太大(但可以間接令JavaScript難以解讀, 有著輕輕的一重保護作用). 不過可以利用一下來找出潛在的 syntax error, 又相當的不錯.