本章將介紹 JavaScript 的函數(shù)式背景:
這既不是一本介紹 Clojure 也不是介紹 JavaScript 的書,這是一本介紹如何用 JavaScript 函數(shù)式編程的書。其中一些函數(shù)式的思想和表現(xiàn)形式都借用了 Clojure,因此叫做 Clojure 風格的函數(shù)式 JavaScript,但是并不要求在讀本書前會 Clojure,而只需要能閱讀 JavaScript 代碼,那就足夠了。 如果你會 Clojure,可以完全忽略我解釋 Clojure 代碼的段落,當然 JavaScript 的部分才是重點。
想學 JavaScript 這不是一本 JavaScript 的教科書,這里只會介紹如何用 JavaScript 進行函數(shù)式編程,所以如果想要系統(tǒng)學習 JavaScript 的話,我猜看一看《JavaScript 語言精粹》已經(jīng)足夠了。另外如果讀者的英文好的話,還有一本可以在線免費閱讀的《JavaScript Allonge》。
想學 Clojure 同樣的,這也不是 Clojure 的教科書,這里只含有一些用于闡述函數(shù)式編程思想的 Clojure 代碼。作為副作用,你確實可以學到一些 Clojure 編程的知識,但很可能是非常零碎不完整的知識。如果是想要系統(tǒng)的了解和學習 Clojure 的話,非常推薦《The Joy of Clojure》,另外,如果讀者英文比較好,還有一本可以免費在線閱讀的《CLOJURE for the BRAVE and TRUE》 也非常的不錯。
函數(shù)式編程的專家 如果你已經(jīng)在日常工作或?qū)W習中使用 Scala,Clojure 或者 Haskell 等函數(shù)式語言編程的話,那么本書對你在函數(shù)式編程上的幫助不會太大。 不過:這本書對解你從函數(shù)式語言遷移到 JavaScript 編程的不適應該是非常有效的,當然,這也正是本書的目的之一。
在開始閱讀本書之前,如果你希望能運行書中的代碼的話,可能需要一些環(huán)境的配置。而且書中的所有源碼和運行方式都可以在本書的 Github 倉庫中找到。當然如果你使用 emacs(同時還配置了 org babel 的話) 閱讀本書的源碼的話,對于大部分代碼只需要光標放在在代碼處按 c-c c-c即可。
JavaScript 原生的 JavaScript 沒有什么好準備的,可以通過 Node 或者 Firefox(推薦)的 Console 運行代碼。當然第五章會有一些使用 sweet.js 寫的 macro,則需要安裝 sweet.js。
安裝 Node/iojs
brew install node
# 或者
brew install iojs
安裝 sweet.js 在安裝完 node 之后命令行輸入:
npm install -g sweet.js
Clojure 書中的 Clojure 代碼大多都用來描述函數(shù)式編程的概念,當然如果想要運行書中的 Clojure 代碼,首先需要安裝 JVM 或者 JDK,至少需要 1.6,推薦安裝 1.8。
安裝 leiningen
leiningen 是 clojure 的包管理工具,類似于 node 的 npm,ruby 的 bundle,python 的 pip。 另外 leinigen 還提供腳手架的功能。可以通過官網(wǎng)的腳本安裝。 mac 用戶可以簡單的使用 brew install leiningen 安裝。
安裝完之后,就可以運行 lein repl 打開 repl,試試輸入下列 clojure 代碼,你將會立馬看見結(jié)果。
(+ 1 1)
;=> 2
編輯器 如果更喜歡使用編輯器用來編輯更長一段的代碼,我推薦非 emacs 用戶使用 Light Table, intellij 用戶對使用 cursive。當然如果讀者已經(jīng)在使用 Emacs,那就更完美了,emacs cider mode 是 Clojure 編程不錯的選擇。
說到 JavaScript 可能第一反應會是一門面向?qū)ο蟮恼Z言。事實上,JavaScript 是基于原型(prototype-based)的 多范式 編程語言。也就是說面向?qū)ο笾皇?JavaScript 支持的其中一種范式而已,由于 JavaScript 的函數(shù)是一等公民,它也支持函數(shù)式編程范式。
常見的編程范式有三種,命令式,面向?qū)ο笠约昂瘮?shù)式,事實上還有第四種,邏輯式編程。 如我們在大學時學過的 C 語言,就是標準的命令式語言。而如果你在大學自學過 Java 打過黑工的話,那么你對面向?qū)ο笠苍偈煜げ贿^了吧。而可能大部分人(以為)接觸函數(shù)式的機會比較少,因為它是更接近于數(shù)學和代數(shù)的一種編程范式。
命令式 這恐怕是我們最熟悉的編程范式了(大部分計算機課程都會是 C),命令式顧名思義就是以一條條命令的方式編程,告訴計算機我需要先做這個任務,然后另一個任務。還有一些控制命令執(zhí)行過程的流控制,比如我們熟悉的循環(huán)語句:
for(var i=0;i<10;i++){
console.log('命令',i)
}
當然還有分支語句,switch 等等,都是用來控制命令的執(zhí)行 過程 。
面向?qū)ο?/strong> 這恐怕是目前最常見的編程范式了(絕大部分的工程項目的語言都會是面向?qū)ο笳Z言)。而面向?qū)ο蟮乃枷雱t更接近于現(xiàn)實世界,封裝好的對象之間通過消息互相傳遞信息。面向?qū)ο笥幸恍┪覀兪煜さ母拍畋热绶庋b,繼承,多態(tài)等等。而面向?qū)ο蟮乃季S主要是通過抽象成包含狀態(tài)和一些方法的對象來解決問題,可以通過繼承關系復用一些方法和行為。
函數(shù)式 函數(shù)式則更接近于數(shù)學,簡單來說就是對表達式求值。跟面向?qū)ο笥兴煌氖呛瘮?shù)式對問題的抽象方式是抽象成 帶有動作的函數(shù)。其思維更像是我們小時候解應用題時需要套用各種公式來求解的感覺。當然函數(shù)式跟面向?qū)ο笠粯舆€包含了很多的概念,比如高階函數(shù),不可變性,惰性求值等等。
http://wiki.jikexueyuan.com/project/clojure-flavored-javascript/images/paradigm.png" alt="" />
圖 1 主要的編程范式
邏輯式編程 可能這個名詞聽的比較少,但是我們經(jīng)常在用而卻可呢過沒有意識到的 SQL 的 query 語句就是邏輯式編程。所謂邏輯式,就是通過提問找到答案的編程方式。比如:
select lastname from someTable where sex='女' and firstname in ('連順','女神')
這里問了兩個問題:
那么得到的答案就是符合問題描述的結(jié)果集了。
除了最常見的 SQL,Clojure 也提供了 core.logic 的庫方便進行邏輯式編程。
說了這么多種編程范式,JavaScript 對函數(shù)式的支持到底如何呢?
首先如果語言中的函數(shù)不是一等的,那么也就跟函數(shù)式編程也就基本劃清界限了。比如 Java 8 之前的版本,值和對象才是一等公民,要寫一個高階函數(shù)可能還需要把函數(shù)包在對象中才行。
幸好 JavaScript 中的函數(shù)是一等函數(shù),所謂一等,就是說跟值一樣都是一等公民,所有值能到的地方,都可以替換成函數(shù)。例如,可以跟值一樣作為別的函數(shù)的參數(shù),可以被別的函數(shù)想值一樣返回,而這個“別的函數(shù)”叫做 高階函數(shù) 。
函數(shù)作為參數(shù) 函數(shù)作為參數(shù)最典型的應用要數(shù) map 了,想必如果沒有使用過 Underscore,也或多或少會用過 ECMAScript 5 中 Array 的 map 方法吧。map 簡單將一個數(shù)組轉(zhuǎn)換為另一個數(shù)組。
[1, 2, 3, 4].map(function(x) {
return ++x;
});
可以看到函數(shù) function(x){return x++} 是作為參數(shù)被傳入 Array 的 map方法中。map 是函數(shù)式編程最常見的標志性函數(shù),想想在 ECMAScript 5 出來之前應該怎么做類似的事情:
var array = [1, 2, 3, 4];
var result = [];
for(var i in array){
result.push(++i);
}
這段命令式的代碼跟利用 map 的函數(shù)式代碼解決問題的方式和角度是完全不同的。命令式需要操心所有的過程,如何遍歷以及如何組織結(jié)果數(shù)據(jù)。而 map 由于將遍歷,操作以及結(jié)果數(shù)據(jù)的組織的過程封裝至 Array 中,從而參數(shù)化了最核心過程。而這里的核心過程就是 map 的參數(shù)里的匿名函數(shù)中的過程,也是我們真正關心的主要邏輯。
函數(shù)作為返回值 函數(shù)作為返回值的用法可能在 JavaScript 中會更為常見。而且在不同場景下被返回的函數(shù)又有著不同的名字。
柯里化 我們把一個多參的函數(shù)變成一次只能接受一個參數(shù)的函數(shù)的過程叫做柯里化。如:
var curriedSum = curry(sum)
var sum5 = curriedSum(5)
var sum5and4 = sum5(4) //=> 9
sum5and4(3) // => 12
當然柯里化這樣做的目的非常簡單,可以部分的配置函數(shù),然后可以繼續(xù)使用這些配置過的函數(shù)。當然,我會在第四章函數(shù)組合那里更詳細的解釋為什么要柯里化,在這之前閑不住的讀者可以先猜猜為什么要把柯里化放函數(shù)組合那一章。
thunk thunk(槽)是指有一些操作不被立即執(zhí)行,也就是說準備好一個函數(shù),但是不執(zhí)行,默默等待著合適的時候被合適的人調(diào)用。我實在想不出能比下圖這個玩意更能解釋 thunk 的了。 在下一章,你會見到如何用 thunk 實現(xiàn)惰性序列。
http://wiki.jikexueyuan.com/project/clojure-flavored-javascript/images/thunk.png" alt="" />
圖 2 thunk 像是一個封裝好待執(zhí)行的容器
越來越函數(shù)式的 ES6 ECMAScript 6(也被叫做 ECMAScript 2015,本書中會簡稱為 ES6)終于正式發(fā)布了,新的規(guī)范有非常的新特性,其中不少借鑒自其他函數(shù)式語言的特性,給 JavaScript 語言添加了不少函數(shù)式的新特性。
雖然瀏覽器廠商都還沒有完全實現(xiàn) ES6 的所有規(guī)范,但是其實我們是可以通過一些中間編譯器使用大部分的 ES6 的新特性,如
Babel
這是目前支持 ES6 實現(xiàn)最多的編譯器了,沒有之一。 主要是 Facebook 在維護,因此也可以編譯 Facebook 的 React。這也是目前能實現(xiàn)尾遞歸優(yōu)化的唯一編譯器。不過關于尾遞歸只能優(yōu)化尾子遞歸,相互遞歸的優(yōu)化還沒有實現(xiàn)。
Traceur
Google 出的比較早得一個老牌編譯器,支持的 ES6 也不少了。但是從 github 上來看似乎已經(jīng)沒有 babel 活躍了。
當然,除了這些也可以直接使用 FireFox。作為 ES6 規(guī)范的主要制定者之一的 Mozilla 出的 Firefox 當然也是瀏覽器中實現(xiàn) ES6 標準最多的。
箭頭函數(shù) 這是 ES6 發(fā)布的一個新特性,雖然 Firefox 支持已久了,不算什么新東西,但是標準化之后還是比較令人激動的。 箭頭函數(shù) 也被叫做 肥箭頭 (fat arrow)9,大致是借鑒自 CoffeeScript 或者 Scala 語言。箭頭函數(shù)是提供詞法作用域的匿名函數(shù)。
聲明一個箭頭函數(shù) 你可以通過兩種方式定義一個箭頭函數(shù):
([param] [, param]) => {
statement
}
// 或者
param => expression
表達式可以省略塊(block)括號,而多行語句則需要用塊括號括起來。
為什么要用箭頭函數(shù) 雖然看上去跟以前的匿名函數(shù)沒有什么區(qū)別,我們可以對比舊的匿名函數(shù)是如何寫一個使數(shù)組中數(shù)字都乘 2 的函數(shù).
var a = [1, 2, 3, 4,5];
a.map(function(x){ return x*2 });
而使用箭頭函數(shù)會變成:
a.map(x => x*2);
使用箭頭函數(shù)可以少寫 function 和 return 以及塊括號,從而讓我們其實更關心的轉(zhuǎn)換關系變得更明顯。略去沒用的長的匿名函數(shù)定義其實可以讓代碼更簡潔更可讀。特別是在傳入高階函數(shù)作為參數(shù)的時候, map(x=>x*2) 更形象和突出的表達了變換的邏輯。
詞法綁定
如果你覺得這種簡化的語法糖還不足以說服你改變匿名函數(shù)的寫法,那么想想以前寫匿名函數(shù)中的經(jīng)常需要 var self=this 的苦惱吧。
1: var Multipler = function(inc){
2: this.inc = inc;
3: }
4: Multipler.prototype.multiple = function(numbers){
5: var self = this; // <=
6: return numbers.map(function(number){
7: return self.inc * number; // <=
8: })
9: }
10: new Multipler(2).multiple([1,2,3,4]) // => [ 2, 4, 6, 8 ]
這樣做很怪不是嗎,因此經(jīng)常出現(xiàn)在各種面試題中,讓你猜猜 this 到底是誰。或者讓你去修正 this 綁定,方法如此之多,但是不管是使用 EcmaScript 5 的 bind,還是 map 的第三個參數(shù)來保證 this 的綁定不會出錯,都逃脫不了要手動修正 this 綁定的命運。
那么如果用箭頭函數(shù)就不會存在這種問題:
Multipler.prototype.multiple = function(numbers){
return numbers.map(number => number*this.inc);
};
new Multipler(2).multiple([1,2,3,4]);// => [ 2, 4, 6, 8 ]
現(xiàn)在,箭頭函數(shù)里面的 this 綁定的是外層函數(shù)的 this 值,不會受到運行時上下文的影響。而是從詞法上就能輕松確定 this 的綁定。不需要 var self=this 了是不是確實方便了許多,不僅不會再被各種怪異的面試題坑了,還讓代碼更容易推理。
尾遞歸優(yōu)化
Clojure 能夠通過 recur 函數(shù)對 尾遞歸 進行優(yōu)化,但是 ES5 的 JavaScript 實現(xiàn)是不會對尾遞歸進行任何優(yōu)化,很容易出現(xiàn) 爆棧 的現(xiàn)象。但是 ES6 的標準已經(jīng)發(fā)布了對尾遞歸優(yōu)化的支持,下來我們能做的只是等各大瀏覽器廠商的實現(xiàn)了。
不過在干等原生實現(xiàn)的同時,我們也可以通過一些中間編譯器如 Babel,把 ES6 的代碼編譯成 ES5 標準 JavaScript,而在 Babel 編譯的過程就可以把尾遞歸優(yōu)化成循環(huán)。
Destructure 在解釋 Destructure 之前,先舉個生動的例子,比如吃在奧利奧的是時候,我的吃法是這樣的:
如果寫成代碼,大致應該是這樣的:
var orea = ["top","middle","bottom"]
var top = orea.shift(),middleAndButton=orea // <1>
var wetMiddleAndButton = dipMilk(middleAndButton) // <2>
var button = lip(wetMiddleAndButton) // <3>
eat([top,button]) // <4>
注意那個詭異的 shift ,如果用 destructure 會寫得稍微優(yōu)雅一些:
var [top, ...middleAndButton] = ["top","middle","bottom"] // <1>
var wetMiddleAndButton = dipMilk(middleAndButton) // <2>
var button = lip(wetMiddleAndButton) // <3>
eat([top,button]) // <4>
有沒有覺得我掰奧利奧的姿勢變酷了許多?這就是 destructure,給定一個特定的模式 [top, ...middleAndButton],讓數(shù)據(jù)["top","middle","bottom"] 按照該模式匹配進來。同樣的,我將會專門在第 6 章介紹模式匹配這個概念,雖然它不是 Clojure 的重要概念,但是確實 Scala 或 Haskell 的核心所在。不過可以放心的是,你也不必在此之前先學習 Scala 和 Haskell,我還是會用最流行的 JavaScript 來介紹模式匹配。
http://wiki.jikexueyuan.com/project/clojure-flavored-javascript/images/patten-matching.jpg" alt="" />
圖 3 我覺得這個玩具可以特別形象的解釋模式匹配這個概念
作為多編程范式的語言,原型鏈支持的當然是面相對象編程,然而卻同時支持一等函數(shù)的 JavaScript 也給函數(shù)式編程帶來了無限的可能。之所以說可能是因為 JavaScript 本身對于函數(shù)式的支持還是非常局限的,為了讓 JavaScript 全面支持函數(shù)式編程還需要非常多的第三方庫的支持。下面我們來列一列到底 JavaScript 比起純函數(shù)式語言,到底還差些什么?
首先需要支持的當然是不可變(immutable)數(shù)據(jù)結(jié)構(gòu),意味著任何操作都不會改變該數(shù)據(jù)結(jié)構(gòu)的內(nèi)容。JavaScript 中除了原始類型其他都是可變的(mutable)。相反,Clojure 的所有數(shù)據(jù)結(jié)構(gòu)都是不可變的。
JavaScript 一共有 6 種原始類型(ES6 新加了 Symbol 類型),它們分別是 Boolean,Null,Undefined,Number String 和 Symbol。 除了這些原始類型,其他的都是 Object,而 Object 都是可變的。
比如 JavaScript 的 Array 是可變的:
var a = [1,2,3]
a.push(4)
a 的引用雖然沒有變,但是內(nèi)容確發(fā)生了變化。
而 Clojure 的 Vector 類型則行為剛好相反:
(def a [1 2 3])
(conj a 4) ;; => [1 2 3 4]
a ;; => [1 2 3]
對 a 的操作并沒有改變 a 的內(nèi)容,而是 conj 操作返回 的改變后的新列表。在接下來的第二章你將會看到 Clojure 是如何實現(xiàn)不可變數(shù)據(jù)結(jié)構(gòu)的。
惰性(lazy)指求值的過程并不會立刻發(fā)生。比如一些數(shù)學題(特別是求極限的)我們可能不需要把所有表達式求值才能得到最終結(jié)果,以防在算過程中一些表達式能被消掉。所以惰性求值是相對于及早求值(eager evaluation)的。
比如大部分語言中,參數(shù)中的表達式都會被先求值,這也稱為 應用序 語言。比如來看下這樣個 JavaScript 的函數(shù):
wholeNameOf(getFirstName(), getLastName())
getFirstName 與 getLastName 會依次執(zhí)行,返回值作為 wholeNameOf 函數(shù)的參數(shù), wholeNameOf最后被調(diào)用。
另外,對于數(shù)組操作時,大部分語言也同樣采用的是應用序。
map(function(x){return ++x}, [1,2,3,4])
所以,這個表達式立刻會返回結(jié)果 [1,2,3,4] 。
當然這并不是說 Javascript 語言使用應用序有問題,但是沒有提供惰性序列的支持就是 JavaScript 的不對了。如果 map 后發(fā)現(xiàn)其實我們只需要前 10 個元素時,去計算所有元素就顯得是多余的了。
面向?qū)ο笸ǔ1槐扔鳛槊~,而函數(shù)式編程是動詞。面向?qū)ο蟪橄蟮氖菍ο?,對于對象的的描述自然是名詞。面向?qū)ο蟀阉胁僮骱蛿?shù)據(jù)都封裝在對象內(nèi),通過接受消息做相應的操作。比如,對象 Kitty 和 Pussy,它們可以接受“打招呼”的消息,然后做相應的動作。而函數(shù)式的抽象方式剛好相反,是把動作抽象出來,比如就是一個函數(shù)“打招呼”,而參數(shù),則是作為數(shù)據(jù)傳入的 Kitty 或者 Pussy,是完全透明的。比如 Kitty 進入函數(shù)“打招呼”時,出來的應該是一只 Hello Kitty 。
面向?qū)ο罂梢酝ㄟ^繼承和組合在對象之間分享一些行為或者說屬性,函數(shù)式的思路就是通過 組合 已有函數(shù)形成一個新的函數(shù)。JavaScript 語言雖然支持高階函數(shù),但是并沒有一個原生的利于組合函數(shù)產(chǎn)生新函數(shù)的方式。關于函數(shù)組合的技巧,會在第四章作詳細的解釋,而這些強大的函數(shù)組合方式卻往往被類似 underscore 庫的光芒掩蓋掉。
Clojure 的數(shù)據(jù)結(jié)構(gòu)都是不可變的,除了使用數(shù)據(jù)結(jié)果本身的方法進行遍歷,另外的循環(huán)手段自然只能是遞歸了。但是在沒尾遞歸優(yōu)化的 JavaScript 中就不會那么愉快了。
在 JavaScript 中可能會經(jīng)??吹竭@樣的代碼:
var a = [1,2,3,4]
var b = [4,3,2,1]
for(var i=0;i<10;i++)
a[i]+=b[i]
console.log(a);
// => [5,5,5,5]
如果使用 Clojure 硬要做類似的事情通常只能使用 reduce 解決,代碼會變成這樣:
(loop [a [1 2 3 4]
b [4 3 2 1]
i (dec (len a))]
(recur (assoc a i (get b i) b (dec i))))
recur 看起來跟 for 循環(huán)非常類似,其實它是尾遞歸,如果把 loop 寫成一個函數(shù):
(defn zipping-add [a b i]
(recur (assoc a i (get b i) b (dec i))))
(zipping-add [1 2 3 4] [4 3 2 1] (dec (len a)))
事實上效果是一樣的,但是如果把 recur想象成是 zipping-add ,明顯能看出 zipping-add 是一個尾遞歸函數(shù)。
因此反過來看,若是要把尾遞歸換成循環(huán)是多么容易的一件事情,關鍵的是需要讓解釋器識別出來尾遞歸。
但是這不是 Clojure 的風格,亦不是函數(shù)式的風格。遞歸應該被認為是比較低級別的操作,像這種高級別的操作還是應該優(yōu)先使用 map,reduce 來解決。
(map #(+ %1 %2) [1 2 3 4] [4 3 2 1])
Clojure 的 map 是個神奇的函數(shù),若是給多個向量,他做的事情會相當于先 zip 成一個向量,再把向量的元素 apply 到組合子上。這樣完全不需要循環(huán)和變量,得到了一段不需要循環(huán)和變量的簡潔的代碼。 但是,在寫低級別的一些代碼的時候,遞歸還是強有力的武器,而且尾遞歸優(yōu)化能帶來更好的性能,在第五章我會更詳細的介紹不可變數(shù)據(jù)結(jié)構(gòu)以及遞歸。
如果提到 JavaScript 的函數(shù)式庫,可能會聯(lián)想到 Underscore12。Underscore 的官網(wǎng)解釋是這樣的:
Underscore 提供了 100 多個函數(shù),不僅有常見的函數(shù)式小助手: map,filter,invoke,還有更多的一些額外的好處……
我就懶得翻譯完了,重點是這句話里面的“函數(shù)式小助手”,這點我實在不是很同意。
比如 map 這個函數(shù)式編程中比較常見的函數(shù),我們來看看看 函數(shù)式語言 中都是怎么做 map 的:
Clojure:
(map inc [1 2 3])
其中 inc 是一個給數(shù)字加一的函數(shù)。 Haskell:
map (1+) [1,2,3]
同樣 (1+) 是一個函數(shù),可以給數(shù)字進行加一操作。
這是非常簡單的 map 操作,應用函數(shù) inc, (1+) 到數(shù)組 中的每一個元素。同樣的事情我們試試用 Underscore 來實現(xiàn)一下:
_.map([1,2,3], function(x){return x+1})
感覺到有什么變化了嗎?有沒有發(fā)現(xiàn)參數(shù)的順序完全不同了?好吧,你可能要說這并不是什么問題?。坎痪褪?map 的 api 設計得不太一樣么?也沒有必要保持所有的語言的 map 都是一樣的吧?
在回答這個問題之前,我想再舉幾個例子,因為除了 Underscore,JavaScript 的函數(shù)式庫還有很多很多:
R.map(function(x){return x+1}, [1,2,3])
fjs.map(function(x){return x+1}, [1,2,3])
應該不需要再多的例子了,不管怎么樣看,underscore 的 map 是否都略顯另類了呢?跟別的語言不一樣就算了,跟其他 JavaScript 的函數(shù)式庫都不一樣的話,是不是有些說不過去了。 我猜 underscore 同學估計現(xiàn)在有種高考出來跟同學對答案,發(fā)現(xiàn)自己的答案跟別人的完全不一樣的心情。
好吧,Underscore 先別急著認錯,大家都這么做,肯定不是偶然。但是原因就說來話長了,我將會在第四章詳細解釋其他函數(shù)式語言/庫為什么都跟 Underscore 不一樣。
當然我可不會選一個“另類”的庫來闡述函數(shù)式編程。我將像編程世界中最好的書《計算機程序的構(gòu)造與解釋》一樣,我選擇用 lisp 語言來闡述函數(shù)式編程概念,而用目前最流行的語言 —— JavaScript 來實踐函數(shù)式。當然我也不會真的用老掉牙的 scheme,因為所有前端開發(fā)者都應該知道,前端最唾棄的就是使用久的東西,這樣一來 Clojure 這門全新的現(xiàn)代 lisp 方言顯然是最好的選擇。
Clojure 是跑著 JVM 上的 lisp 方言,而 ClojureScript 是能編譯成 JavaScript 的 Clojure。但是請不要把 ClojureScript 與 CoffeeScript,LiveScript,TypeScript 做比較,就像每一行 Clojure 代碼不能一一對應到 Java 代碼一樣,你可能很難像 CoffeeScript 對應 JavaScript 一樣能找到 ClojureScript 與其編譯出來的 JavaScript 的對應關系。
http://wiki.jikexueyuan.com/project/clojure-flavored-javascript/images/everyscript.png" alt="" />
圖 4 各種編譯成 JavaScript 的函數(shù)式語言
不管怎么樣,ClojureScript 把 Clojure 帶到了前端確實是非常令人激動的一件事情。就跟前端程序員能在后端寫 JavaScript 一樣,Clojure 程序員終于能在前端也能找到自己熟悉的編程姿勢。但是如同 Clojure 于 Java 的交互一樣(或者更壞), ClojureScript 與 JavaScript 及 JavaScript 的庫的交互并不是那么容易,或者可以說,不那么優(yōu)雅。而且前端開發(fā)者可能并不能很快的適應 lisp 語言,項目(特別是開源項目)的維護不能只靠懂 clojure 的少數(shù)開發(fā)者,所以如果能用最受歡迎的 JavaScript,又還能使用到 Clojure 的所有好處,那將再好不過了。幸運的是,Clojure 的持久性數(shù)據(jù)結(jié)構(gòu)被 David Nolen 移植到了原生 JavaScript —— mori。
由于是移植的,所有的數(shù)據(jù)結(jié)構(gòu)以及操作數(shù)據(jù)結(jié)構(gòu)的函數(shù)都是 ClojureScript 保持一致,而且是作為 JavaScript 庫,可以在原生 JavaScript 的代碼中使用。顯然 mori 是最適合用于前端函數(shù)式實踐的庫,當然也是本書為什么說是 Clojure 風格的函數(shù)式 JavaScript 的原因了。
選擇 mori 的另一原因是因為它特別區(qū)別于其他的函數(shù)式庫的地方——它使用 ClojureScript 的數(shù)據(jù)結(jié)構(gòu)。也就是說從根本上消除了 JavaScript 可變的數(shù)據(jù)結(jié)構(gòu)模型,更利于我們的進行函數(shù)式編程。
為了保持從風格上更類似于 Clojure,以及遷移 Clojure 中的一些 macro,本書中也使用了我寫的一系列的 macro —— ru-lang。更多的關于 macro 的討論我會放到第五章。
當然,選擇 mori 并不說明它是工程的上函數(shù)式類庫的最佳選擇,facebook 活躍維護的 Immutable.js 也是不錯的選擇。但是在這里,mori 確實是能將 Clojure 編程思想蔓延到 JavaScript 中的最好橋梁。