最近更新: 2009-11-10

再探 JavaScript的中介編程 foreach

網友 WanCW 在 JavaScript的中介編程與反射能力示範 一文中回應 文章中的 foreach() 並未產生新的程式或是修改現有的程式,好像不太能算是 metaprogramming?

並非如此,其實 foreach 在中介編程(metaprogramming)的領域是經典樣式。只是我上文的例子太精簡,以至於看不出它的威力。嗯,如果不來個複雜點的程式碼,確實不容易看出 foreach 到底可以幫我們省下多少程式碼。我就來個複雜點的示範吧。

傳統迭代控制結構

首先,我先寫一段傳統的程式碼。我分別配置了一個陣列a和一個 XML 文件 x。接著用for(;;){}迭代控制結構去傾印它們的內容。

本文範例中使用了 Document 資料型別,而這是以網頁瀏覽器為宿主時才會擁有的原生型別,所以請透過瀏覽器(如 Firefox, IE)等執行。如果不知道如何執行的讀者,可以開啟 JavaScript shell 此網頁,把程式碼剪貼到上面去執行。

var a = [0, 1, 2, 3, 4];

var x = document.implementation.createDocument ('', '', null);
var items = x.createElement('items');
var item, i;
for (i = 0; i < 5; i++) {
    item = x.createElement('item');
    item.appendChild(x.createTextNode(i));
    items.appendChild(item);
}
x.appendChild(items);

print('==== foreach 使用前 ====');

//走訪 array ,你要寫 array 的程式碼
for (i = 0; i < a.length; i++) {
    v = a[i];

    print(v);
}

//走訪 xml ,你要寫 xml 的程式碼
for (i = x.firstChild.firstChild;
     i;
     i = i.nextSibling)
{
    v = i.firstChild.nodeValue;

    print(v);
}

如無意外,你會看到兩次 01234。這分別是 ax 的傾印結果。 經驗豐富的程序員知道這種 for(;;){} 迭代控制結構在程式中的重複頻率有多高。 不論你的迭代內容是要做什麼,只要你想從頭到尾走訪一遍,你就要重複輸入一次for(;;){}

抽出迭代控制結構,化為 foreach

遵循 DRY (程序員天生是懶人) 原則,這種重複的程式碼就要抽離出來。 我分別定義了兩個建構者(Java叫"類別"), DataArray 負責陣列,DataXml 負責 XML 文件。同樣都定義了 foreach 方法。而它們的內容其實就是上面的 for(;;){} ,我把它們搬進類別中了。 至於迭代內容則以函數加以參數化,傳給 foreach 調用。

function DataArray(init) {
    var a = init;

    this.foreach = function(f) {
        var i, v;
        for (i = 0; i < a.length; i++) {
            v = a[i];

            f(v);
        }
    }
}

function DataXml(init) {
    var x = init;

    this.foreach = function(f) {
        var i, v;
        for (i = x.firstChild.firstChild;
             i;
             i = i.nextSibling)
        {
            v = i.firstChild.nodeValue;

            f(v);
        }
    };
}

//順便定義 print(v)
function pv(v) {
    print(v);
}

使用 foreach 取代迭代控制結構

接下來就是重頭戲了,

print('==== foreach 使用後 ====');
var da = new DataArray(a);

da.foreach( pv );

var dx = new DataXml(x);

dx.foreach( pv );

瞧,一整個簡化了,你不用再看見重複的 for(;;){} 了。甚至不用再區分 array 或 xml 而寫不同的程式碼。

好戲就這樣嗎?不,還沒呢。精彩的總是壓軸演出。

print('==== 再進一步 ====');

containers = [da, dx];

(new DataArray(containers)).foreach( function(d) {
    d.foreach( pv )
});

print('不用 foreach 的話,你要這樣寫');

for (i = 0; i < containers.length; i++) {
    d = containers[i];

    if (d.toString() == '[object XMLDocument]') {
        //... 請自己copy上面走訪 x 的程式碼
    }
    else if (d.toString() == '[object ???') {
        //如果有一堆不同資料型態,你要寫下一排 else if...
    }
    else {
        //... 請自己copy上面走訪 a 的程式碼
    }
}

有沒有添加 foreach 語法的差異性有多大,現在是一目瞭然。

8.8 Custom Control Structures

Ruby’s use of blocks, coupled with its parentheses-optional syntax, make it very easy to define iterator methods that look like and behave like control structures.

Flanagan and Matz, The Ruby Programming Language, page 281, O'Reilly 2008

在 JavaScript 中,自定的 foreach 可以讓我們把重複的迭代控制碼 for(;;){} 寫在個體方法中。我們只需要將處理元素內容的迭代器(包在 for {} 區塊中的那些程式碼),透過一般函數或匿名函數加以參數化傳給 foreach ,它就自動幫我們做好迭代控制工作了。

看看上面的例子,我們在使用 foreach 後省下了一大堆程式碼,這正是 metaprogramming 。

如果覺得上述的示範還是不算中介編程,那麼不妨換個方式來看本文的例子。請把 foreach 看成一段 macro,而不要看成一個函數。那麼當我寫下 d.foreach( pv ); 之後,各位就可以想像成 JavaScript 自動把上面那一行代換成(產生) for (xxx;xxx;xxx) { v = i; pv(v) }

當然我上面的例子中顯示,JavaScript這個 "foreach" 巨集還具有判斷對象型別套用不同for(;;){}的功能。在 C++ 中,這是用 template 來做的.

One style of programming which focuses heavily on metaprogramming is language-oriented programming, which is done via domain-specific programming languages.

metaprogramming@Wikipedia

除了macro或eval的方式,其實 JavaScript 更傾向於透過 DSL 的方式去實踐中介編程。用 JavaScript 去加強 JavaScript 自己(不透過其他語言),改變了 JavaScript 原本的編程風格。如果你做的夠多,甚至可以讓你以後寫出完全不像 JavaScript 語法的 JavaScript 程式碼。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/10637755.html

樂多舊回應
wancw.wang@gmail.com(WanCW) (#comment-20048193)
Tue, 10 Nov 2009 07:01:14 +0800
感謝您的回覆。您花了很大的篇幅展示 foreach() 的威力,卻未能解開我原本的疑問。

就我的理解,您實作的 foreach() 並未產生新的程式或是修改現有的程式,所以它應該沒有涉及 metaprogramming 的範疇。

不知是我忽略了您的範例程式碼中的什麼細節或是我對 metaprogramming 的瞭解有誤?
未留名 (#comment-20048309)
Tue, 10 Nov 2009 08:46:53 +0800
This is only an illustration of "how to use code to write more code";

我喜歡這句在wikipedia的解釋~~
未留名 (#comment-20048683)
Tue, 10 Nov 2009 11:08:15 +0800
WanCW,你把 metaprogramming 當成 macro 了。

Wiki 也說了:
"Not all metaprogramming involves generative programming. If programs are modifiable at runtime or if an incremental compilation is available, then techniques can be used to perform metaprogramming without actually generating source code."

你不妨換個方式來看本文的例子,請你把 foreach 看成一段 macro,而不要看成一個函數。那麼當我寫下:

d.foreach( pv );

之後,你就可以想像成 JavaScript 自動把上面那一行代換成(產生)下面的code:

for (xxx;xxx;xxx) {
v = i;
pv(v)
}

當然我上面的例子中顯示,JavaScript這個"foreach"巨集還具有判斷對象型別套用不同for(;;) 碼的功能。在 C++ 中,這是用 template 來做的.

Wiki:"One style of programming which focuses heavily on metaprogramming is language-oriented programming, which is done via domain-specific programming languages."

除了macro或eval,其實 JavaScript 更傾向於透過 DSL 的方式去實踐 metaprogramming 。用 JavaScript 去加強 JavaScript 自己(不透過其他語言),改變了 JavaScript 原本的編程風格。如果你做的夠多,甚至可以讓你以後寫出完全不像 JavaScript 語法的 JavaScript 程式碼。
wancw.wang@gmail.com(WanCW) (#comment-20054801)
Wed, 11 Nov 2009 18:40:08 +0800
--
中間刪除(by 石頭成)
--
P.S. 最後吹毛求疵一下:「Wiki」這個字彙有其明確的意義,並不等於「Wikipedia」。我覺得在使用時應該要注意,不要混淆了。
未留名 (#comment-20055125)
Wed, 11 Nov 2009 20:04:36 +0800
metavige 的提示你沒看,我寫的你也不接受。我不是高手,Martz 也不是大師,顯然我們講的內容不夠權威。

這樣吧,你拿這問題去問你的老師。繳了那麼多學費別浪費了。

為了避免佔版面,我刪掉回應文字了,不過最後一句我留下來了。在公司打字太快,忘了把 wiki 改成 Wikipedia。
未留名 (#comment-20137965)
Wed, 02 Dec 2009 20:15:30 +0800
恩 系統會檔某些字元
http://colin-x1124.myweb.hinet.net/jsTool/foreach.html
這是測試頁面
未留名 (#comment-20141977)
Fri, 04 Dec 2009 00:49:47 +0800
我不並清楚這些程式的意圖。
就結果來看,我也只能說 Ok.