編程范式
編程范式是:解決編程中的問(wèn)題的過(guò)程中使用到的一種模式,體現(xiàn)在思考問(wèn)題的方式和代碼風(fēng)格上。這點(diǎn)很像語(yǔ)言吗垮,語(yǔ)言本身會(huì)體現(xiàn)出不同國(guó)家的人的思考方式和行為模式。
常見的編程范式有下面幾種:
- 命令式編程
- 面向?qū)ο缶幊?/li>
- 函數(shù)式編程
除了這三個(gè)之外凹髓,我們還會(huì)接觸到其他的編程范式烁登,如:聲明式。
編程范式之間不是互斥關(guān)系蔚舀,而是可以結(jié)合在一起使用的饵沧。我們往往需要結(jié)合各種編程范式來(lái)完成一個(gè)程序功能。
在學(xué)習(xí)寫代碼的過(guò)程中赌躺,我們一般先接觸命令式編程狼牺,然后學(xué)習(xí)面向?qū)ο缶幊蹋嫦驅(qū)ο缶幊炭梢宰屛覀兒芊奖愕靥幚砀鼜?fù)雜的問(wèn)題礼患。這篇文章里是钥,我們會(huì)介紹函數(shù)式編程。
不同的編程范式有不同的代碼表現(xiàn)
比如從來(lái)沒(méi)有坐過(guò)電梯的人缅叠,第一次坐電梯悄泥,電梯在 10 樓,人在 1 樓肤粱,他會(huì)按下弹囚,讓電梯下來(lái)。按錯(cuò)按鈕是因?yàn)樗昧似硎拐Z(yǔ)领曼,而不是把自己的想法提交出去鸥鹉。
相似地,你寫的代碼就像電梯的按鈕界面悯森,是讓自己或者他人閱讀的宋舷。只有達(dá)成了相同的共識(shí)才能更好地理解绪撵。通過(guò)這次文章可以讓大家更好地理解函數(shù)式編程瓢姻。
下面是幾種編程范式的代碼片段:
const app = 'github';
const greeting = 'Hi, this is ';
console.log(greeting + app);
這是命令式編程,通過(guò)調(diào)用 const
和 console.log
進(jìn)行賦值和輸出音诈。
const Program = function() {
this.app = 'github';
this.greeting = function() {
console.log('Hi, this is ' + this.app);
};
};
const program = new Program();
program.greeting();
這是面向?qū)ο缶幊袒眉睿覀儼颜麄€(gè)程序抽象成現(xiàn)實(shí)生活中的一個(gè)對(duì)象绎狭,這個(gè)對(duì)象會(huì)包含屬性和方法。通過(guò)類的概念褥傍,我們有了生成對(duì)象的一個(gè)工廠儡嘶。使用 new
關(guān)鍵字創(chuàng)建一個(gè)對(duì)象,最后調(diào)用對(duì)象的方法恍风,也能完成剛才我們用命令式編程完成的程序功能蹦狂。
const greet = function(app) {
return 'Hi, this is ' + app;
};
console.log(greet('github'));
這是簡(jiǎn)單的函數(shù)式編程,通過(guò)函數(shù)的調(diào)用完成程序的功能朋贬。但是一般情況下的函數(shù)式編程會(huì)更復(fù)雜一些凯楔,會(huì)包含函數(shù)的組合。
不同的編程范式適用的場(chǎng)景不同
- 命令式編程:流程順序
- 面向?qū)ο缶幊蹋何矬w
- 函數(shù)式語(yǔ)言:數(shù)據(jù)處理
我們往往會(huì)在不同場(chǎng)景下使用不同的編程范式锦募,通過(guò)編程范式的結(jié)合來(lái)實(shí)現(xiàn)一個(gè)程序摆屯。
我們通過(guò)命令式編程去讓程序按步驟地執(zhí)行操作。
面向?qū)ο缶幊虅t是把程序抽象成和現(xiàn)實(shí)生活中的物體一樣的對(duì)象糠亩,對(duì)象上有屬性和方法虐骑,通過(guò)對(duì)象之間的修改屬性和調(diào)用方法來(lái)完成程序設(shè)計(jì)。
而函數(shù)式編程則適用于數(shù)據(jù)運(yùn)算和處理赎线。
再仔細(xì)看下之前的代碼廷没,我們就會(huì)發(fā)現(xiàn)這些編程范式往往是要結(jié)合起來(lái)使用的。
const app = 'github';
const greeting = 'Hi, this is ';
console.log(greeting + app);
這個(gè)例子里面垂寥,除了命令式之外腕柜,我們還可以把前兩句語(yǔ)句賦值解讀成聲明式編程。
const Program = function(app) {
this.app = app;
this.greeting = function() {
console.log('Hi, this is ' + this.app);
};
};
const program = new Program('github');
program.greeting();
這里例子里面矫废,我們看到在類的 greeting
方法里面也用到了命令式的 console.log
盏缤。在最后的執(zhí)行過(guò)程中的 program.greeing()
也是命令式的。
const greet = function(app) {
return 'Hi, this is ' + app;
};
console.log(greet('github'));
函數(shù)式編程
使用函數(shù)式編程可以大大提高代碼的可讀性蓖扑。
函數(shù)式編程的學(xué)習(xí)曲線
你編寫的每一行代碼之后都要有人來(lái)維護(hù)唉铜,這個(gè)人可能是你的團(tuán)隊(duì)成員,也可能是未來(lái)的你律杠。如果代碼寫的太過(guò)復(fù)雜潭流,那么無(wú)論誰(shuí)來(lái)維護(hù)都會(huì)對(duì)你炫技式的故作聰明的做法倍感壓力。
對(duì)于復(fù)雜計(jì)算的場(chǎng)景下柜去,使用函數(shù)式編程相比于命令式編程有更好的可讀性灰嫉。
從命令式的編程到函數(shù)式編程轉(zhuǎn)換的道路上,可讀性會(huì)變低嗓奢,但是一旦度過(guò)了一個(gè)坎讼撒,也就是在你大量使用函數(shù)式編程時(shí),可讀性便會(huì)大大提升「校可是我們往往會(huì)被這個(gè)坎阻撓钳幅,在發(fā)現(xiàn)可讀性下降后放棄學(xué)習(xí)函數(shù)式編程。
因此除了學(xué)習(xí)函數(shù)式編程本身的知識(shí)之外炎滞,我們還需要明白學(xué)習(xí)可能經(jīng)歷的過(guò)程和最終的結(jié)果敢艰。
函數(shù)式編程定義
函數(shù)是第一公民。
JavaScript 是一門在設(shè)計(jì)之處就完全支持函數(shù)式編程的語(yǔ)言册赛,在 JavaScript 里面钠导,函數(shù)可以用 function
聲明,作為全局變量森瘪,也就是這里說(shuō)的“第一公民”辈双。我們不再使用 var
、const
或者 let
等關(guān)鍵字聲明函數(shù)柜砾。我們也會(huì)大大減少變量的聲明湃望,通過(guò)函數(shù)的形參來(lái)替代變量的聲明。
函數(shù)式編程大量通過(guò)函數(shù)的組合進(jìn)行邏輯處理痰驱,因此我們?cè)诤竺鏁?huì)看到很多輔助函數(shù)证芭。通過(guò)這些輔助函數(shù),我們可以更方便得修改和組合函數(shù)担映。
什么是函數(shù)废士?
一個(gè)函數(shù)就是包含輸入和輸出的方程。數(shù)據(jù)流方向是從輸入到輸出蝇完。
在數(shù)學(xué)里面我們學(xué)到的函數(shù)是這樣的:
y = f(x)
在 JavaScript 里面官硝,函數(shù)是這樣表示的:
function(x) { return y; }
代碼中的函數(shù)和數(shù)學(xué)意義上的函數(shù)概念是一樣的。
函數(shù)和程序的區(qū)別
- 程序是任意功能的合集短蜕,可以沒(méi)有輸入值氢架,可以沒(méi)有輸出值。
- 函數(shù)必須有輸入值和輸出值朋魔。
函數(shù)適合的場(chǎng)景
函數(shù)適合:數(shù)學(xué)運(yùn)算岖研。不適合:與真實(shí)世界互動(dòng)。
實(shí)際的編程需要修改硬盤等警检。如果不改變東西孙援,等于什么都沒(méi)做。也就沒(méi)辦法完成任務(wù)了扇雕。
JavaScript 和函數(shù)式編程
JavaScript 支持函數(shù)式編程拓售。使用 JavaScript 進(jìn)行函數(shù)式編程時(shí),我們要使用 JavaScript 的子集镶奉。不使用 for 循環(huán), Math.random, Date, 不修改數(shù)據(jù)铃拇,來(lái)避免副作用,做到函數(shù)式編程浪谴。
下面,面向 JavaScript 開發(fā)者莹菱,介紹在 JavaScript 函數(shù)式編程中用到的一些概念移国。
高階函數(shù)
高階函數(shù)是由一個(gè)或多個(gè)函數(shù)作為輸入的函數(shù)吱瘩,或者是輸出一個(gè)函數(shù)的函數(shù)。
[1, 2, 3].map(function(item, index, array) {
return item * 2;
});
[1, 2, 3].reduce(function(accumulator, currentValue, currentIndex, array) {
return accumulator + currentValue;
}, 0);
map
和 reduce
是高階函數(shù)迹缀,它接收一個(gè)函數(shù)作為參數(shù)使碾。
純函數(shù)
純函數(shù)有兩個(gè)特點(diǎn):
- 純函數(shù)是冪等的
- 純函數(shù)沒(méi)有副作用
純函數(shù)是可靠的,可預(yù)測(cè)結(jié)果祝懂。帶來(lái)了可讀性和可維護(hù)性票摇。
冪等
冪等是指函數(shù)任意多次執(zhí)行所產(chǎn)生的影響和一次執(zhí)行的影響相同。函數(shù)的輸入和輸出都需要冪等砚蓬。
function add(a, b) {
return a + b;
}
上面的函數(shù)是冪等的矢门。
function add(a) {
return a + Math.random();
}
上面使用了隨機(jī)數(shù),每次執(zhí)行得到的結(jié)果不同灰蛙,所以這個(gè)函數(shù)不冪等祟剔。
var a = 1;
function add(b) {
return a + b;
}
上面使用到函數(shù)外部的數(shù)據(jù),當(dāng)外部數(shù)據(jù)變化時(shí)摩梧,函數(shù)執(zhí)行的結(jié)果不再相同物延,所以這個(gè)函數(shù)不冪等。
var c = 1;
function add(a, b) {
c++;
return a + b;
}
上面的函數(shù)修改了函數(shù)外部的數(shù)據(jù)仅父,所以也不冪等叛薯。
副作用
副作用是當(dāng)調(diào)用函數(shù)時(shí),除了返回函數(shù)值之外笙纤,還對(duì)主調(diào)用函數(shù)產(chǎn)生附加的影響耗溜。
最常見的副作用是 I/O(輸入/輸出)。對(duì)于前端來(lái)說(shuō)省容,用戶事件(鼠標(biāo)强霎、鍵盤)是 JS 編程者在瀏覽器中使用的典型的輸入,而輸出的則是 DOM蓉冈。如果你使用 Node.js 比較多城舞,你更有可能接觸到和輸出到文件系統(tǒng)、網(wǎng)絡(luò)系統(tǒng)和/或者 stdin / stdout(標(biāo)準(zhǔn)輸入流/標(biāo)準(zhǔn)輸出流)的輸入和輸出寞酿。
純函數(shù)
一個(gè)純函數(shù)需要滿足下面兩個(gè)條件:
- 純函數(shù)是冪等的
- 純函數(shù)沒(méi)有副作用
不可變數(shù)據(jù)
不可變數(shù)據(jù)是指保持一個(gè)對(duì)象狀態(tài)不變家夺。
值的不可變性并不是不改變值。它是指在程序狀態(tài)改變時(shí)伐弹,不直接修改當(dāng)前數(shù)據(jù)拉馋,而是創(chuàng)建并追蹤一個(gè)新數(shù)據(jù)。這使得我們?cè)谧x代碼時(shí)更有信心,因?yàn)槲覀兿拗屏藸顟B(tài)改變的場(chǎng)景煌茴,狀態(tài)不會(huì)在意料之外或不易觀察的地方發(fā)生改變随闺。
在函數(shù)式和非函數(shù)式編程中,不可變數(shù)據(jù)對(duì)我們都有幫助蔓腐。
使用不可變數(shù)據(jù)的準(zhǔn)則
- 使用
const
矩乐,不使用let
- 不使用
splice
、pop
回论、push
散罕、shift
、unshift
傀蓉、reverse
以及fill
修改數(shù)組 - 不修改對(duì)象屬性或方法
使用不可變數(shù)據(jù)的弊端
不可變數(shù)據(jù)有更多內(nèi)存開銷欧漱。
修改數(shù)據(jù)的情況下,直接替換了變量的值葬燎,內(nèi)存開銷不變误甚。
使用不可變數(shù)據(jù)后,我們復(fù)制了一個(gè)對(duì)象谱净,內(nèi)存開銷翻倍窑邦。
使用 immutableJS 等輔助庫(kù)后,可以更好地利用之前的數(shù)據(jù)岳遥,優(yōu)化了內(nèi)存開銷奕翔。
閉包 vs 對(duì)象
閉包和對(duì)象是一樣?xùn)|西的兩種表達(dá)方式。一個(gè)沒(méi)有閉包的編程語(yǔ)言可以用對(duì)象來(lái)模擬閉包浩蓉;一個(gè)沒(méi)有對(duì)象的編程語(yǔ)言可以用閉包來(lái)模擬對(duì)象派继。兩者都可以用來(lái)維護(hù)數(shù)據(jù)。
var obj = {
one: 1,
two: 2
};
function run() {
return this.one + this.two;
}
var three = run.bind(obj);
three(); // => 3
function getRun() {
var one = 1;
var two = 2;
return function run(){
return one + two;
};
}
var three = getRun();
three(); // => 3
上面兩種方式都可以用來(lái)完成程序功能捻艳,對(duì)象和函數(shù)之間可以轉(zhuǎn)換驾窟。
常見的輔助函數(shù)
unary
reverseArgs
curry
uncurry
compose
pipe
asyncPipe
unary
,reverseArgs
认轨,curry
和 uncurry
是用來(lái)進(jìn)行參數(shù)操作的绅络。compose
,pipe
和 asyncPipe
是用來(lái)進(jìn)行函數(shù)組合的嘁字。
unary
unary
是用來(lái)限制某個(gè)函數(shù)只接收一個(gè)參數(shù)的恩急。常見的使用場(chǎng)景是處理 parseInt
函數(shù):
['1', '2', '3'].map(parseInt);
// => [1, NaN, NaN]
['1', '2', '3'].map(unary(parseInt));
// => [1, 2, 3]
unary
的實(shí)現(xiàn)方式可以是:
const unary = (fn) => {
return (arg) => {
return fn(arg);
};
};
reverseArgs
reverseArgs
是用來(lái)講函數(shù)參數(shù)反轉(zhuǎn)的,實(shí)現(xiàn)方式如下:
const reverseArgs = (fn) => {
return (...args) => {
return fn(...args.reverse());
};
};
curry
curry
是用來(lái)把函數(shù)執(zhí)行滯后的纪蜒,讓我們可以逐步把參數(shù)傳入這個(gè)函數(shù)衷恭,當(dāng)參數(shù)完整之后,目標(biāo)函數(shù)才會(huì)執(zhí)行纯续。常見的用法如下:
function add(a, b) {
return a + b;
}
function add10(a) {
return add(10, a);
}
add10(1); // => 11
通過(guò) curry
函數(shù)随珠,可以把上面的代碼優(yōu)化一下:
function add(a, b) {
return a + b;
}
const curriedAdd = curry(add);
const add10 = curriedAdd(10);
add10(1); // => 11
curry
的實(shí)現(xiàn)思路如下:
把 args
灭袁,保存起來(lái),每個(gè) curried
函數(shù)接受一個(gè)參數(shù)窗看,將參數(shù)拼在之前的參數(shù)后面茸歧。
const curry = (fn) => {
const curried = (curArg) => {
const args = [...prevArgs, curArg];
return curried;
};
return curried;
};
修改成用閉包保存參數(shù)。
const curry = (fn) => {
return (curArg) => {
const args = [...prevArgs, curArg];
return nextCurried(...args);
};
};
遞歸調(diào)用 nextCurried
显沈,第一次柯里化的函數(shù)不傳入?yún)?shù)软瞎。
const curry = (fn) => {
const nextCurried = (...prevArgs) => {
return (curArg) => {
const args = [...prevArgs, curArg];
return nextCurried(...args);
};
};
return nextCurried();
};
最后補(bǔ)全 arity
參數(shù),來(lái)定義目標(biāo)函數(shù)的參數(shù)數(shù)量构罗。這樣铜涉,我們可以定義柯里化后的參數(shù)數(shù)量智玻,如果傳入的參數(shù)數(shù)量到了函數(shù)需要的數(shù)量遂唧,則直接執(zhí)行函數(shù),并傳入所有的參數(shù)吊奢。
const curry = (fn, arity = fn.length) => {
const nextCurried = (...prevArgs) => {
return (curArgs) => {
const args = [...prevArgs, curArgs];
if (args.length >= arity) {
return fn(...args);
}
return nextCurried(...args);
};
};
return nextCurried();
};
或者我們可以實(shí)現(xiàn)一個(gè)支持傳入多個(gè)參數(shù)的柯里化函數(shù):
const curry = (fn, arity = fn.length) => {
const nextCurried = (...prevArgs) => {
return (...curArgs) => {
const args = [...prevArgs, ...curArgs];
if (args.length >= arity) {
return fn(...args);
}
return nextCurried(...args);
};
};
return nextCurried();
};
compose
compose
用來(lái)串聯(lián)執(zhí)行函數(shù)盖彭,執(zhí)行順序是從后向前的。與之對(duì)應(yīng)的是 pipe
函數(shù)页滚,同樣是串聯(lián)執(zhí)行函數(shù)召边,但是執(zhí)行順序是從前向后的。
compose
的用法:
function add10(value) {
return value + 10;
}
function multiple10(value) {
return value * 10;
}
const add10AndMultiple10 = compose(multiple10, add10);
add10AndMultiple10(1); // => 110
compose
的實(shí)現(xiàn):
const compose = (...fns) => {
return fns.reduce((a, b) => {
return (...args) => {
return a(b(...args));
};
});
};
或者通過(guò) reduceRight
簡(jiǎn)單地從右邊向左執(zhí)行裹驰,這是更好理解的一種實(shí)現(xiàn)隧熙,但是有參數(shù)個(gè)數(shù)的限制。
const compose = (...fns) => {
return (input) => {
return fns.reduceRight((value, fn) => {
return fn(value);
}, input);
};
};
pipe
pipe
也是用來(lái)組合函數(shù)的幻林,串聯(lián)執(zhí)行的順序是從前向后贞盯,與 compose
相反。pipe
的實(shí)現(xiàn)可以是:
const pipe = (...fns) => {
return fns.reduceRight((a, b) => {
return (...args) => {
return a(b(...args));
}
});
};
pipe
的用法如下:
const addA = (value) => {
return value + 'A';
};
const addB = (value) => {
return value + 'B';
};
pipe(addA, addB)('1') // => 1AB
asyncPipe
對(duì)于異步函數(shù)來(lái)說(shuō)沪饺,如果我們要串聯(lián)執(zhí)行躏敢,可以使用 asyncPipe
。實(shí)現(xiàn)可以是:
const asyncPipe = (...fns) => {
return fns.reduceRight((next, fn) => {
return (...args) => {
fn(...args, next);
};
}, () => {});
};
用法是:
const addA = (value, next) => {
next(value + 'A', 'a');
};
const addB = (value, anotherValue, next) => {
console.log(anotherValue); // => a
next(value + 'B');
};
const consoleLog = (value, next) => {
console.log(value);
};
asyncPipe(addA, addB, consoleLog)('1'); // => 1AB
函數(shù)式編程在數(shù)據(jù)結(jié)構(gòu)上的運(yùn)用
實(shí)現(xiàn)鏈表
主要思路是用函數(shù)閉包代替對(duì)象保存數(shù)據(jù)整葡。
const createNode = (value, next) => {
return (x) => {
if (x) {
return value;
}
return next;
};
};
const getValue = (node) => {
return node(true);
};
const getNext = (node) => {
return node(false);
};
const append = (next, value) => {
if (next === null) {
return createNode(value, null);
}
return createNode(getValue(next), append(getNext(next), value));
};
const reverse = (linkedList) => {
if (linkedList === null) {
return null;
}
return append(reverse(getNext(linkedList)), getValue(linkedList));
};
const linkedList1 = createNode(1, createNode(2, createNode(3, null)));
const linkedList2 = reverse(linkedList1);
getValue(linkedList1); // => 1
getValue(getNext(linkedList1)); // => 2
getValue(getNext(getNext(linkedList1))); // => 3
getValue(linkedList2); // => 3
getValue(getNext(linkedList2)); // => 2
getValue(getNext(getNext(linkedList2))); // => 1
同樣可以用函數(shù)式編程實(shí)現(xiàn)二叉樹件余。
總結(jié)
希望大家能夠通過(guò)學(xué)習(xí)函數(shù)式編程范式,加深對(duì)軟件研發(fā)的理解遭居,開拓視野啼器,找到更多組織代碼方式。
函數(shù)式編程能夠更好地組織業(yè)務(wù)代碼中的數(shù)據(jù)處理俱萍,更多地復(fù)用了函數(shù)端壳,減少了中間變量。
但是函數(shù)式編程也有缺點(diǎn)鼠次,它增加了學(xué)習(xí)成本更哄,需要大家理解高階函數(shù)芋齿。
參考資料
- Anjana Vakil: Learning Functional Programming with JavaScript - JSUnconf 2016
- Anjana Vakil: Immutable data structures for functional JS - JSConf EU 2017
- JavaScript 輕量級(jí)函數(shù)式編程
- Douglas Crockford: Monads and Gonads (YUIConf Evening Keynote)
- A Brief Intro to Functional Programming
- JavaScript 中的函數(shù)式編程