最近更新: 2012-02-22

撰寫乾淨的 eval 程式碼的技巧

JavaScript 的 eval() 功能很強大,但想用得好卻不容易。 寫在 eval 內的程式碼,經常被抱怨不能寫太長、很難修改維護。 其實老練的 JavaScript 程式人員有許多技巧可以讓這件事變得容易。 本文則將說明其中一種讓 eval 內的程式碼變得易寫易讀的技巧。

eval_bad.js 是典型的難寫難讀的 eval 寫法。

var prefix = "X-";

eval(" \
    var lists = { \
        "name": "abc", \
        "age" : 1, \
        "code": prefix + "0010" \
    }; \
     \
    for (var p in lists) { \
        alert(p + ": " + lists[p]); \
    } \
");

這種寫法難寫難讀。大部份的書籍,甚至因此建議程式人員不要用 eval()

  • 難寫: 你必須要仔細地為「字串內的程式碼」中的字串括號加上跳脫符號。 如果字串內程式碼中想要寫入多行程式,那麼也要留意字串分行的 \ 。 最不幸的是,JavaScript 語法分析器只會把它當成普通的字串,不會幫你檢查內部程式碼的語法。
  • 難讀: 太多的跳脫符號與分行。 程式碼編輯器的語法分色也不會應用於上,你只會看到一串同色的"字串"。

想要改善這種狀況,我們必須思考的問題是:「我該如何把程式中的一段程式碼變成字串?」

我說的可不是在程式碼編輯器裡剪貼一段程式碼,然後複製到一對雙引號(")這種事。 我指的是如何讓一個程式,在執行過程中,從自己的身上取得一段程式碼,把這段程式碼變成程式中的字串。 喔,對了。如果你還分不清楚「字串」與「程式碼」的區別,你還不適合做程式人員。

在 JavaScript 語言中,此問題的解答難度,取決於回答者對 JavaScript 語言規範的熟悉程度。 簡單來說,就是回答者是否看完 ECMA-262 這份規格書。

這問題的解法,在 JavaScript 語言規範中有著直接了當的門路可循。 請看 ECMA-262 第 15.3.4.2 項: Function.prototype.toString。 依我手上的PDF文件頁次,是第3版第99頁,第5.1版第119頁。

15.3.4.2 Function.prototype.toString ( )

An implementation-dependent representation of the function is returned. This representation has the syntax of a FunctionDeclaration. Note in particular that the use and placement of white space, line terminators, and semicolons within the representation string is implementation-dependent.

ECMA-262 3rd edition, p.99

這段話的白話是說,當你調用一個「函數」的 toString() 方法時,此方法會傳回這個函數的宣告文句,亦即文字內容是「這個函數的程式碼」的字串。 這個提示足夠明顯了。想要從程式中取出一段程式碼當成字串? 這就是了。

我先寫一段小程式來看看調用函數的 toString() 方法時,到底是個什麼結果。 請看 function-toString.js 。

function sawp(a, b) {
    var c = a;
    a = b;
    b = c;
}

print(swap.toString());
// alert(swap.toString());

毫不意外,它的輸出結果正好就是我寫的 swap 函數的整段程式碼。

接著需要處理一下函數宣告的頭尾。 函數的 toString() 方法傳回的是一段完整的函數宣告。 而讓 eval() 執行,只需要函數內的程式碼,並不需要頭尾的敘述。 為此,我需要設計一個簡單的函數,它接受一個函數作為參數。 這個函數會調用參數的 toString() 方法取得其宣告文句。 接著以頭尾的 { , } 為判斷依據,用字串函數 slice() 取出其中的程式碼文句 (ECMA-262 稱為 function body)。

eval_clean.js 是以 eval_bad.js 為對照所改寫的清晰版本。 其中 _script() 函數,就是我用來取出程式碼文句的函數。

function _script(f) {
    var ctx = f.toString();
    return ctx.slice(ctx.indexOf('{') + 1, ctx.lastIndexOf('}'));
}

var prefix = "X-";

eval(_script(function(){
    var lists = {
        "name": "abc", 
        "age" : 1,
        "code": prefix + "0010"
    };

    for (var p in lists) {
        alert(p + ": " + lists[p]);
    }
}));

eval_clean.js 與 eval_bad.js 相比,就易寫易讀了。

  • 易寫: 你不必再為了跳脫字元與分行的 \ 煩心。 同時,JavaScript 語法分析器會幫你檢查語法。
  • 易讀: 它與其他程式碼的外觀一樣。 如果你的程式碼編輯器提供語法分色的功能,也一樣會應用於其中。 不再是一行同色的"字串"。

當我需要組成程式碼或是在不同的 JavaScript 端點間傳遞要執行的程式碼時,經常採用這種方式撰寫。 使用 eval() 不再是程式人員的負擔。


補充內容。端點是一個雙關語。 JavaScript 是一個程式語言,在它的規範中,稱呼它實際運行的環境為 host (寄宿處、宿主)。 而 host 在資訊科學中的另一個意義就是設備端點。

「不同的 JavaScript 端點間傳遞要執行的程式碼」這句話是有點抽象,我補充幾個實際情境。

  • 1. 雲端運算的場法。將我的運算方程式散佈到其他機器的 JavaScript 端點執行。
  • 2. 在 gnome-shell/seed/gjs 的環境撰寫一段 JavaScript 程式碼,交給 Gtk.WebKit 執行。或者反過來。 例如: JavaScript 與 Desktop - Desktop and WebKit
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/18991164.html

樂多舊回應
未留名 (#comment-22329770)
Wed, 29 Feb 2012 17:09:42 +0800
沒想到還有這種方法,受益良多,感恩
未留名 (#comment-22330778)
Thu, 01 Mar 2012 13:46:41 +0800
有人問我「在不同的 JavaScript 端點間傳遞要執行的程式碼」這句話代表的時機是什麼?

端點是一個雙關語。
JavaScript 是一個程式語言,在它的規範中,稱呼它實際運行的環境為 host (寄宿處、宿主)。
而 host 在資訊科學中的另一個意義就是設備端點。

這句話是有點抽象,我補充幾個實際情境,各位就懂了。

1. 雲端運算的場法。將我的運算方程式散佈到其他機器的 JavaScript 端點執行。

2. 在 gnome-shell/seed/gjs 的環境撰寫一段 JavaScript 程式碼,交給 Gtk.WebKit 執行。
例如: http://blog.roodo.com/rocksaying/archives/14456843.html
未留名 (#comment-22340480)
Tue, 06 Mar 2012 12:01:26 +0800
不好意思,還是有些問題不懂,想請教一下:
在瀏覽器用 script tag載入 js檔案,文字檔的內容就會被瀏覽器當作程式碼執行…這個效果是不是您說的「傳遞要執行的程式碼」?像 http://www.dotblogs.com.tw/grence/archive/2010/06/11/15812.aspx裡的方法三

如果是,在 JavaScript已經可以把 function當作普通變數傳遞,為什麼還要把 function body當字串解出來,再用 eval執行?
在文章裡的例子…
_script(function(){/* code */});
效果等於
(function(){/* code */})();
但 eval感多繞了一段路。
未留名 (#comment-22340510)
Tue, 06 Mar 2012 12:21:12 +0800
本文的程式碼是示範用的,並不表示我真的這麼用。
請再仔細看看我補充的兩個實際情景,特別是第二個的例子。

我使用 eval 的情境,大部份是在不同的 javascript host 間傳遞程式碼,另一部份是用於 meta-programming 的場合,例如動態建立一個匿名類別。

1. 我撰寫的 JavaScript 程式碼,有些並不是使用在瀏覽器中,而是像 gnome-shell/gjs/seed 這類單獨的 JavaScript 解釋器。沒有所謂 script tag 。

2. 在 A host 定義的 function ,只在 A host 中存在。對 B host 而言不存在,你不能直接傳遞。不能直走的情形,當然就要繞路了。這時候就必須用 eval 繞路。
未留名 (#comment-22340678)
Tue, 06 Mar 2012 13:45:41 +0800
瞭解了!謝謝指教。
hoamon@gmail.com(hoamon) (#comment-22347064)
Fri, 09 Mar 2012 08:20:54 +0800
這實在太令人驚豔了。
未留名 (#comment-22353772)
Mon, 12 Mar 2012 17:29:06 +0800
感謝您的分享! 非常受用...
未留名 (#comment-22707974)
Tue, 11 Dec 2012 04:33:13 +0800
哇!好讚的技巧!!!
未留名 (#comment-22921782)
Wed, 12 Jun 2013 06:57:14 +0800
未留名 (#comment-22923656)
Thu, 13 Jun 2013 09:17:16 +0800
toSource ,如參考文件開頭斗大的「Non-Standard」所示,不是標準項目。
我的習慣是除非在其他 ECMAScript 實作品中有替代品,否則無視之。