函數柯里化

基礎概念

當一個函數有多個參數的時候,先傳遞一部分參數調用他(這部分參數以后永遠不變),然后返回一個新的函數接受剩余的參數,返回結果;
簡言之就是:多變量函數拆解為單變量的多個函數的依次調用;


可以干嘛呢?

可以利用它來實現對函數參數的緩存,降低函數粒度,把多元函數轉換成一元函數,實現函數的組合,產生更強大的功能


核心流程分析

就是利用閉包和遞歸調用,可以形成一個不銷毀的私有作用域,把預先處理的內容放到不銷毀的作用域里面,返回一個函數供以后調用;
舉個例子:

比如我們有一個判斷用戶年齡是否大于某個值的函數

// 普通的純函數
function checkAge (min, age) {
    return age >= min
}
// 普通調用
console.log(checkAge(18, 20))  //true
console.log(checkAge(18, 24))  //true
console.log(checkAge(60, 30))  //false

可能需要經常判斷用戶是否成年(大于18歲),為了減少代碼重復,所以改造如下

// 柯里化后的函數
function checkAge (min) {
    return function (age) {
        return age >= min
    }
}
const checkAge18 = checkAge(18)
const checkAge60 = checkAge(60)
console.log(checkAge18(20)) //true
console.log(checkAge18(24)) //true
console.log(checkAge60(30)) //false

以上就是一個針對checkAge函數的柯里化改造,他的自由度很低,因此需要封裝一個通用的柯里化函數;


實現思路

首先,我們通過調用lodash提供的柯里化函數(curry)來了解一下如何使用,并且分析一下實現思路

const _ = require('lodash')
function getSum (a, b, c) {
  return a + b + c
}
// 定義一個柯里化函數
const curried = _.curry(getSum)

// 如果輸入了全部的參數,則立即返回結果
console.log(curried(1, 2, 3)) // 6
//如果傳入了部分的參數,此時它會返回當前函數,并且等待接收getSum中的剩余參數
console.log(curried(1)(2, 3)) // 6
console.log(curried(1, 2)(3)) // 6</pre>

通過以上可以看出,柯里化函數的運行過程其實是一個參數的收集過程,將每一次傳入的參數收集起來,在最后統一處理

所以,實現思路:

  • 調用curry,傳遞一個函數,然后需要返回一個柯里化函數(curried)
  • 如果調用curried傳遞的參數和getSum參數個數相同,就立即執行并返回結果;如果調用curried傳遞的是部分參數,那么需要返回一個新函數,等待接受getSum其他參數

具體實現如下:

function curry(func) {
  return function curriedFn(...args) {
    // 若實參的個數小于形參的個數
    if (args.length < func.length) {
      return function () {
        // 等待傳遞的剩余參數
        // 第一部分參數在args里面,第二部分參數在arguments里面
        return curriedFn(...args.concat(...arguments));
      };
    }
    // 如果實參大于等于形參的個數,立即執行并返回結果
    // args是剩余參數
    return func(...args);
  };
}

注意:這里有個細節,就是要柯理化的函數不能有默認值,否則該函數的length屬性將失真;
將造成結果提前返回或者報錯

如下:

    • image

該技術的優缺點

上面費那么大勁封裝,到底有什么好處呢?

優點:

  • 參數復用;參考上面的checkAge函數,把18這個參數緩存起來,多個地方用到18的就可以直接調用

  • 將多元函數比變成一元函數,然后組合函數產生更強大功能

  • 延遲運行;像經常使用的bind,就是基于柯里化實現的;

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)

    return function() {
        return _this.apply(context, args)
    }
}

那缺點也顯然易見:

  • 使用了大量的閉包,內存得不到釋放,容易造成內存泄漏

對比傳統的函數調用,則不會產生閉包,使用完即可釋放

其實在大部分應用中,主要的性能瓶頸是在操作DOM節點上,這js的性能損耗基本是可以忽略不計的,只要注意閉包的內存釋放即可放心使用。


面試題

一)

// 實現一個add方法,使計算結果能夠滿足如下預期:
add(1,2,3) = 6;
add(1,2)(3) = 6;
add(1)(2)(3) = 6;

這個題目是想讓add函數執行后,返回一個能夠繼續執行的函數,最終計算出所有參數的和,重點在于每次接受的參數可以有一個,也可以有多個(add接受的參數個數固定);

答案如下:

function curry(func) {
  return function curriedFn(...args) {
    // 若實參的個數小于形參的個數
    if (args.length < func.length) {
      return function () {
        // 等待傳遞的剩余參數
        // 第一部分參數在args里面,第二部分參數在arguments里面
        return curriedFn(...args.concat(...arguments));
      };
    }
    // 如果實參大于等于形參的個數,立即執行并返回結果
    // args是剩余參數
    return func(...args);
  };
}
function add(a,b,c){
   return a+b+c;
}
const newFn =  curry(add)
newFn(1)(2)(3)  //6
newFn(1,2)(3)   //6
newFn(1,2,3)    //6

上述考題是參數固定:也就是add已知參數就是3個;那參數不固定的,如何解決呢?請看第2題

二)

// 實現一個add方法,使計算結果能夠滿足如下預期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

這個題目相較于第1題,它的難點在于add的參數不固定;所以要繼續優化;

先來看下面兩種解法

解法1.

// 柯里化寫法
function sum(...arr) {
  return arr.reduce((per, next) => {
    return per + next;
  }, 0);
}

function curry(fn) {
  let args = [];
  return function curried(...res) {
    if (res.length) {
      args = [...args, ...res];
      return curried;
    } else {
      return fn.apply(this, args);
    }
  };
}
let add = curry(sum);
console.log(add(1)(2)(3)()); //6

解法2.

//toString 寫法
function curry(a) {
  function curried(item) {
    a += item;
    return curried;
  }
  curried.toString = function () {
    return a;
  };

  return curried;
}
console.log(curry(1)(2)(3).toString()); //6

以上兩種方式雖然都能實現,但是解法1需要最后再調用一次,而解法2需要多調用一個轉換函數;
都有點勉強,不太符合考題調用方式;

那來看最后一種實現方式:

解法3.

function add(...args) {
  let final = [...args];
  setTimeout(() => {
    console.log(final.reduce((sum, cur) => sum + cur));
  }, 0);
  const inner = function (...args) {
    final = [...final, ...args];
    return inner;
  };
  return inner;
}
console.log(add(1)(2)(3)); //6

這個方法利用了異步編程,setTimeout中的內容延遲執行,算是個奇淫技巧,但終歸是符合了考題的調用方法;

具體使用哪種,還要看面試官想考什么?
如果是考柯里化知識點,那就選解法1
如果必須按照題目方式調用,那只能選擇解法3

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容