第六章:函數語言程式設計 (fango)

fango發表於2012-06-21

隨著程式的增大,它也會更復雜更難理解。我們都覺得自己聰明,當然,我們也還是人,一點兒混亂都能讓我們迷惑。然後一切就開始走下坡。我們處理那些不是真懂的問題,都有點像總在電影裡出現的場景:隨便選一根電線剪斷,來解除定時炸彈。如果命好就能選對——尤其你是電影中的英雄而且擺出個適當的演戲姿勢——但把一切都炸掉,也是很有可能的。

我們得承認大部分時候一個程式斷掉不會造成大爆炸。但如果一個程式,在某人無知的擺弄之下,退化為一堆搖搖欲墜的錯誤,要把它重新整得有點兒人味兒,可真是種可怕的折磨,你有時覺得還是乾脆推倒重來算了。

因此,程式設計師一直在找如何能儘量降低自己程式的複雜度。一種重要的方式,是試著做出更抽象的程式碼。寫程式的時候,隨處都可能一不小心跑去處理瑣碎的細節。你碰到一個小問題,開始處理,再到下一個小問題,如此這般。這樣寫的程式碼就像老祖母講故事。

是的,寶貝。做豆粥你要先剝豆,要乾的那種。然後要泡至少一夜,不然你就得煮上好幾個小時。我記得有一次,我那傻小子想煮豆粥。你信嗎?他竟然沒泡豆子?我們牙都快蹦掉了,大家都是。這樣,你泡豆子時,要給每人一杯,要注意它們泡著會漲起來。要是你不當心,不管拿什麼裝,它們都會溢位來,還要加很多水來泡,我說過,要一杯,它們還乾的時候。然後泡好了就用一份幹豆子配四分水來煮。先慢燉兩個小時,就是你要蓋好,用一點小火,然後加寫蔥,芹菜杆切好片兒,也可以加一兩個胡蘿蔔和一些火腿。把它們再煮幾分鐘,然後就可以吃了。

還可以這樣寫菜譜:

每人:一杯剝好的幹豆,一半切碎的洋蔥,半顆胡蘿蔔,一段芹菜莖,可配火腿。
泡豆一夜,(每人)配四分水慢火燉兩小時,加蔬菜、火腿,再煮十分鐘。

這雖然短些,但如果你不知道怎樣泡豆,你還是會搞砸——水太少了。但如何泡豆是可以查到的,這就是竅門。如果你假定讀者具備一些基本知識,你就能講得更短些,更明白些,就能處理更大一些的概念。這,大概就是’抽象‘的意思吧。

這個不著邊兒的菜譜故事怎麼和程式設計扯在一起的?當然,顯然,菜譜也是一套程式。進而,廚藝的基本知識應該能對應到函式和其它程式設計師可以使用的結構。如果你還記得本書的介紹一章,像while這樣的東西能輕易地做出迴圈,而且在第四章,我們寫了些簡單的函式,用來使其它的函式短些直接些。這種工具,一些是語言自身提供的,另一些是程式設計師自己造的,用來減低程式其它部分無趣細節的數量,從而使寫程式更容易。


函數語言程式設計,作為本章的主題,通用巧妙的函式組合來完成抽象。配備整套基礎函式武裝起來的程式設計師,比一切從零開始的要高效太多,當然最重要的是,你得會用它們。不幸的是,Javascript的標配就那麼幾個可憐的函式,因此我們不得不挽起袖子,或者最好能使用別人的程式碼(詳見第九章)。

還有些其它大眾些的抽象方式,最出名的是物件導向的程式設計,是第八章的主角。

如果有些品味,一個醜陋的細節一定開始騷擾你了,那就是無數次的用for迴圈過一個陣列:for (var i = 0; i < something.length; i++),這可以抽象嗎?

問題是,儘管函式大多可以接受值,將其組合並返回些東西,這個迴圈包括了一段必須執行的程式碼。寫個函式走過陣列列印每個單元比較容易:

function printArray(array) {
    for (var i = 0; i < array.length; i++)
        print(array[i]);
}

但除了列印我們要做些別的該怎麼辦?因為‘做些別的’可以表示為一個函式,而函式也是值,我們可以把這個動作作為函式值來傳遞:

function forEach(array, action) {
  for (var i = 0; i < array.length; i++)
    action(array[i]);
}

forEach(["Wampeter", "Foma", "Granfalloon"], print);

並且通過使用無名函式,像for迴圈這樣的可以少寫些無用的細節:

function sum(numbers) {
  var total = 0;
  forEach(numbers, function (number) {
    total += number;
  });
  return total;
}
show(sum([1, 10, 100]));

注意由於詞法作用域規則,變數total在無名函式內部是可見的。同時注意這個版本沒比for迴圈短多少並且拖著很難看的 });尾巴——花括號結束無名函式體,小括號結束forEach的函式呼叫,而需要分號是因為這個呼叫本身是條語句。

有個變數繫結了陣列當前的單元,number,所以無需再使用numbers[i],並且如果此陣列是在對某個表示式求值時產生的,也無需把它放在變數裡,而是可以直接傳遞給forEach。

第四章的cat-code 包含下面一段:

var paragraphs = mailArchive[mail].split("\n");
for (var i = 0; i < paragraphs.length; i++)
    handleParagraph(paragraphs[i]);

這現在可以寫為:

forEach(mailArchive[mail].split("\n"), handleParagraph);

整體上講,多使用抽象(或說‘高層’)的結構能帶來更多的資訊和更少的噪音: sum中的程式碼可以讀為‘把陣列中的每個數加到總量上’,而不是‘這邊兒有個變數從0開始,它可以向後數到陣列的長度,並對應此變數的每份值我們檢視相應的陣列單元並加至總數’。


forEach所作的就是取一個公式,也就是這裡的‘走遍陣列’,並加以抽象。此公式中的‘縫隙’,也就是這裡要對每個單元做些什麼,是靠傳遞給此公式函式的函式來填補的。

作用於函式的函式稱為高階函式。通過操作函式,它們能在一個全新的高度探討行為。第三章的makeAddFunction函式也是個高階函式。只是它不把函式作為參量值,而是生成一個函式。

高階函式可以用來概括許多普通函式無法輕易表述的演算法。如果你儲備了這樣一些函式並能任意調配,它可以幫你更明晰地思考問題:不靠那些亂麻變數和迴圈,你可以把演算法拆分成幾個基本演算法的組合,把它們按名呼叫,從而無需一遍遍地鍵入。

靠著寫下想做什麼而不是如何去做,我們就可以在高層抽象的工作了。實際上,這意味著更短、更明顯、也更舒心的程式碼。


另一種高階函式‘更改’所給的函式值:

function negate(func) {
  return function(x) {
    return !func(x);
  };
}
var isNotNaN = negate(isNaN);
show(isNotNaN(NaN));

negate返回的函式將其參量餵給原來的函式func,然後對結果取反。可如果你想取反的函式需要多個參量該怎麼辦?你可以從arguments陣列得到傳遞給這個函式的所有參量,可如果不知道參量的個數,又怎能呼叫函式呢?

函式都有個方法叫apply(應用),正是用於此處的。它需要兩個參量。第一個參量的作用會在第八章講解,此處我們只需放null。第二個參量,是一個陣列包含了一個函式所必須應用的參量。

show(Math.min.apply(null, [5, 6]));

function negate(func) {
  return function() {
    return !func.apply(null, arguments);
  };
}

不幸的IE瀏覽器帶著一堆不是‘真正’函式的內建函式,或者差不多的東西。使用typeof操作符時這些東西報告自己是‘object'類,它們沒有apply方法。你寫的函式不會受這份兒苦,它們是真正的函式。


我們來看看幾個和陣列相關的演算法。sum函式其實是那種稱為reduce或者fold的演算法的一種:

function reduce(combine, base, array) {
  forEach(array, function (element) {
    base = combine(base, element);
  });
  return base;
}

function add(a, b) {
  return a + b;
}

function sum(numbers) {
  return reduce(add, 0, numbers);
}

通過不斷的使用一個函式把陣列的單元和一個基值組合,reduce把陣列組合為一個單一的值。這恰恰是sum要做的,所以使用reduce可以使之更短,當然加法是個操作符而不是Javascript的函式,所以我們首先要把它放在一個函式裡。

reduce將函式作為第一個參量,而不是像forEach那樣作為最後一個,部分原因是出於傳統——其它語言是這樣的——部分也是因為我們可以玩個特別的技巧,這將在本章最後討論。它意味著在呼叫reduce時,把要reduce的函式寫為無名函式,看著會怪怪的,因為此時其它參量會跟在函式的後面,就和普通的for語句一點兒都不象了。


Ex6.1:寫個countZeroes函式,參量是數字陣列,返回其中零值的個數。使用reduce。 然後寫高階函式count,參量是一個陣列和一個測試函式,返回當測試函式返回true所對應陣列單元的個數。使用此函式重新實現countZeroes。

map是另一個與陣列相關的通用‘基本演算法’。它走遍一個陣列,對每個單元應用一個函式,正如forEach。但它不會丟掉函式返回的那些值,而是用它們做出一個新的陣列。

function map(func, array) {
  var result = [];
  forEach(array, function (element) {
    result.push(func(element));
  });
  return result;
}

show(map(Math.round, [0.01, 2, 9.89, Math.PI]));

注意第一個參量成為func,而不是function。這是因為function是關鍵字,不可以用作變數名。


特蘭西瓦尼亞的高山密林中曾住著一位隱士。大部分時間,他就在山林間徜徉,與樹談心,與鳥同樂。但時不時的,當瓢潑大雨把他堵在小木屋,當呼嘯山風讓他感到不可忍受的渺小,這位隱士就迫切地想寫些東西,希望讓思緒奔湧在紙上,讓它們在那裡長得大過他自己。

他寫詩歌、寫小說、寫哲思,都悲慘地以失敗收場。隱士最後決定寫本技術書。他年輕時做過一陣兒電腦程式設計,他覺得如果能靠這事寫本好書,名聲和賞識必會接踵而至。

於是他動手了。剛開始他用樹皮,但發現不太實際。他下到最近的村子買了臺電腦。寫了幾章之後,他發現應該用HTML格式寫,以便能放在自己的網頁上。


你熟悉HTML嗎?它是在網上往頁上加標註的方法,而且我們也會在本書中用上幾次,所以如果你懂就再好不過了,懂個大概也行。如果你是個好學生,現在你可以去網上找個不錯的HTML介紹看看,讀完了再回來。可我的學生大多差勁,所以我就簡單講幾句,希望夠用。

HTML代表‘HyperText Mark-up Language’(超文標註語言)。HTML的檔案都是文字。因為必須要表達文字的結構,例如那些文字是標題,那些要紫色等等,就有些字元有了特殊含義,這類似Javascript字串中的反斜線。‘大於’和‘小於’字元用來生成‘標籤’,標籤給文件中的文字多一些資訊。它可以代表自己,例如標註一頁中影像應該放置的位置,它還可以包含文字以及其它標籤,例如用它標註段落的開始和結束。

一些標籤是必須有的,整個HTML文件必須包含在html標籤中。此處是一份HTML文件的例子:

<html>
  <head>
    <title>A quote</title>
  </head>
  <body>
    <h1>名言</h1>
    <blockquote>
      <p>我們思考和程式設計的語言與問題以及我們想到的解法之間的聯絡非常緊密。正因如此為消滅出現錯誤而限制語言的特色最好情況下也是危險的。 </p>
      <p>-- Bjarne Stroustrup</p>
    </blockquote>
    <p>strstr先生是C++程式語言的發明人,但其人其實挺有見地的。還有,這裡有張鴕鳥的照片:</p>
    <img src="img/ostrich.png"/>
  </body>
</html>

開啟,然後用結束。html元素總是包含兩個孩子:head和body。前一個包含關於此文件的資訊,後一個包含實際文件。

大部分標籤名是密文縮寫。h1代表‘heading 1’(標題 1),是最大的一種標題。還有h2到h6相應用於小一些的標題。p代表‘paragraph’(段落),而img代表‘image'(影像)。img標籤不包含任何文字以及其它標籤,但它也有些而外資訊,src="img/ostrich.png",稱為’屬性‘。此處,它包含這裡要顯示的影像檔案的資訊。

因為<和>在HTML中有特殊含義,它們不可以直接寫在文件的文字中。如果你在HTML文件中想寫'5<10',就必須寫為'5&lt;10',這裡’lt'代表‘less than’(小於),'&gt;'是指'>',並且因為這些程式碼也給了 & 特殊含義,普通的'&'就要寫為'&amp;'。

這些只是最基本的HTML,但應該足夠理解此章以及後續章節中中的HTML文件,而不會完全迷糊了。


*譯者會調整此處的console用法*


拾起之前的故事,隱者希望他的書是HTML格式的。剛開始他在手稿中直接寫這些標籤,但鍵入所有這些大於小於號讓他手很痛,而且他總是不記得要把'&'寫成'&amp;'。這讓他頭很痛。接下來,他嘗試著用微軟字處理寫書,然後儲存為HTML。可得到的HTML比之前的大15倍,也更加複雜。除此之外,微軟字處理讓他頭很大。

他最終找到的方案是這樣:他用普通文字寫書,但段落的分割與標題的樣式會有簡單的規則。然後,他寫了個程式,把這樣的文字精確地轉換為他希望的HTML。

規則是:

  1. 段落由空白行分隔。
  2. 由%開頭的段落是標題。越多%符號,標題越小。
  3. 段落內,兩個星號之間的文字要加重點。
  4. 腳註寫在方括號裡。

隱士拼命掙扎了半年,他的書也只寫了幾段。就在此刻,他的小木屋被雷劈了,他死了,他的寫作野心也永久地平復了。從他焦黑的電腦殘片裡,我可以復原以下的檔案:

% 程式設計聖經

%% 兩面一體

機器表面之下,程式流淌。不費吹灰之力,它伸縮隨意。偉大和諧之中,電子散聚。監視器之表格,如水之漣漪。元氣在之下隱匿。

造物主創制機器,放入處理器與記憶。從此昇華起程式的兩面一體。

處理器之面為陽,名為控制。記憶之面為陰,名為資料。

資料為微小位元之聚集,然具繁複形式。控制只靠簡單指令,然能執行困難操作。細微瑣碎之上,龐大繁雜鶴起。

程式之源為資料。控制隨之。控制而後創新資料。物物相生,生生不息。此乃數控和諧之道也。

數控本無型。程式元祖以混元鑄其程式。假以時日,元之資料晶固為資料型別,而混之控制禁錮於控制結構與函式。

%% 論

樊遲問數控相生之道。子曰:‘思編譯器,自編譯之。’

樊遲問曰:‘程式元祖以簡單之器且無程式設計之語,而得美崙之程,吾輩徒具複雜機器與程式語言,何也?’   子曰:‘古之築者,泥枝而而,可得美奐之草居者也。’

隱者十年磨一程。狂言之:‘陋程以286之器運MS DOS,可算群星之蹤跡‘。  
子曰:’沒人還有286電腦和MS DOS了。‘

子韋編一小程式,到處是全域性狀態和貌似近路。讀之,樊遲問曰:‘您老教導我們曰,此術不可為之,然此處您可為之,何也?‘,子曰,’屋未失火不要叫救火車‘(此非慫恿懶散程式設計,實為警惕經驗法則神經依賴症。)

%% 智

一學生抱怨數目字。‘我取2的平方根再平方,結果不準也!’。聞知,子樂。‘此為一紙,給我寫下2的平方根的精確值。’

子曰,‘你不順著木之紋理用鋸,需用更大力氣。你不按問題之條理程式設計,需寫更大程式。’

子立與子素吹噓最近所寫持續之尺寸。‘20萬線’,子立道,‘不計註釋’,‘啥?’子素曰,‘我的已達100萬’。子曰,‘我最佳程式有500行’。聞知,子立子素感化之。

樊遲木雞電腦前久矣,墨面沉眉,欲得難題之美麗答案而不得其所。子擊其背而喝曰‘*鍵*‘。樊遲始作一醜惡解答。止,而立得美麗答案。

%% 序

新程員作序如蟻之築丘,日進一寸,而不思尺丈。其程式好比流沙。一時可立,增之必塌(此為無組織程式碼內部不一致以及重複組織之危也)。

見此,程員始費思於結構。其序乃堅其架如石之像。其序雖固,然必摧之方得改之(此為結構常禁錮程式發展者也)。

程式大師知何時應用結構何時保持簡單形式。其序如膠泥,固而可塑。

%% 語

程式語言之始,語法語義加之。語法為其程式格式,語義是其功能。語法美麗語義清晰,則程式可為雄偉之樹。語法笨拙語義混淆,程式必為荊棘灌木。

子素需以Java作程式,其函式原始非常。每個早晨,其坐於電腦前,開始抱怨。整日發誓,責怪是語言全部做錯。子聞知久矣,責之,曰‘每個語言有其道也。順之,不可以其它語言之法程式設計。’

為了紀念我們的好隱士,我希望替他完成HTML生成程式。這個問題的一個比較好的解法可以是:

  1. 在每個空行處切分檔案為段落。
  2. 去除標題段落開頭的%,並標記為標題。
  3. 處理每個段落,分為普通部分、重點部分和註腳。
  4. 移動全部註腳至檔案尾部,原處保留數字標記。
  5. 將每部分用正確的HTML標籤包住。
  6. 全部組合至單一HTML文件。

此方案不接受重點部分包括註腳,反之亦然。有些武斷,但可以簡化示例程式。如果學完本章,你覺得意猶未盡,大可修改程式支援‘內嵌’標記。

整篇手稿作為一個字串的值,可以呼叫recluseFile函式得到。


演算法的第一步很簡單。一個空行就是兩個換行符在一起。如果你還記得第四章中字串的split方法,你就曉得這樣玩了:

var paragraphs = recluseFile().split("\n\n");
print("Found ", paragraphs.length, " paragraphs.");

Ex6.2, 寫函式processParagraph,當給定一個段落字串參量時,檢查此段是否為標題。如是,剝離%字元並對其計數。然後返回一個物件,包括兩個屬性,content包含此段的文字,type為所需包住的此段的標籤,"p"是正常段落,"h1"是帶一個%的標題,而"hX"是帶X個%字元的標題。
記得可以用字串的charAt方法查詢給定字元。


此處我們可以試一下之前看過的map函式。

var paragraphs = map(processParagraph,
                     recluseFile().split("\n\n"));

"咣",我們有了一個分門別類的段落物件陣列。可是我們超前了,忘了演算法的第3步:

  1. 處理每個段落,分為普通部分、重點部分和註腳。

這可以分解為:

  1. 如果段落由星號開始,拿出重點部分並存好。
  2. 如果段落由左方括號開始,拿出註腳並存好。
  3. 否則,拿出的部分直到第一個重點部分或者註腳,或者字串結束,並作為普通文字存好。
  4. 對段落餘下部分,從1開始重複。

Ex.6.3, 打造一個函式splitParagraph,當給定一個段落字串時,返回此段落的片段陣列。考慮合適的片段表示方式。
可以嘗試indexOf方法,在字串中查詢一個字元或者子串並返回其位置,如未找到,返回-1.
此演算法有點繞,也有些不太正確或者太羅嗦的講述。如果你碰到問題,只要想想就行了。試著寫些內部函式來處理組成演算法的一小部分。


我們現在可以讓processParagraph同時分割段落內部的文字了。我的版本可以這樣修改:

function processParagraph(paragraph) {
  var header = 0;
  while (paragraph.charAt(0) == "%") {
    paragraph = paragraph.slice(1);
    header++;
  }

  return {type: (header == 0 ? "p" : "h" + header),
          content: splitParagraph(paragraph)};
}

用它對應段落陣列則可以得到一個段落物件的陣列,後者進而包含片段物件陣列。下一步,就是拿出註腳,代之以參考。像這樣:

function extractFootnotes(paragraphs) {
  var footnotes = [];
  var currentNote = 0;

  function replaceFootnote(fragment) {
    if (fragment.type == "footnote") {
      currentNote++;
      footnotes.push(fragment);
      fragment.number = currentNote;
      return {type: "reference", number: currentNote};
    }
    else {
      return fragment;
    }
  }

  forEach(paragraphs, function(paragraph) {
    paragraph.content = map(replaceFootnote,
                            paragraph.content);
  });

  return footnotes;
}     

每個片段都呼叫replaceFootnote函式。如果它拿到的片段應該保留,就返回此片段,可如果拿到的是註腳,就把此註腳加入footnotes陣列,然後返回對它的引用。順便每個註腳和引用也都編上號。


這樣我們的工具就足以從檔案中提取所需的資訊了。現在所剩的,就是生成正確的HTML。

好多人覺得粘合字串是生成HTML的絕好方法。如果他們需要一個連結,比如去到一個你可以下圍棋的網站,他們的做法是:

var url = "http://www.gokgs.com/";
var text = "Play Go!";
var linkText = "<a href=\"" + url + "\">" + text + "</a>";
print(linkText);

(這裡的a是個標籤,用來生成HTML文件裡的連結)。。。這不僅笨,而且如果text字串裡剛好有尖括號或者&,它也不是對的。莫名其妙的事會出現在你的網站,你也看起來像個受辱的玩票的。我們當然不想這樣。寫幾個簡單的HTML生成函式應該不難,那就開始吧。


生成HTML的祕方是把HTML文件作為一個資料結構,而不是一整篇文字。Javascript的物件提供了一個極其簡單的建模方法:

var linkObject = {name: "a",
                  attributes: {href: "http://www.gokgs.com/"},
                  content: ["Play Go!"]};

每個HTML單元帶有個name,給出所代表的標籤名。如果具備屬性,則也有一個attributes,作為存放這些屬性的物件。如果有內容,則有一個content,是一個此單元所包括的其它單元的陣列。字串的角色是我們HTML文件中的文字片段,因此陣列["Play Go!"]是指這個連結中只有一個元素,是一片簡單的文字。

直接鍵入這些物件是笨了點,可我們不必如此。我們給自己造個省事函式:

function tag(name, content, attributes) {
  return {name: name, attributes: attributes, content: content};
}

注意,因為我們允許attributes和content對某元素不適用時保持未定義,此函式的第二第三個參量如果不需要,可以不必給出。

tag還是有些原始,因此我們對常用元素也省省事,例如連結,或者簡單文件中的外層結構:

function link(target, text) {
  return tag("a", [text], {href: target});
}

function htmlDoc(title, bodyContent) {
  return tag("html", [tag("head", [tag("title", [title])]),
                      tag("body", bodyContent)]);
}


Ex.6.4, 可以參考之前的HTML文件,寫image函式,對給定影像檔案地址,生成HTML元素img

生成文件之後,它必須縮減為一個字串。從我們生成的資料結構做出這個字串在直接不過了。要記得的最重要的事,是轉換我們文件文字中的特殊字元:

function escapeHTML(text) {
  var replacements = [[/&/g, "&amp;"], [/"/g, "&quot;"],
                      [/</g, "&lt;"], [/>/g, "&gt;"]];
  forEach(replacements, function(replace) {
    text = text.replace(replace[0], replace[1]);
  });
  return text;
}

字串的replace方法,可以找出第一個參量給出模式的所有出現,並用第二個參量替換,從而得到一個新字串,所以"Borobudur".replace(/r/g, "k")的結果是 "Bokobuduk"。先別管模式句法,第10章我們會講。escapeHTML函式把這些不同的替換放進一個陣列,就可以逐一迴圈它們並應用在參量上了。

雙引號也要被替換,是因為使用此函式處理HTML標籤屬性中的文字。這些文字是用雙引號括起的,所以它們內部不可以有雙引號。

呼叫替換四次,意味著電腦必須走過整個字串四遍,以便檢查並替換其內容。這樣效率不高。如果我們在意,可以寫個複雜些的函式,類似之前的splitParagraph那種,來只走一遍。只是現在我們要偷點懶。還有,第10章提供了一個更好的方法。

要把一個HTML單元變為字串,我們可以使用遞迴函式:

function renderHTML(element) {
  var pieces = [];

  function renderAttributes(attributes) {
    var result = [];
    if (attributes) {
      for (var name in attributes) 
        result.push(" " + name + "=\"" +
                    escapeHTML(attributes[name]) + "\"");
    }
    return result.join("");
  }

  function render(element) {
    // Text node
    if (typeof element == "string") {
      pieces.push(escapeHTML(element));
    }
    // Empty tag
    else if (!element.content || element.content.length == 0) {
      pieces.push("<" + element.name +
                  renderAttributes(element.attributes) + "/>");
    }
    // Tag with content
    else {
      pieces.push("<" + element.name +
                  renderAttributes(element.attributes) + ">");
      forEach(element.content, render);
      pieces.push("</" + element.name + ">");
    }
  }

  render(element);
  return pieces.join("");
}

注意in迴圈從Javascript物件提取屬性,以便由此得出HTML標籤屬性。同時注意有兩個地方,陣列用來積累字串,以便合併出一個結果字串。可為什麼我不從空字串開始,再使用+=操作符加上內容呢?

這是因為生成新字串,尤其是長的那種,非常費事。要記得Javascript的字串值不可變更。如果你要給它接上點什麼,就會得到一個新的字串,而舊的保持不變。如果我們靠不斷地接上一堆短字串,來得到一個長字串,這樣每次新生成一個字串,都在接上新的之後被白白丟棄。如果換個方式,我們把這些短字串存放在一個陣列中,然後合併,則只是生成一個長字串而已。


這樣,我們試試這個HTML生成系統。。。

print(renderHTML(link("http://www.nedroid.com", "Drawings!")));

似乎不錯。

var body = [tag("h1", ["The Test"]),
            tag("p", ["Here is a paragraph, and an image..."]),
            image("img/sheep.png")];
var doc = htmlDoc("The Test", body);
viewHTML(renderHTML(doc));

似乎我現在需要提醒你,這個方案不盡完美。它實際上整出一個XML,貌似HTML,可結構嚴些。簡單情況下,例如此處,這不會添什麼麻煩。可總有些事情,雖是正確的XML,但不是正常的HTML,這會讓瀏覽器顯示我們生成的文件的時候被忽悠。例如,如果文中有個空的script標籤(用來在網頁放入Javascript),瀏覽器不會知道它是空的,而是認為後面的都是Javascript。(此例,放一個空格在此標籤,問題就解決了。它不空了,也有了正常的結束標籤)。


Ex.6.5, 寫一函式renderFragment,並用它實現另一函式renderParagraph,對給定的段落物件(註腳已經剔除),生成正確的HTML元素(可能是段落或者標題,取決於段落物件的type屬性)。

此函式可以用於輸出註腳引用:

function footnote(number) {
  return tag("sup", [link("#footnote" + number,
                          String(number))]);
}

sup標籤以‘上標’顯示其內容,也就是比其它的文字小些高些。連結目標可以像"#footnote1"。包含一個‘#’字元的連結指向頁中的‘錨點’(anchor),此處我們用它使讀者點選註腳連結時,可以跳到頁尾的註腳處。

顯示重點欄位的標籤是em,普通文字不靠標籤即可顯示。

我們就快完了。唯一還沒有顯示函式的是註腳。要使"#footnote1"工作,每個註腳都必須包括一個錨點。HTML裡的錨點靠a元素指定,它也同時用在連結。此處,它需要一個name屬性,而不是href。

function renderFootnote(footnote) {
  var anchor = tag("a", [], {name: "footnote" + footnote.number});
  var number = "[" + footnote.number + "] ";
  return tag("p", [tag("small", [anchor, number,
                                 footnote.content])]);
}

然後是這裡的函式,給定一個合適格式的檔案和文件名後,會返回一個HTML文件:

function renderFile(file, title) {
  var paragraphs = map(processParagraph, file.split("\n\n"));
  var footnotes = map(renderFootnote,
                      extractFootnotes(paragraphs));
  var body = map(renderParagraph, paragraphs).concat(footnotes);
  return renderHTML(htmlDoc(title, body));
}

viewHTML(renderFile(recluseFile(), "The Book of Programming"));

陣列的concat方法,可以用來連線另一個陣列,就像+操作可以用於字串那樣。

後續章節的示例程式會預設並使用幾個基礎的高階函式,例如map和reduce。偶而也會加入個新工具。第九章我們會更有結構的開發這個‘基本’函式集。


使用高階函式時,Javascript的操作符不是函式這點經常很煩人。我們有幾處需要add或equals函式。你會同意,每次重寫它們是很難受。此現在起,我們假定存在一個物件op,包含如下函式:

var op = {
  "+": function(a, b){return a + b;},
  "==": function(a, b){return a == b;},
  "===": function(a, b){return a === b;},
  "!": function(a){return !a;}
  /* and so on */
};

這樣我們可以寫 reduce(op["+"], 0, [1, 2, 3, 4, 5])來對一個陣列求和。可如果我們需要equals或者makeAddFunction,而它們的某個參量已經帶有一個值了,該當如何?此時,我們又需要回來重新寫個新函式了。

對此,稱為“部分應用”(partial application)的東西就有用了。你想生成一個新函式,它已經知道一部分的參量,並且會把多餘的部分,放在這些確定的參量的後面。這可以通過創造性的使用一個函式的apply方法完成:

function asArray(quasiArray, start) {
  var result = [];
  for (var i = (start || 0); i < quasiArray.length; i++)
    result.push(quasiArray[i]);
  return result;
}

function partial(func) {
  var fixedArgs = asArray(arguments, 1);
  return function(){
    return func.apply(null, fixedArgs.concat(asArray(arguments)));
  };
}

我們希望允許同時繫結多個參量,因此就有必要用asArray函式從arguments物件中得到普通的陣列。它把它們的內容拷貝到一個真正的陣列,就可以對其使用concat方法。它還可以有第二個參量,用來剔除開始的幾個參量。

同時注意我們有必要把外層函式(partial)的arguments存入一個不同名字的變數,否則內層函式就不可以看到它們了—— 內層函式有自己的arguments變數,會遮住外層函式的。

此時equals(10)就可以寫為partial(op["=="], 10),而不需要一個特別的equals函式。你就可以這樣做了:

show(map(partial(op["+"], 1), [0, 2, 4, 6, 8, 10]));

通常通過給map一個函式來部分應用map會比較有用,這就是map得函式參量放在陣列參量前面得原因。這’提升‘了函式,使它不是操作一個值而是一陣列的值。例如,如果你有一組陣列的陣列的值,並且希望得出所有值的平方,你可以這樣:

function square(x) {return x * x;}
show(map(partial(map, square), [[10, 100], [12, 16], [0, 1]]));


如果你要組合函式,最後一個有用的竅門,是函式合成(composition)。本章開始講到的函式negate,把’取反‘操作應用在呼叫一個函式之後的結果之上:

function negate(func) {
  return function() {
    return !func.apply(null, arguments);
  };
}

這是一個廣義模式的狹義案例:呼叫函式A,再應用B在其結果上。合成是數學上的常用概念。它可以出現在下面的高階函式中:

function compose(func1, func2) {
  return function() {
    return func1(func2.apply(null, arguments));
  };
}

var isUndefined = partial(op["==="], undefined);
var isDefined = compose(op["!"], isUndefined);
show(isDefined(Math.PI));
show(isDefined(Math.PIE));

這裡我們定義新函式而根本沒有使用關鍵字function。這可用於當你需要生成一個簡單函式,以便用於map或者reduce的時候。但,如果一個函式變得複雜過此處的例子,通常就用function來寫會短些(也更高效些)。

相關文章