函數(shù)式編程可以理解為以函數(shù)作為主要載體的編程方式,用函數(shù)去拆解夹孔、抽象一般的表達式
在函數(shù)式編程中,函數(shù)就是一個管道(pipe)析孽。這頭進去一個值搭伤,那頭就會出來一個新的值,沒有其他作用
與命令式相比袜瞬,這樣做的好處在哪怜俐?主要有以下幾點:
語義更加清晰
可復用性更高
可維護性更好
作用域局限,副作用少
compose
如果一個值要經(jīng)過多個函數(shù)邓尤,才能變成另外一個值佑菩,就可以把所有中間步驟合并成一個函數(shù),這叫做"函數(shù)的合成"(compose)
const compose = function (f, g) {
return function (x) {
return f(g(x));
};
}
上圖中裁赠,X和Y之間的變形關系是函數(shù)f殿漠,Y和Z之間的變形關系是函數(shù)g,那么X和Z之間的關系佩捞,就是g和f的合成函數(shù)g·f绞幌。
函數(shù)的合成還必須滿足結合律。
compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)
compose實現(xiàn)思路就是先把傳入的函數(shù)都緩存起來一忱,然后在傳入數(shù)據(jù)的時候莲蜘,再挨個的使用apply執(zhí)行函數(shù), 上一個函數(shù)的輸出數(shù)據(jù)帘营,作為下一個函數(shù)的輸入數(shù)據(jù)
compose遵循的是從右向左運行票渠,而不是由內(nèi)而外運行桩了。也就是說compose是從最后一個函數(shù)開始執(zhí)行
const compose = function() {
const args = arguments;
let start = args.length - 1;
return function() {
let i = start;
const result = args[start].apply(this, arguments);
while (i--) result = args[i].call(this, result);
return result;
};
};
函數(shù)就像數(shù)據(jù)的管道(pipe)扰藕。那么,函數(shù)合成就是將這些管道連了起來冗荸,讓數(shù)據(jù)一口氣從多個管道中穿過禀梳。組合讓我們的代碼簡單而富有可讀性杜窄。
curry
f(x)和g(x)合成為f(g(x)),有一個隱藏的前提算途,就是f和g都只能接受一個參數(shù)塞耕。如果可以接受多個參數(shù),比如f(x, y)和g(a, b, c)嘴瓤,函數(shù)合成就非常麻煩扫外。
這時就需要函數(shù)柯里化了莉钙。所謂"柯里化",就是把一個多參數(shù)的函數(shù)筛谚,轉化為單參數(shù)函數(shù)磁玉。
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
柯里化實現(xiàn)思路就是當傳入的參數(shù)個數(shù)沒有到達func函數(shù)要求的參數(shù)個數(shù)的時候一直返回柯里化函數(shù)。 只有參數(shù)滿足func函數(shù)的個數(shù)的時候才通過apply執(zhí)行func函數(shù)
//不考慮函數(shù)的個數(shù)刻获,簡單的實現(xiàn)
function curry(fn) {
// 獲取第一個參數(shù)以后的參數(shù)蜀涨,也就是除了fn以后的參數(shù)
const args = [].slice.call(arguments, 1);
return function() {
//將當前函數(shù)和后面的函數(shù)的參數(shù)加起來
var newArgs = args.concat([].slice.call(arguments));
//將所有函數(shù)參數(shù)加起來并傳入fn執(zhí)行
//不管this,目的就是執(zhí)行fn
return fn.apply(this, newArgs);
};
};
//考慮到參數(shù)個數(shù)
function curry(func , thisArg){
//將所有參數(shù)收到thisArg
if ( !Array.isArray(thisArg) ) {
thisArg = [];
}
return function(){
let args = Array.prototype.slice.call(arguments);
if ( (args.length+thisArg.length) < func.length ) {
return curry(func , thisArg.concat(args));
}
return func.apply(this , thisArg.concat(args));
};
}
說個題外話蝎毡,bind就是利用柯里化實現(xiàn)的厚柳,只不過兩者目的不一樣,柯里化是收集所有除參數(shù)函數(shù)的其他參數(shù)沐兵,然后執(zhí)行參數(shù)函數(shù)别垮,而bind是將第一個參數(shù)設為this,然后將所有其他參數(shù)放到參數(shù)函數(shù)里面執(zhí)行扎谎,柯里化不考慮this的問題碳想,而bind需要考慮this的問題,下面是bind的粗略實現(xiàn)
Function.prototype.bind = Function.prototype.bind ||
function(context){
var self = this
var args = Array.prototype.slice.call(arguments, 1)
return function(){
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
//綁定context毁靶,確定this
return self.apply(context,finalArgs);
}
}
function add(a, b) {
return a + b;
}
var addCurry = curry(add, 1, 2);
addCurry() // 3
//或者
var addCurry = curry(add, 1);
addCurry(2) // 3
//或者
var addCurry = curry(add);
addCurry(1, 2) // 3
有了柯里化以后胧奔,我們就能做到,所有函數(shù)只接受一個參數(shù)预吆。
pure function(轉載)
一個函數(shù)的返回結果只依賴于它的參數(shù)龙填,并且在執(zhí)行過程里面沒有副作用,我們就把這個函數(shù)叫做純函數(shù)拐叉。
函數(shù)的返回結果只依賴于它的參數(shù)
const a = 1
const foo = (b) => a + b
foo(2) // => 3
foo 函數(shù)不是一個純函數(shù)岩遗,因為它返回的結果依賴于外部變量 a,我們在不知道 a 的值的情況下凤瘦,并不能保證 foo(2) 的返回值是 3宿礁。雖然 foo 函數(shù)的代碼實現(xiàn)并沒有變化,傳入的參數(shù)也沒有變化蔬芥,但它的返回值卻是不可預料的梆靖,現(xiàn)在 foo(2) 是 3,可能過了一會就是 4 了坝茎,因為 a 可能發(fā)生了變化變成了 2涤姊。
const a = 1
const foo = (x, b) => x + b
foo(1, 2) // => 3
現(xiàn)在 foo 的返回結果只依賴于它的參數(shù) x 和 b,foo(1, 2) 永遠是 3嗤放。今天是 3,明天也是 3壁酬,在服務器跑是 3次酌,在客戶端跑也 3恨课,不管你外部發(fā)生了什么變化,foo(1, 2) 永遠是 3岳服。只要 foo 代碼不改變剂公,你傳入的參數(shù)是確定的,那么 foo(1, 2) 的值永遠是可預料的吊宋。
這就是純函數(shù)的第一個條件:一個函數(shù)的返回結果只依賴于它的參數(shù)纲辽。
函數(shù)執(zhí)行過程沒有副作用
一個函數(shù)執(zhí)行過程對產(chǎn)生了外部可觀察的變化那么就說這個函數(shù)是有副作用的。
我們修改一下 foo:
const a = 1
const foo = (obj, b) => {
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 3
counter.x // => 1
我們把原來的 x 換成了 obj璃搜,我現(xiàn)在可以往里面?zhèn)饕粋€對象進行計算拖吼,計算的過程里面并不會對傳入的對象進行修改,計算前后的 counter 不會發(fā)生任何變化这吻,計算前是 1吊档,計算后也是 1,它現(xiàn)在是純的唾糯。但是我再稍微修改一下它:
const a = 1
const foo = (obj, b) => {
obj.x = 2
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 4
counter.x // => 2
現(xiàn)在情況發(fā)生了變化怠硼,我在 foo 內(nèi)部加了一句 obj.x = 2,計算前 counter.x 是 1移怯,但是計算以后 counter.x 是 2香璃。foo 函數(shù)的執(zhí)行對外部的 counter 產(chǎn)生了影響,它產(chǎn)生了副作用舟误,因為它修改了外部傳進來的對象葡秒,現(xiàn)在它是不純的。
但是你在函數(shù)內(nèi)部構建的變量脐帝,然后進行數(shù)據(jù)的修改不是副作用:
const foo = (b) => {
const obj = { x: 1 }
obj.x = 2
return obj.x + b
}
雖然 foo 函數(shù)內(nèi)部修改了 obj同云,但是 obj 是內(nèi)部變量,外部程序根本觀察不到堵腹,修改 obj 并不會產(chǎn)生外部可觀察的變化炸站,這個函數(shù)是沒有副作用的,因此它是一個純函數(shù)疚顷。
除了修改外部的變量旱易,一個函數(shù)在執(zhí)行過程中還有很多方式產(chǎn)生外部可觀察的變化,比如說調(diào)用 DOM API 修改頁面腿堤,或者你發(fā)送了 Ajax 請求阀坏,還有調(diào)用 window.reload 刷新瀏覽器,甚至是 console.log 往控制臺打印數(shù)據(jù)也是副作用笆檀。
純函數(shù)很嚴格忌堂,也就是說你幾乎除了計算數(shù)據(jù)以外什么都不能干,計算的時候還不能依賴除了函數(shù)參數(shù)以外的數(shù)據(jù)酗洒。
為什么要煞費苦心地構建純函數(shù)士修?因為純函數(shù)非臣纤欤“靠譜”,執(zhí)行一個純函數(shù)你不用擔心它會干什么壞事棋嘲,它不會產(chǎn)生不可預料的行為酒唉,也不會對外部產(chǎn)生影響。不管何時何地沸移,你給它什么它就會乖乖地吐出什么痪伦。如果你的應用程序大多數(shù)函數(shù)都是由純函數(shù)組成,那么你的程序測試雹锣、調(diào)試起來會非常方便
參考鏈接: