以往常常看到」函數式編程「這一名詞,卻始終沒有花時間去學習,暑期實習結束以後一直忙於邊養老邊減肥,81天成功瘦身30斤+ ,開始迴歸正常的學習生活。
便在看《JavaScript函數式編程》這本書,以系統瞭解函數式編程的知識。本文試圖儘量系統的描述JavaScript函數式編程。固然認識暫時停留於本書介紹的程度,若有錯誤之處,還請指正。編程
注:本書採用的函數式庫Underscore。一下部分代碼運行時,需引入Underscore。數組
咱們用一句話來直白的描述函數式編程:緩存
函數式編程經過使用函數來將值轉換成抽象單元,接着用於構建軟件系統。閉包
歸納的來講,函數式編程包括如下技術app
肯定抽象,併爲其構建函數dom
利用已有的函數來構建更爲複雜的抽象編程語言
經過將現有的函數傳給其餘函數來構建更加複雜的抽象ide
注:JavaScript並不只限於函數式編程語言,如下是另外3種經常使用的編程方式。函數式編程
命令式編程: 經過詳細描述行爲的編程方式函數
基於原型的面向對象編程: 基於原型對象及其實例的編程方式
元編程:對JavaScript執行模型數據進行編寫和操做的編程方式
純函數堅持如下屬性(堅持純度的標準不只將有助於使程序更容易測試,也更容易推理。)
其結果只能從它的參數的值來計算
不能依賴於能被外部操做改變的數據
不能改變外部狀態
所謂"反作用"(side effect),指的是函數內部與外部互動(最典型的狀況,就是修改全局變量的值),產生運算之外的其餘結果。
函數式編程強調沒有"反作用",意味着函數要保持獨立,全部功能就是返回一個新的值,沒有其餘行爲,尤爲是不得修改外部變量的值。
上一點已經提到,函數式編程只是返回新的值,不修改系統變量。所以,不修改變量,也是它的一個重要特色。在其餘類型的語言中,變量每每用來保存"狀態"(state)。不修改變量,意味着狀態不能保存在變量中。
函數式編程使用參數保存狀態,最好的例子就是遞歸。下面的代碼是一個將字符串逆序排列的函數,它演示了不一樣的參數如何決定了運算所處的"狀態"。
function reverse(string) { if(string.length == 0) { return string; } else { return reverse(string.substring(1, string.length)) + string.substring(0, 1); } }
「一等」這個術語一般用來描述值。當函數被看做「一等公民」時,那它就能夠去任何值能夠去的地方,不多有限制。好比數字在Javascript裏就是一等公民,同程
做爲一等公民的函數就會擁有相似數字的性質。
var fortytwo = function(){return 42} // 函數與數字同樣能夠存儲爲變量 var fortytwo = [32, function(){return 42}] // 函數與數字同樣能夠存儲爲數組的一個元素 var fortytwo = {number: 32, fun: function(){return 42}} // 函數與數字同樣能夠做爲對象的成員變量 32 + (function(){return 42}) () // 函數與數字同樣能夠在使用時直接建立出來 // 函數與數字同樣能夠被傳遞給另外一個函數 function weirdAdd(n, f){ return n + f()} weirdAdd(32, function(){return 42}) // 函數與數字同樣能夠被另外一個函數返回 return 32; return function(){return 42}
Applicative編程是特殊函數式編程的一種形式。Applicative編程的三個典型例子是map,reduce,filter
函數A做爲參數提供給函數B。 (即定義一個函數,讓它接收一個函數,而後調用它)
_.find(["a","b",3,"d"], _.isNumber) // _.find與_.isNumber都是Underscore中的方法 // 自行實現一個Applicative函數 function exam(fun, coll) { return fun(coll); } // 調用 exam(function(e){ return e.join(",") }, [1,2,3]) // 結果 」1,2,3「
定義:一個高階函數應該能夠執行如下至少一項操做。
以一個函數做爲參數
返回一個函數做爲結果
// max是一個高階函數 var people = [{name: "Fred", age: 65}, {name: "Lucy", age: 36}]; _.max(people, function(p) { return p.age }); //=> {name: "Fred", age: 65}
可是,在某些方面這個函數是受限的,並非真正的函數式。具體來講,對於_.max
而言,比較老是須要經過大於運算符(>)來完成。
不過,咱們能夠建立一個新的函數finder。它接收兩個函數:一個用來生成可比較的值,而另外一個用來比較兩個值並返回當中的」最佳「值。
function finder(valueFun, bestFun, coll) { return _.reduce(coll, function(best, current) { var bestValue = valueFun(best); var currentValue = valueFun(current); return (bestValue === bestFun(bestValue, currentValue)) ? best : current; }); }
在任何狀況下,咱們如今均可以用finder
來找到不一樣類型的」最佳「值:
finder(function(e){return e.age}, Math.max, people) // => {name: 」Fred", age: 65} finder(function(e){return e.name}, function(x, y){ return (x.charAt((0) === "L") ? x : y),people}) // 偏好首字母爲L的人 // => {name:"Lucy", age: 36}
縮減一點
函數finder
短小精悍,而且能按照咱們預期來工做,但爲了知足最大程度的靈活性,它重複了一些邏輯。
// 在 finder函數中 return (bestValue === bestFun(bestValue, currentValue)) ? best : current; // 在輸入的函數參數中 return (x.charAt((0) === "L") ? x : y
你會發現上述二者的邏輯是徹底相同的。finder的實現能夠根據如下兩個假設來縮減。
若是第一個參數比第二個參數「更好」,比較最佳值的函數返回爲true
比較最佳值的函數知道如何「分解」它的參數
在以上假設的基礎下,咱們能夠實現一個更簡潔的best函數。
function best(fun, coll) { return _.reduce(coll, function(x, y) { return fun(x, y) ? x : y; }); } best(function(x,y) { return x > y }, [1,2,3,4,5]); //=> 5
首先,從一個簡單的函數repeat
開始。它以一個數字和一個值爲參數,將該值進行屢次複製,並放入一個數組中:
function repeat(times, VALUE) { return _.map(_.range(times), function() { return VALUE; }); } repeat(4, "Major"); //=> ["Major", "Major", "Major", "Major"]
使用函數,而不是值
經過將參數從值替換爲函數,打開了一個充滿可能性的世界。
function repeatedly(times, fun) { return _.map(_.range(times), fun); } repeatedly(3, function() { return Math.floor((Math.random()*10)+1); }); //=> [1, 3, 8]
再次強調,「使用函數,而不是值」
咱們經常會知道函數應該被調用多少次,但有時候也知道何時推出並不取決於「次數」,而是條件!所以我能夠定義另外一個名爲iterateUntil
的函數。iterateUntil
接收2個參數,一個用來執行一些動做,另外一個用來進行結果檢查。
function iterateUntil(fun, check, init) { var ret = []; var result = fun(init); while (check(result)) { ret.push(result); result = fun(result); } return ret; };
function invoker (NAME, METHOD) { // 接收一個方法,並在任何給定的對象上調用它 return function(target /* args ... */) { if (!existy(target)) fail("Must provide a target"); var targetMethod = target[NAME]; var args = _.rest(arguments); return doWhen((existy(targetMethod) && METHOD === targetMethod), function() { return targetMethod.apply(target, args); }); }; }; var rev = invoker('reverse', Array.prototype.reverse); _.map([[1,2,3]], rev); //=> [[3,2,1]]
高階函數捕獲參數
高階函數的參數是用來「配置」返回函數的行爲的。對於makeAdder
而言,它的參數配置了其返回函數每次添加數值的大小
function makeAdder(CAPTURED) { return function(free) { return free + CAPTURED; }; } var add10 = makeAdder(10); add10(32); //=> 42
捕獲變量的好處
用閉包來捕獲增長值,並用做後綴。(但這樣並不具備引用透明)
function makeUniqueStringFunction(start) { var COUNTER = start; return function(prefix) { return [prefix, COUNTER++].join(''); } }; var uniqueString = makeUniqueStringFunction(0); uniqueString("dari"); //=> "dari0" uniqueString("dari"); //=> "dari1"
精華:使用現有的零部件來創建新的行爲,這些新行爲一樣也成爲了已有的零部件。
// 接收一個或多個函數,而後不斷嘗試依次調用這些函數的方法,直到返回一個非`undefined`的值 function dispatch(/* funs */) { var funs = _.toArray(arguments); var size = funs.length; return function(target /*, args */) { var ret = undefined; var args = _.rest(arguments); for (var funIndex = 0; funIndex < size; funIndex++) { var fun = funs[funIndex]; ret = fun.apply(fun, construct(target, args)); if (existy(ret)) return ret; } return ret; }; } var str = dispatch(invoker('toString', Array.prototype.toString), invoker('toString', String.prototype.toString)); str("a"); //=> "a" str(_.range(10)); //=> "0,1,2,3,4,5,6,7,8,9"
在這裏,咱們想作的只是返回一個遍歷函數數組,並apply
給一個目標對象的函數,返回第一個存在的值。dispatch
知足了多態JavaScript
函數的定義。這樣簡化了委託具體方法的任務。例如,在underscore的實現中,你常常會看到許多不一樣的函數重複這樣的模式。
確保目標的存在
檢查是否有原生版本,若是是則使用它
若是沒有,那麼作一些實現這些行爲的具體任務。
作特定類型的任務(如適用)
作特定參數的任務(如適用)
作特定個參數的任務(如適用)
一樣的模式也體如今Underscore
的函數_.map()
的實現中:
_.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; };
使用dispatch
能夠簡化一些這方面的代碼,而且更容易擴展。想象一下,你正在寫一個能夠爲數組和字符串類型生成字符描述的
函數。使用dispatch則能夠優雅的實現:
var str = dispatch(invoker('toString', Array.prototype.toString), invoker('toString', String.prototype.toString)); str("a"); //=> "a" str(_.range(10)); //=> "0,1,2,3,4,5,6,7,8,9"
柯里化函數爲每個邏輯參數返回一個新函數。
例如:
// 除法 function divide(n,d){ return n/d; } // 手動柯里化 function curryDivide(n) { return function(d) { return n/d; }; }
curryDivide
是手動柯里化函數,也就是說,我顯示地返回對應參數數量的函數。
自動柯里化參數
// 接收一個函數,並返回一個只接受一個參數的函數。 function curry(fun) { // 柯里化一個參數,雖然彷佛沒什麼用 return function(arg) { return fun(arg); }; } function curry2(fun) { // 柯里化兩個參數 return function(secondArg) { return function(firstArg) { return fun(firstArg, secondArg); }; }; } function curry3(fun) { // 柯里化三個參數 return function(last) { return function(middle) { return function(first) { return fun(first, middle, last); }; }; }; };
curry2函數接受一個函數並將其柯里化成兩個深層參數的函數。能夠用它來實現先前定義的除法函數。
var divide10 = curry2(div)(10) divide10(50) // => 5
柯里化函數有利於指定JavaScript函數行爲,並將現有函數「組合」爲新函數。而且使用柯里化比較容易產生流利的函數式API。
柯里化函數逐漸返回消耗參數的函數,直到全部參數都耗盡。然而,部分應用函數是一個「部分「執行,等待接收剩餘的參數當即執行的函數。
// 部分應用一個或兩個已知的參數 function partial1(fun, arg1) { return function(/* args */) { var args = construct(arg1, arguments); // construct爲拼接數組,在此代碼略去 return fun.apply(fun, args); }; } function partial2(fun, arg1, arg2) { return function(/* args */) { var args = cat([arg1, arg2], arguments); // cat也爲拼接數組,在此代碼略去 return fun.apply(fun, args); }; } // 部分應用任意數量的參數 function partial(fun /*, pargs */) { var pargs = _.rest(arguments); return function(/* arguments */) { var args = cat(pargs, _.toArray(arguments)); return fun.apply(fun, args); }; }
一種理想化的函數式程序是向函數流水線的一端輸送的一塊數據,從另外一端輸出一個全新的數據塊。!_.isString(name)
這個流水線由_.isString
和!
組成
_.isString
接收一個對象,並返回一個布爾值
!
接收一個布爾值,並返回一個布爾值
// 經過組合多個函數及其數據轉換創建新的函數 function isntString(str){ return !_.isString(str) } isntString(1) // => true // 還可使用Underscore的_.compose函數實現一樣的功能 // _.compose函數從右往左執行。即最右邊函數的結果會被送入其左側的函數,一個接一個 var isntString = _.compose(function(x) { return !x }, _.isString); isntString([]); //=> true
理解遞歸對理解函數式編程來講很是重要,緣由有三。
遞歸的解決方案包括使用對一個普通問題子集的單一抽象的使用
遞歸能夠隱藏可變狀態
遞歸是一種實現懶惰和無限大結構的方法
在編寫自遞歸函數時,規則以下
知道何時中止
決定怎樣算一個步驟
把問題分解成一個步驟和一個較小的問題
function myLength(ary) { if (_.isEmpty(ary)) // _.isEmpty什麼時候中止 return 0; else // 進行一個步驟 1+ ; return 1 + myLength(_.rest(ary)); // 小一些的問題 _.rest(ary) }
尾遞歸
尾遞歸與通常自遞歸的明顯區別是,」一個步驟「和」縮小的問題「中的元素都要進行遞歸調用。
function tcLength(ary, n) { var l = n ? n : 0; if (_.isEmpty(ary)) return l; else return tcLength(_.rest(ary), l + 1); } tcLength(_.range(10)); //=> 10
兩個或多個函數相互調用被稱爲相互遞歸。下面看一個例子,用謂詞函數來檢查偶數和奇數:
function evenSteven(n) { if (n === 0) return true; else return oddJohn(Math.abs(n) - 1); } function oddJohn(n) { if (n === 0) return false; else return evenSteven(Math.abs(n) - 1); } // 相互遞歸調用來回反彈彼此之間遞減某個絕對的值,知道一方或另外一方達到0 evenSteven(4) // => true oddJohn(11) // =>true
儘管遞歸技術上是可行的,可是由於JavaScript引擎沒有優化遞歸調用,所以,在使用或寫遞歸函數時,可能會碰到以下錯誤
evenSteven(10000) // 棧溢出
遞歸應該被看做一個底層操做,應該儘量地避免(很容易形成棧溢出)。普通的共識是,首先是要函數組合,僅當須要的時才使用遞歸和蹦牀。
蹦牀(tramponline):使用蹦牀展平調用,而不是深度嵌套的遞歸調用。
首先,看看如何手動修復evenOline
和oddOline
使得遞歸調用不會溢出。一個辦法是返回一個函數,它包裝調用,而不是直接直接調用。
function evenOline(n) { if (n === 0) return true; else return partial1(oddOline, Math.abs(n) - 1); } function oddOline(n) { if (n === 0) return false; else return partial1(evenOline, Math.abs(n) - 1); } oddOline(3)()() // 返回的只是一個函數調用 // => function(){return evenOline(Math.abs(n) - 1)} oddOline(3)()()() // 將函數調用執行 // => true oddOline(10000)()()()... // 10000個()去執行返回的函數調用 // => true
固然,咱們不能直接向用戶暴露這個API,能夠提升另一個函數trampoline
,從程序執行來進行扁平化處理。
function trampoline(fun /*, args */) { // 不斷調用函數的返回值,知道它不是一個函數爲止 var result = fun.apply(fun, _.rest(arguments)); while (_.isFunction(result)) { result = result(); } return result; } trampoline(oddOline, 10000) // false
因爲調用鏈的間接性,使用蹦牀增長了相互遞歸函數的一些開銷。然而滿總比溢出要好。一樣,你可能不但願強迫用戶使用trampoline
,只是爲了不堆棧溢出。咱們能夠進一步隱藏其外觀。
function isEvenSafe(n) { if (n === 0) return true; else return trampoline(partial1(oddOline, Math.abs(n) - 1)); } function isOddSafe(n) { if (n === 0) return false; else return trampoline(partial1(evenOline, Math.abs(n) - 1)); }
使用jQuery等庫常常會使用連接,連接可讓咱們的代碼更加簡潔,以下是連接的實現示例。
連接方法的原理在於。每一個連接的方法都返回統一的宿主對象引用。
function createPerson() { var firstName = ""; var age = 0; return { setFirstName: function(fn) { firstName = fn; return this; }, setAge: function(a) { age = a; return this; }, toString: function() { return [firstName, lastName, age].join(' '); } }; } createPerson() .setFirstName("Mike") .setAge(108) .toString(); //=> "Mike 108"
惰性鏈
上述連接是直接執行,然而咱們也能夠實行惰性鏈,即便其先緩存待執行的函數,等到調用執行函數時一塊兒執行。
封裝了一些行爲的函數一般被稱爲thunk
,存儲在_calls
中的thunk
期待將做爲接受force
方法調用的對象的中間目標。
function LazyChain(obj) { this._calls = []; // 用於緩存待執行函數的數組 thunk this._target = obj; // 目標對象 } LazyChain.prototype.invoke = function(methodName /*, args */) { // 將函數壓入的方法 var args = _.rest(arguments); this._calls.push(function(target) { var meth = target[methodName]; return meth.apply(target, args); }); return this; }; LazyChain.prototype.force = function() { // 強制執行this._calls中的函數 return _.reduce(this._calls, function(target, thunk) { return thunk(target); }, this._target); }; // 使用,直到force方法被調用纔將 concat, sort,join執行 new LazyChain([2,1,3]) .invoke('concat', [8,5,7,6]) .invoke('sort') .invoke('join',' ') .force(); // => "1 2 3 4 5 6 7 8"
連接模式有利於給對象的方法調用建立流程的API,可是對於函數式API則未必。
方法鏈接有各類各樣的缺點,包括緊耦合對象的set和get邏輯。主要問題是,函數鏈常常會作調用之間改變傳遞的共同引用。函數式API重點在操做值而不是引用。
一下是管道的具體實現
function pipeline(seed /*, args */) { return _.reduce(_.rest(arguments), function(l,r) { return r(l); }, seed); }; pipeline(42, function(n){return -n},function(n){return n+1}) // => -41
本文更多的是對《JavaScript函數式編程》一書的摘要,並透過一段段代碼試圖闡述函數式編程的思想。
但願之後的工做中可以吸收函數式編程的好,並慢慢對其加深理解。從書中獲取知識,最終仍是要落於實踐中去的。
同時,但願可以經過這篇文章幫助不瞭解函數式編程的小夥伴創建系統的認識。
WilsonLiu's blog首發地址:http://blog.wilsonliu.cn