函数柯里化-浅尝
🤔 什么是柯里化?
在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數且返回結果的新函數的技術。 —— 百度百科
emmm…讀起來有點繞口,沒明白是什么意思?別急,我們來舉個簡單的🌰。
現在我們需要實現一個add函數, 函數執行結果返回三個參數之和。一般情況下,我們會這樣寫:
function add (x, y, z) {return x + y + z; }add(1, 2, 3);現在我們有一個新的需求,需要將add函數的調用方式轉換成add(1)(2)(3);
這時,我們稍加思索,也能很容易的想到寫出下面這種:
function add (x) {return function (y) {return function (x) {return x + y + z;}} }// es6 簡化寫法 // const add = x => y => z => (x+y+z);add(1)(2)(3);實際上,將add函數進行轉換的這個過程,我們就稱之為函數柯里化。
??需要注意的是,對于JavaScript語言的函數柯里化概念與 數學和計算機科學中的柯里化概念并無完全一致。通過文章開頭百度百科對柯里化的說明,我們能夠知道,在計算機科學中,柯里化函數只能接受單一參數,但是在JavaScript語言實際使用柯里化函數時,我們也可以傳遞一個或多個參數。
這也就是說柯里化之后的add函數,我們也可以這樣調用:
add(1, 2)(3); add(1)(2, 3); ...顯而易見,我們上面重寫的add函數并不能滿足這種形式的調用;
同時,假如現在我們需要計算add(1)(2)(3)(4)這樣的功能,它也是束手無策。
由此可見,我們上面重寫的add函數雖然也實現了函數柯里化,但是這并不是我們想要的效果。
🤔那我們怎么才能實現我們想要的柯里化呢?
紅寶書上面給了我們一種函數柯里化實現的方法:
function curry(fn){var args = Array.prototype.slice.call(arguments, 1);return function (){var innerArgs = Array.prototype.slice.call(arguments);var finalArgs = args.concat(innerArgs);return fn.apply(null, finalArgs);} }function add(num1, num2){return num1+num2; }var cur = curry(add,1); console.log(cur(2,3));在這種實現方式中,實現柯里化的函數curry的第一個參數為需要柯里化的函數,后面的參數可以傳入,也可以傳一部分,也可以不傳。在調用柯里化之后的函數的時候再傳入剩余的參數。
這種實現方式只能把參數列表通過兩次傳遞(進行柯里化的時候和調用柯里化函數的時候),顯然。這也并不是我們想要的效果。
在實現這個函數柯里化的方法之前,我們需要明確一下我們的需求:
1、每次傳遞的參數個數不固定;
2、調用的此時次數也不固定;
那么每次調用之后返回的就不能是一個值,而必須是一個函數,就如紅寶書給出方法一樣。那么現在面臨的問題就是,由于調用的次數不確定,如果判斷何時結束以及如何處理?
這里就需要用到一個細節知識:
對象(包括數組、對象、函數等)參與算數運算或者邏輯運算時,就會無參調用其toString或者valueOf方法得到一個原始值,然后參與運算。基于上面的思路,我們可以實現如下:
function add () {let args = [...arguments];function _add () {args = args.concat([...arguments]);return _add;}_add.toString = function () {return args.reduce((total, cur) => {return total + cur;})}return _add; }console.log(+add(1)(2)(3,4)(5)); // 這里需要使用‘+’運算符進行隱式類型轉換,才會執行toString方法,達到我們想要的目的;到這里,我們的一個累加功能的柯里化函數就實現了。
就我個人理解看來,柯里化是一種編程思想,也可以稱之為一種編程技巧。它運用閉包的機制,把一些參數存儲起來,提供給下級的上下文使用。
看到這里,大家肯定就會問了,費這一頓勁把它寫成柯里化函數到底有什么好處呢?
先上結論,再一一細說:
1、參數復用;
2、提前確認;
3、延遲運行;
參數復用:
假如,我們需要寫一個將一個數組每一個元素放大n倍的函數,一般情況下,我們會這樣寫:
function amplify = (arr, num) {return arr.map(item => item * num); }而我們在調用的時候:
const arr1 = [1,2,3]; const arr2 = [3,6,9];// 把 arr1放大3倍; amplify(arr1, 3); amplify(arr2, 3);// 如果我們有很大數組都需要放大3倍,那我們每次調用的時候都需要第二個參數傳一個3柯里化之后:
function amplify (num) {return function (arr) {return arr.map(item => item * num);} }// 使用es6語法更加簡單 // const amplify => num => arr => arr.map(item => item *num);// 如果需要將數組擴大3倍; const amplify3 = amplify(3); // 如果需要擴大5倍; const amplify5 = amplify(5);// 調用; amplify3(arr1); amplify3(arr2);這樣,我們就不用每次在調用的時候都需要將擴大的倍數傳進去了,達到了參數復用的效果。
提前確認
比如我們在定義一個函數的時候依賴某個全局參數或者環境變量等進行一些判斷,從而執行不同的操作時:
const fn = function () {if (a) {// 如果 a 存在的時候;// do something...} else {// 如果 a 不存在時,進行另外的處理;} }這樣寫雖然可以滿足我們的需求,但是每次調用fn時,都會對a進行判斷。
const fn = (function () {if (a) {return function () {// do something 存在a時進行的處理}} else {return function () {// do something 不存在a時進行的出來}} })();// 一種更加清晰的寫法; const fn = function (a) {if (a) {return function () {// do something 存在a時進行的處理}} else {return function () {// do something 不存在a時進行的出來}} }const func1 = fn(a); // 然后再調用func1通過這種寫法,代碼就能在自然執行時提前確認應該返回哪一個函數,不會在每次調用的時候再進行判斷。
延遲執行
事實上,我們上面的描述和例子中已經體現出來它延遲執行的特性.
通過閉包的方式將傳遞的參數保存在返回的函數的上下文中,但是返回的函數并不是立即執行,而是等待調用。
JavaScript中常用的bind方法就是通過柯里化機制實現的:
Function.prototype.bind = function (context) {const _this = this;const args = Array.prototype.slice .call(arguments, 1);return function () {return _this.apply(context, args);} }bind方法返回一個具有固定的this指向的新的函數。
通用柯里化工具函數實現:
function curring (fn) {return function curried (...args) {if(args.length >= fn.length) {// 如果傳入的參數個數比函數實際的參數個數多,直接執行并返回結果;return fn.apply(this, args)} else {// 如果參數個數還不夠,則繼續收集參數;return function(...rest) {return curried.apply(this, [...args, ...rest]);}}} }我們常用的工具庫lodash也實現了curry函數,并且支持placeholders,通過占位符來改變參數傳入的順序。
var abc = function(a, b, c) {return [a, b, c]; };var curried = _.curry(abc);// Curried with placeholders. curried(1)(_, 3)(2); // => [1, 2, 3]組合函數
組合函數也是函數式編程中的一個重要概念,將執行過程的每一步放到一個功能專一的函數中,在需要使用的地方直接調用函數。函數式編程側重于關注函數的計算結果,而不用關注函數的實現。同樣的,我們通過一個🌰來介紹組合函數以及它的作用:
現在,我們需要實現這樣一個函數:給定一個數字數組,可能包含0、undefinedy以及null。然后將給定的數組進行去零、篩除大于10的元素,在將數組每項乘以3,然后輸出結果數組。
我們先按照正常的寫法來實現一下:
// 對一個數組 去零 篩選 再乘3// 去零函數const filterBoolean = arr => arr.filter(Boolean);// 篩除掉大于10的元素, 這里我們可以就可以小用一下上面講過的函數柯里化思想const filterSmaller = num => arr => arr.filter(item => item <= num);const filterSmaller10 = filterBigger(10);// 將數組的每項乘以3const multiply = num => arr => arr.map(item => item * num);const multiply3 = multiply(3);const arr = [null, 2, 0, 8, 12, 7, 3];// 最后計算結果const resArr = multiply3( filterSmaller10( filterBoolean(arr) ) )可以看出,我們雖然實現了功能,但是在最后計算的時候,對多個函數進行了嵌套,但是可讀性比較差,代碼并不優雅。
正是基于這種情況,我們可以構建一個組合函數,函數可以接受任意函數為參數,每個參數函數只能接受一個參數,最后返回一個鋪排后的函數,像下面這樣:
const resFn = compose(a, b, c) resFn(arr) // c(b(a(arr)));基于這種思想我們可以實現以下的代碼:
function compose (...funcs) {return functiion(origin) {rest.reduce((total, item) => {return item(total);}, origin)} }// 簡化寫法 // const compose = (...rest) => startNum => rest.reduce((total, item) => item(total), startNum);// 調用; const modifyArr = compose(filterBoolean, filterSmaller10, multiply3); const resArr = modifyArr(arr);然后再對上面的代碼稍加優化:
function compose (...funcs) {return (x) => {let len = funcs.length;// 如果沒有處理函數,直接返回原數據if(len === 0) return x;// 如果只有一個處理函數,直接執行并返回結果if(len === 1) return funcs[0](x);reutrn funcs.reduce((total, func) => {return func(total);}, x)} }const resArr = compose(filterBoolean, filterSmaller10, multiply3)(arr)組合函數的思想在前端框和庫中有著廣泛的使用,比如在redux中:
redux源碼:redux/src/compose.ts
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-wo6g444o-1668999769669)(https://flowus.cn/preview/79508fd8-93f7-4d8f-a4b8-2e1cee57e3d7)]
🚩到這里,本篇的內容就講完了,函數柯里化和組合函數的思想在函數式編程中都非常重要,靈活適當的使用在我們日常的代碼開發可以使我們的代碼簡潔優雅,增加代碼的可讀性。
總結
- 上一篇: /u 反斜杠u 编码总结
- 下一篇: hive的调优操作