作者 | 秦粵
點擊查看視頻
大家好,我是秦粵。阿里巴巴自從2019年加入Ecma後,我們就在Ecma中積極參與推進,決定JavaScript語言發展未來的各種議題,阿里巴巴的議題Error Cause也已經成功的推進到Stage 2。今年在阿里內部由前端委員會牽頭也成立了阿里巴巴前端標準化小組,用組織架構保障阿里前端,持續在影響前端生態未來的標準規範上發力。
與此同時我們也特別關注到TC39成員中,不少成員都在關注和推進JavaScript中函數式編程的體驗。作為今年D2語言框架會場的出品人,因此我們邀請了RxJS的團隊負責人Ben,引入JavaScript函數式編程的議題。另外,今年Ecma組織的大事件ES2020已經發布,我們也將繼續邀請TC39的成員Ujjwal,講解其中有哪些特性值得我們前端同學關注以及這些議題的誕生過程。
函數式編程 in Javascript
函數式編程的思想從1930年提出來已經過去很久了,但大多數工程師使用的場景並不多。函數式編程在JavaScript中幾乎是天然就支持的,Brendan Eich參考的語言中包括了函數式編程語言Lisp,因此JavaScript有了函數是一等公民和閉包的概念。
我們大學學習的離散數學,線性代數等等課程,都很容易讓我們習慣面向指令編程。我們的代碼習慣性都是從上到下,從左到右,加上控制指令block的方式。
ES5之前,如果我們學習了面向原型,會在我們的代碼中採用prototype。
ES6和TS後,如果我們學習了面向對象,代碼中又會加上Class抽象。
但某種程度而言,這些代碼方式都是面向機器友好的。函數式編程是否可以帶來一些改變呢?
我自己使用函數式編程的感受是,在面對每個要開發的功能,就像在解數學題。首要是思考數據加工過程如何拆解,初期要不停查工具,看看有哪些函數方法可以給我使用。後期熟練後,會開始用函數式註釋,畢竟長時間不看自己原先的代碼,同樣會忘記。而且不會採用一逗(句號)到底的方式,分段一下。
函數式編程的基礎概念,我在此就不囉嗦了。它們是:範疇論,函數一等公民,純函數,組合,柯里化,等等。
感興趣的同學可以學習一下:https://mostly-adequate.gitbooks.io/mostly-adequate-guide/content/。
舉個例子
"show me the code",我們來看看具體例子:調用後臺API返回了一個數組,其中我們需要對商品的金額進行匯率運算,轉換成用戶本地的匯率顯示。
{
success: true,
data: {
'name': 'ProductsDetail',
'list': [
{
name:'T shirt',
price:'10.00',
unit:'$'
},
{
name:'T shirt with logo',
price:'15.00',
unit:'$'
},
{
name:'T shirt with brand logo',
price:'20.00',
unit:'$'
},
],
},
}
{
userCurrency: '¥',
userCurrencyRate: '6.5',
}
通常,我們會直接寫一個循環,或者用UnderscoreJS這樣的庫將price獲取到轉化成數字,然後乘以userCurrencyRate。
我們不妨看看函數式的做法,先從簡單的函數拆解來看。例如我們乘法函數,我們通常會這樣寫:
const multiply = function(x,y) {
return x * y;
}
multiply(3, 5);// 15
而當我們想複用乘法函數,例如固定乘3的函數。我們可以採用JS高級編程的柯里化Currying。
先複習一下柯里化Currying:一個函數將返回一個新的函數,直到接收所有參數。我們用Vanilla JS可以寫成:
const multiply = function(x) {
return function(y) {
return x * y;
}
}
const multiply3 = multiply(3);
multiply3(5);// 15
不過這樣還有一些不方便,Curry函數這些的寫法,我們想用multiply函數multiply(3,5)又不可以了。JS高級編程常見的面試題之一,就是autoCurry,顧名思義根據參數判斷函數如何返回。有興趣的同學可以自己挑戰一下實現autoCurry函數,這裡我們就直接採用RamdaJS的curry方法。
const R = require('ramda');
const multiply = function(x, y) {
return x * y;
};
const multiplyAutoCurry = R.curry(multiply);
multiplyAutoCurry(3)(5);// 15
multiplyAutoCurry(3, 5);// 15
有了Curry函數,對於數組我們可以使用map函數,將函數應用到每一項。
const R = require('ramda');
const multiply = function (x, y) {
return x * y;
};
const multiplyAutoCurry = R.curry(multiply);
const multiply3 = multiplyAutoCurry(3);
R.map(multiply3, [1, 2, 3]);// [3, 6, 9]
因此函數式編程的版本實現,我們就可以寫成:
const R = require('ramda');
const multiply = function (x, y) {
return x * y;
};
const multiplyAutoCurry = R.curry(multiply);
const multiplyCurrency = multiplyAutoCurry(6.5);
const parseIntCurry = R.curry(parseInt);
const parseInt10 = parseIntCurry(R.__, 10);
const currencyConvert = R.compose(multiplyCurrency, parseInt10);
const result = {
success: true,
data: {
name: 'ProductsDetail',
list: [
{
name: 'T shirt',
price: '10.00',
unit: '$'
},
{
name: 'T shirt with logo',
price: '15.00',
unit: '$'
},
{
name: 'T shirt with brand logo',
price: '20.00',
unit: '$'
},
],
},
};
const priceArray = R.pluck('price', R.prop('list', R.prop('data', result)));// ['10.00', '15.00', '20.00']
R.map(currencyConvert, priceArray);// [ 65, 97.5, 130 ]
由於API返回的結果中price是字符串,為了消除不確定性,我們需要先將price轉為數字。這裡我們可以用parsefloat,為了演示如何將build-in函數轉化成函數式,我選擇了parseInt,通過第二個參數傳入10解決低版本瀏覽器兼容性問題。
R.compose是將2個方法組合使用,執行順序是從右到左。然後我們只需要將我們組合好的工廠函數應用到獲取到的價格數組上就行了。
D2 語言框架專場
大家可以發現,我們整個運算過程只使用一次元數據result
,這也就是“無副作用”。另外我們將整個過程都轉化成了數學解題,而不是讓自己像計算機一樣思考。前端同學們熟悉的React庫也在使用函數式編程的思想,例如單向數據流,State要求immutable。
對於數組和數據流的操作,特別適合函數式操作。ReactiveX就是將數據看成是函數式流處理,因此抽象出Observable, Observer, Subscription等等概念。
import { fromEvent } from 'rxjs';
import { throttleTime, scan } from 'rxjs/operators';
fromEvent(document, 'click')
.pipe(
throttleTime(1000),
scan(count => count + 1, 0)
)
.subscribe(count => console.log(`Clicked ${count} times`));
今年D2的語言與框架專場邀請了RxJS:Reactive Extensions For JavaScript團隊負責人Ben Lesh,講解如何利用JS特性重構RxJS,讓代碼更小更快。
另外函數式在TC39中,也是大廠專家積極討論的對象,如何優化JS中寫函數式的體驗,相關議題有:
https://github.com/tc39/proposal-pipeline-operator
https://github.com/tc39/proposal-partial-application
https://github.com/tc39/proposal-pattern-matching
https://github.com/tc39/proposal-record-tuple
同時我們也邀請了Igalia的TC成員Ujjwal,為我們講解ES2020和ES2021都有哪些可以提升我們JS開發體驗的新特性,以及這些特性是如何從TC39中誕生的,我們如何決定影響JS語言的未來。
結語
函數式編程也不是銀彈,但在適用的場景,非常好用。大家無妨給自己的工具箱多裝備一個函數式編程的工具。我在今年的D2大會語言與框架專場等你。
參考文獻:
https://mostly-adequate.gitbooks.io/mostly-adequate-guide/content/
Hey Underscore, You're Doing It Wrong!:
https://www.youtube.com/watch?v=m3svKOdZijA
JavaScript函數式編程:
https://github.com/doodlewind/jshistory-cn/blob/master/part-1.md
https://mostly-adequate.gitbooks.io/mostly-adequate-guide/content/
關注「Alibaba F2E」
把握阿里巴巴前端新動向