最近更新: 2013-01-23

跨端點傳遞程式碼與資料的技巧

我先前寫了篇「撰寫乾淨的 eval 程式碼的技巧」,其中一個應用領域是在不同的 JavaScript 端點間傳遞要執行的程式碼。 但我同事又碰到另一個狀況,他不只要傳遞程式碼,同時程式碼中還要有一段從本地端點產生的資料。 由於資料中包含複雜結構與字串,他原先的做法一直碰到斷句或是逸出字元錯誤的困擾。 於是我又在「撰寫乾淨的 eval 程式碼的技巧」這之上擴充了可以放入資料的內容。

首先看看我同事所要做的事。基本上可以簡化成下列 issue-1.js 展現的形式:

function() {
    var sum = 0;
    a.forEach(function(v) {
        sum += v;
    });
    document.getElementById('output3').innerHTML = sum;
}

他要把上列的程式碼傳給其他 JavaScript 端點執行。程式碼的內容套用我在「撰寫乾淨的 eval 程式碼的技巧」寫的 _script() 來做沒問題。但這段程式碼中卻有一個變數 aa 的內容也要由本地端點產生再連帶程式碼一併傳給其他端點。這就有問題了。

說起來, JavaScript 在不同端點間傳遞資料的技巧很早之前就出現了,那就是如今大家耳熟能詳的 JSON 。 所以 issue-1.js 要連帶資料傳給其他端點的做法,可如下列所示:

// ---- at local host ----
var code = _script(function() {
    function(a) {
        var sum = 0;
        a.forEach(function(v) {
            sum += v;
        });
        document.getElementById('output3').innerHTML = sum;
    }
});
var data = JSON.stringify([1,2,3]);

pass_to_remote_host(code, data);

// ---- at remote host ----
var req = recv_code_data();
func = req.code;
func(req.data);  // data is already unserialized.

不過這樣的解決方式限制頗多,而且有點不直覺、不便利。 所以我又想了一個比較直觀的作法。這個作法借用以值傳遞(Pass by value)的概念,用「竄改」(interpolate)的方式,直接將資料夾帶入程式碼中。使用者唯一需要記住的是,要加一個 $ 符號在資料的代表變數名稱前。

下列是我擴充改寫後的新 _script() ,再加碼送另一個 _script_valist()。兩套方法任君選擇。

function _script(f, args) {
    var ctx = f.toString();
    var k;
    ctx = ctx.slice(ctx.indexOf('{') + 1, ctx.lastIndexOf('}'));
    if (args) {
        ctx = ctx.replace(/\$[\$\w]+/g, function(matched, offset, src) {
            k = matched.substring(1);
            return (args[k] ? JSON.stringify(args[k]) : matched);
        })
    }
    return ctx;
    /*
    string.replace() with replace function:
    see ECMA-262 3rd edition, 15.5.4.11 String.prototype.replace, page 102.
    */
}

function _script_valist(f) {
    var ctx = f.toString();
    var args = arguments;
    // 讓 args 指向參數清單,以便讓 ctx.replace 取得
    var idx;
    ctx = ctx.slice(ctx.indexOf('{') + 1, ctx.lastIndexOf('}'));
    return ctx.replace(/\$\d/g, function(matched, offset, src) {
        // 這個範圍的 arguments 指向這個匿名函數的參數清單,
        // 而不是 _script_valist 的參數清單
        idx = parseInt(matched.substring(1));
        return (args[idx] ? JSON.stringify(args[idx]) : matched);
    });
    /*
    arguments:
    see ECMA-262 3rd edition, 10.1.6 Activation Object, page 38.
    */
}

PHP 或 Perl 等語言的使用者應該很熟悉像 "hello $name" 這種在字串中夾入變數值的語法。而我在此使用的表達方式也是相同的。使用者的程式碼中,以 $ 符號標示出要帶入變數值的符號名稱,然後我的 _script_script_valist 就會將變數值帶入符號位置,產生完整可用的程式碼文字出來,讓使用者傳遞給其他端點。

sample.html 示範這兩個函數的用法與實際產出。

<html>
<script>
// 此處省略 _script 與 _script_valist 的定義。

function test() {
    var res;

    var b = 123;
    var c = [3,2,1];

    res = _script(function(){
        var s = "hello world";
        s += ' rock fdsa ds af sa';
        f2($a, $b, $c);
    }, 
    {
        'a': 'hello $b, world',
        'b': b,
        'c': c
    });
    document.getElementById('output1').innerHTML = res;

    res = _script_valist(function(){
        var s = 'hello world';
        s += ' rock' + 'fdsa ds af sa';
        f2($1, $2, $3, $4);
    }, 
        'hello $2 world', b, c, {'mail': 'rock@123'}
    ); 
    document.getElementById('output2').innerHTML = res;

    eval(_script_valist(function(){
        var sum = 0;
        ($1).forEach(function(v) {
            sum += v;
        });
        document.getElementById('output3').innerHTML = sum;
    },
    [1,2,3,4,5]
    ));
}
</script>
<body onload="test();">
<pre id="output1"></pre>

<pre id="output2"></pre>

<pre id="output3"></pre>

</html>

$ 符號是 JavaScript 的合法符號表達字元之一,所以不會造成任何語法解釋的錯誤。

_script() 傳遞資料的方式是用「鍵/值」組 ,所以第二個參數是一個「鍵/值」組的陣列。其中鍵的名稱會對應到程式碼中的「$符號」,接著將值替換到這個位置上。

_script_valist() 傳遞資料的方式則是用參數清單,第二個參數及其以後的內容都視為要傳遞(替換入程式碼)的資料。參數清單是用順序位置表示各個資料項目,又資料項目是從第二個參數位置開始,所以 $1 代表第二個參數,餘類推。

_script_valist() 的好處是可以省下輸入「鍵/值」的功夫。缺點是資料項目多時,滿目的 $1, $2, $3 不容易聯想它們的意義與預期內容。

樂多舊網址: http://blog.roodo.com/rocksaying/archives/21307942.html