下一篇《函數式編程之Promise的奇幻漂流》 javascript
曾經的你是否是總在工做和學習過程當中聽到函數式編程(FP)。但學到函子的時候老是一頭霧水。本文是我在函數式編程學習過程當中,總結的筆記,也分享給想學函數式編程的同窗。html
在學以前,你先問本身幾個問題,或者看成一場面試,看看下面的這些問題,你該怎麼回答?前端
var Container = function(x) { this.__value = x; }
Container.of = x => new Container(x);
Container.prototype.map = function(f){
console.log(f)
return Container.of(f(this.__value))
}
Container.of(3).map(x=>x+1).map(x => 'Result is ' + x);
console.log(Container.of(3).map(x=>x+1).map(x => 'Result is ' + x))
複製代碼
如今就讓咱們帶着問題去學習吧。文章的最後,咱們再次總結這些問題的答案。java
面對對象(OOP)能夠理解爲是對數據的抽象,好比把一我的抽象成一個Object,關注的是數據。 函數式編程是一種過程抽象的思惟,就是對當前的動做去進行抽象,關注的是動做。react
舉個例子:若是一個數a=1 ,咱們但願執行+3(f函數),而後再*5(g函數),最後獲得結果result是20
數據抽象,咱們關注的是這個數據:a=1 通過f處理獲得 a=4 , 再通過g處理獲得 a = 20
過程抽象,咱們關注的是過程:a要執行兩個f,g兩操做,先將fg合併成一個K操做,而後a直接執行K,獲得 a=20
複製代碼
問題:f和g合併成了K,那麼能夠合併的函數須要符合什麼條件呢?下面就講到了純函數的這個概念。es6
定義:一個函數若是輸入參數肯定,輸出結果是惟一肯定的,那麼他就是純函數。
特色:無狀態,無反作用,無關時序,冪等(不管調用多少次,結果相同)面試
下面哪些是純函數 ?編程
let arr = [1,2,3];
arr.slice(0,3); //是純函數
arr.splice(0,3); //不是純函數,對外有影響
function add(x,y){ // 是純函數
return x + y // 無狀態,無反作用,無關時序,冪等
} // 輸入參數肯定,輸出結果是惟一肯定
let count = 0; //不是純函數
function addCount(){ //輸出不肯定
count++ // 有反作用
}
function random(min,max){ // 不是純函數
return Math.floor(Math.radom() * ( max - min)) + min // 輸出不肯定
} // 但注意它沒有反作用
function setColor(el,color){ //不是純函數
el.style.color = color ; //直接操做了DOM,對外有反作用
}
複製代碼
是否是很簡單,接下來咱們加一個需求?
若是最後一個函數,你但願批量去操做一組li而且還有許多這樣的需求要改,寫一個公共函數?數組
function change (fn , els , color){
Array.from(els).map((item)=>(fn(item,color)))
}
change(setColor,oLi,"blue")
複製代碼
那麼問題來了這個函數是純函數嗎?bash
首先不管輸入什麼,輸出都是undefined,接下來咱們分析一下對外面有沒有影響,咱們發現,在函數裏並無直接的影響,可是調用的setColor對外面產生了影響。那麼change到底算不算純函數呢?
答案是固然不算,這裏咱們強調一點,純函數的依賴必須是無影響的,也就是說,在內部引用的函數也不能對外形成影響。
問題:那麼咱們有沒有什麼辦法,把這個函數提純呢?
定義:只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
javascript
function add(x, y) {
return x + y;
}
add(1, 2)
******* 柯里化以後 *************
function addX(y) {
return function (x) {
return x + y;
};
}
var newAdd = addX(2)
newAdd (1)
複製代碼
如今咱們回過頭來看上一節的問題?
若是咱們不讓setColor在change函數裏去執行,那麼change不就是純函數了嗎?
javascript
function change (fn , els , color){
Array.from(els).map((item)=>(fn(item,color)))
}
change(setColor,oLi,"blue")
****** 柯里化以後 *************
function change(fn){
return function(els,color){
Array.from(els).map((item)=>(fn(item,color)))
}
}
var newSetColor = change(setColor);
newSetColor(oLi,"blue")
複製代碼
上面那個例子是直接重寫了change函數,能不能直接在原來change的基礎上經過一個函數改爲 newSetColor呢?
javascript
function change (fn , els , color){
Array.from(els).map((item)=>(fn(item,color)))
}
change(setColor,oLi,"blue")
//******* 經過一個curry函數*************
var changeCurry = curry(change);
var newSetColor = changeCurry(setColor);
newSetColor(oLi,"blue")
複製代碼
哇!真的有這種函數嗎?固然做爲幫助函數(helper function),lodash 或 ramda都有啊。咱們在深刻的系列的課程中會動(chao)手(xi)寫一個。
問題:處理上一個問題時,咱們將一個函數做爲參數傳到另外一個函數中去處理,這好像在函數式編程中很常見,他們有什麼規律嗎?
定義:函數當參數,把傳入的函數作一個封裝,而後返回這個封裝函數,達到更高程度的抽象。
很顯然上一節用傳入fn的change函數就是一個高階函數,顯然它是一個純函數,對外沒有反作用。可能這麼講並不能讓你真正去理解高階函數,那麼我就舉幾個例子!
定義 :調用函數自己的地方均可以其等價函數;
javascript
function __equal__(fn){
return function(...args){
return fn.apply(this,args);
}
}
//第一種
function add(x,y){
return x + y
}
var addnew1 = __equal__(add);
console.log(add(1,2));
console.log(addnew1(1,2));
//第二種
let obj = {
x : 1,
y : 2,
add : function (){
console.log(this)
return this.x + this.y
}
}
var addnew2 = __equal__(obj.add);
console.log( obj.add() ) ; //3
console.log( addnew2.call(obj)); //3
複製代碼
第一種不考慮this
第二種考慮this
好了,看懂代碼後,咱們發現,這好像和直接把函數賦值給一個變量沒啥區別,那麼等價函數有什麼好處呢?
等價函數的攔截和監控:
javascript
function __watch__(fn){
//偷偷乾點啥
return function(...args){
//偷偷乾點啥
let ret = fn.apply(this,args);
//偷偷乾點啥
return ret
}
}
複製代碼
咱們知道,上面本質就是等價函數,fn執行結果沒有任務問題。可是能夠在執行先後,偷偷作點事情,好比consle.log("我執行啦")。
問題:等價函數能夠用於攔截和監控,那有什麼具體的例子嗎?
前端開發中會遇到一些頻繁的事件觸發,爲了解決這個問題,通常有兩種解決方案:
javascript
function throttle(fn,wait){
var timer;
return function(...args){
if(!timer){
timer = setTimeout(()=>timer=null , wait);
console.log(timer)
return fn.apply(this,args)
}
}
}
const fn = function(){ console.log("btn clicked")}
const btn = document.getElementById('btn');
btn.onclick = throttle(fn , 5000);
複製代碼
分析代碼
因此,咱們經過對等價函數監控和攔截很好的實現了節流(throtle)函數。而對函數fn執行的結果絲毫沒有影響。這裏給你們留一個做業,既然咱們實現了節流函數,那麼你能不能根據一樣的原理寫出防抖函數呢?
問題:哦,像這樣節流函數,在我平時的項目中直接寫就行了,你封裝成這樣一個函數彷佛還麻煩了呢?
在平時,若是咱們不借助方法函數去實現節流函數,咱們可能會直接這麼去實現節流函數。
var timer;
btn.onclick = function(){
if(!timer){
timer = setTimeout(()=>timer=null , 5000);
console.log("btn clicked")
}
}
複製代碼
那麼與以前的高階函數有什麼區別呢?
很顯然,在下面的這例子中,咱們每次在須要作節流的時候,咱們每次都須要這樣從新寫一次代碼。告訴 程序如何執行。而上面的高階函數的例子,咱們定義好了一個功能函數以後,咱們只須要告訴程序,你要作 什麼就能夠啦。
那下面你們看看,若是遍歷一個數組,打印出每一個數組中的元素,如何用兩種方法實現呢?
//命令式
var array = [1,2,3];
for (i=0; i<array.length;i++){
console.log(array[i])
}
//聲明式
array.forEach((i) => console.log(i))
複製代碼
看到forEach是否是很熟悉,原來咱們早就在大量使用函數式編程啦。
這裏咱們能夠先停下來從頭回顧一下,函數式編程。
問題:如今咱們對函數編程有了初步的瞭解,但還並無感覺到它的厲害,還記得咱們以前講到的純函數能夠合併嗎?下一節,咱們就去實現它
function double(x) {
return x * 2
}
function add5(x) {
return x + 5
}
double(add5(1))
複製代碼
上面的代碼咱們實現的是完成了兩個動做,不過咱們以爲這樣寫double(add5(x)),不是很舒服。 換一個角度思考,咱們是否是能夠把函數合併在一塊兒。 咱們定義了一個compose函數
var compose = function(f, g) {
return function(x) {
return f(g(x));
};
};
複製代碼
有了compose這個函數,顯然咱們能夠把double和add5合併到一塊兒
var numDeal = compose(double,add5)
numDeal(1)
複製代碼
那麼這時候就有幾個問題,
這三道題咱們留做思考題。咱們在深刻的專題裏會去實現的哈。
問題:如今咱們想完成一些功能都須要去合併函數,並且合併的函數還會有必定順序,咱們能不能像JQ的鏈式調用那樣去處理數據呢。
講到函子,咱們首先回到咱們的問題上來。以前咱們執行函數一般是下面這樣。
function double(x) {
return x * 2
}
function add5(x) {
return x + 5
}
double(add5(1))
//或者
var a = add5(5)
double(a)
複製代碼
那如今咱們想以數據爲核心,一個動做一個動做去執行。
(5).add5().double()
複製代碼
顯然,若是能這樣執行函數的話,就舒服多啦。那麼咱們知道,這樣的去調用要知足
因此咱們試着去給他建立一個引用類型
class Num{
constructor (value) {
this.value = value ;
}
add5(){
return this.value + 5
}
double(){
return this.value * 2
}
}
var num = new Num(5);
num.add5()
複製代碼
咱們發現這個時候有一個問題,就是咱們通過調用後,返回的就是一個值了,咱們沒有辦法進行下一步處理。因此咱們須要返回一個對象。
class Num{
constructor (value) {
this.value = value ;
}
add5 () {
return new Num( this.value + 5)
}
double () {
return new Num( this.value * 2)
}
}
var num = new Num(2);
num.add5 ().double ()
複製代碼
咱們發現,new Num( this.value + 5),中對this.value的處理,徹底能夠經過傳進去一個函數去處理
而且在真實狀況中,咱們也不可能爲每一個實例都建立這樣有不一樣方法的構造函數,它們須要一個統一的方法。
class Num{
constructor (value) {
this.value = value ;
}
map (fn) {
return new Num(fn(this.value))
}
}
var num = new Num(2);
num.map(add5).map(double)
複製代碼
咱們建立了一個map的方法,把處理的函數fn傳了進去。這樣咱們就完美的實現啦,咱們設想的功能啦。
最後咱們整理一下,這個函數。
class Functor{
constructor (value) {
this.value = value ;
}
map (fn) {
return Functor.of(fn(this.value))
}
}
Functor.of = function (val) {
return new Functor(val);
}
Functor.of(5).map(add5).map(double)
複製代碼
如今Functor.of(5).map(add5).map(double)去調用函數。有沒有以爲很爽。
哈哈,更爽的是,你已經在不知不覺間把函子的概念學完啦。上面這個例子總的Functor就是函子。如今咱們來總結一下,它有那些特色吧。
嗯,這下明白什麼是函子了吧。在初學函數編程時,必定不要太過於糾結概念。看到好多,教程上在講 函子時全然不提JavaScript語法。用生硬的數學概念去解釋。
我我的以爲書讀百遍,其義自見。對於編程範式的概念理解也是同樣的,你先知道它是什麼。怎麼用。 多寫多練,天然就理解其中的含義啦。總抱着一堆概念看,是很難看懂的。
以上,函子(Functor)的解釋過程,我的理解。也歡迎你們指正。
問題:咱們實現了一個最通用的函子,如今別問問題,咱們趁熱打鐵,再學一個函子
咱們知道,在作字符串處理的時候,若是一個字符串是null, 那麼對它進行toUpperCase(); 就會報錯。
Functor.of(null).map(function (s) {
return s.toUpperCase();
});
複製代碼
那麼咱們在Functor函子上去進行調用,一樣也會報錯。
那麼咱們有沒有什麼辦法在函子裏把空值過濾掉呢。
class Maybe{
constructor (value) {
this.value = value ;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
}
Maybe.of = function (val) {
return new Maybe(val);
}
var a = Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
複製代碼
咱們看到只須要把在中設置一個空值過濾,就能夠完成這樣一個Maybe函子。
因此各類不一樣類型的函子,會完成不一樣的功能。學到這,咱們發現,每一個函子並無直接去操做須要處理的數據,也沒有參與處處理數據的函數中來。
而是在這中間作了一些攔截和過濾。這和咱們的高階函數是否是有點像呢。因此你如今對函數式編程是否是有了更深的瞭解啦。
如今咱們就用函數式編程作一個小練習: 咱們有一個字符串‘li’,咱們但願處理成大寫的字符串,而後加載到id爲text的div上
var str = 'li';
Maybe.of(str).map(toUpperCase).map(html('text'))
複製代碼
若是在有編寫好的Maybe函子和兩個功能函數的時候,咱們只須要一行代碼就能夠搞定啦
那麼下面看看,咱們的依賴函數吧。
let $$ = id => Maybe.of(document.getElementById(id));
class Maybe{
constructor(value){
this.__value = value;
}
map(fn){
return this.__value ? Maybe.of(fn(this.__value)) : Maybe.of(null);
}
static of(value){
return new Maybe(value);
}
}
let toUpperCase = str => str.toUpperCase();
let html = id => html => {
$$(id).map(dom => {
dom.innerHTML = html;
});
};
複製代碼
咱們來分析一下代碼
var html = function(id) {
return function (html) {
$$(id).map(function (dom) {
dom.innerHTML = html;
});
};
};
複製代碼
你們再來想一個問題 Maybe.of(str).map(toUpperCase).map(html('text'))最後的值是什麼呢?
咱們發現最後沒有處理的函數沒有返回值,因此最後結果應該是 Maybe {__value: undefined}; 這裏面給你們留一個問題,咱們把字符串打印在div上以後想繼續操做字符串該怎麼辦呢?
問題:在理解了函子這個概念以後,咱們來學習本文最後一節內容。有沒有很開心
Monad函子也是一個函子,其實很原理簡單,只不過它的功能比較重要。那咱們來看看它與其它的 有什麼不一樣吧。
咱們先來看這樣一個例子,手敲在控制檯打印一下。
var a = Maybe.of( Maybe.of( Maybe.of('str') ) )
console.log(a);
console.log(a.map(fn));
console.log(a.map(fn).map(fn));
function fn(e){ return e.value }
複製代碼
class Maybe{
constructor (value) {
this.value = value ;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
join ( ) {
return this.value;
}
}
Maybe.of = function (val) {
return new Maybe(val);
}
複製代碼
咱們想取到裏面的值,就把它用join方法返回來就行了啊。因此我給它加了一個join方法
var a = Maybe.of( Maybe.of('str') )
console.log(a.join().map(toUpperCase))
複製代碼
因此如今咱們能夠經過,join的方法一層一層獲得裏面的數據,並把它處理成大寫
如今你確定會好奇爲何會產生Maybe.of( Maybe.of('str')) 結構呢?
還記得html那個函數嗎?咱們以前留了一個問題,字符串打印在div上以後想繼續操做字符串該怎麼辦呢?
很顯然咱們須要讓這個函數有返回值。
let html = id => html => {
return $$(id).map(dom => {
dom.innerHTML = html;
return html
});
};
複製代碼
分析一下代碼。
那麼這時候咱們想,既然咱們在執行的時候就知道,它會有影響,那我能不能在執行的時候,就把這個應該 給消除呢。
class Maybe{
constructor (value) {
this.value = value ;
}
map (fn) {
return this.value ? Maybe.of(fn(this.value)) : Maybe.of(null);
}
join ( ){
return this.value;
}
chain(fn) {
return this.map(fn).join();
}
}
複製代碼
咱們寫了一個chain函數。首先它調用了一下map方法,執行結束後,在去掉一層嵌套的函子
因此在執行的時候,咱們就能夠這樣去寫。
Maybe.of(str).map(toUpperCase).chain(html('text'))
複製代碼
這樣返回的函數就是隻有一層嵌套的函子啦。
學到這裏咱們已經把所有的函數式編程所涉及到概念都學習完啦。如今要是面試官拿這樣一道題問題,答案是什麼?是否是有點太簡單啦。
var Container = function(x) { this.__value = x; }
Container.of = x => new Container(x);
Container.prototype.map = function(f){
console.log(f)
return Container.of(f(this.__value))
}
Container.of(3).map(x=>x+1).map(x => 'Result is ' + x);
console.log(Container.of(3).map(x=>x+1).map(x => 'Result is ' + x))
複製代碼
但你會發現咱們並無具體糾結每個概念上,而是更多的體如今可實現的代碼上,而這些代碼你也並不陌生。
哈哈,那你可能會問,我是否是學了假的函數式編程,並無。由於我以爲函數式編程也是編程,最終都是要回歸到平常項目的實踐中。而應對不一樣難度的項目,所運用的知識固然也是不同的,就比如造船,小船有小船的造法,郵輪有油輪的造法,航母有航母的造法。你沒有 必要把所有的造船知識點,逐一學完纔開始動手。平常何況在工做中,你可能也並有真正的機會去造航母(好比寫框架)。與其把大量的時間都花在理解那些概念上,不如先動手造一艘小船踏實。因此本文中大量淡化了不須要去當即學習的概念。
如今,當你置身在函數式編程的那片海中,看見泛起的一葉葉扁舟,是否是再也不陌生了呢?
是否是在海角和天邊,還劃出一道美麗的曲線?
那麼接下來咱們會動手實踐一個Underscore.js 的庫。進一步深刻每一個細節去了解函數式編程。 學習更多的技巧。
最後本文是我學習函數式編程的筆記,寫的時候常常自言自語,偶爾還安慰本身。若是有錯的地方,歡迎你們批評指正。
文章最後總結的上面的答案是有的,不過如今還在我心中,等我有時間在寫啊 啊 啊。。。。