前端進擊的巨人(五):學會函數柯里化(curry)

前端進擊的巨人(五):學會函數柯里化(curry)

柯里化(Curring, 以邏輯學家Haskell Curry命名)html

寫在開頭

柯里化理解的基礎來源於咱們前幾篇文章構建的知識,若是還未能掌握閉包,建議回閱前文。前端

代碼例子會用到 apply/call ,通常用來實現對象冒充,例如字符串冒充數組對象,讓字符串擁有數組的方法。待對象講解篇會細分解析。在此先了解,二者功能相同,區別在於參數傳遞方式的不一樣, apply 參數以數組方式傳遞,call 多個參數則是逗號隔開。git

apply(context, [arguments]);
call(context, arg1, arg2, arg3, ....);

代碼例子中使用到了ES6語法,對ES6還不熟悉的話,可學習社區這篇文章:《30分鐘掌握ES6/ES2015核心內容(上)》程序員


函數柯里化

函數柯里化在JavaScript中實際上是高階函數的一種應用,上篇文章咱們簡略介紹了高階函數(能夠做爲參數傳遞,或做爲返回值)。github

理論知識太枯燥,來個生活小例子,"存款買房"(富二代繞道)。假設買房是咱們存錢的終極目標。那麼在買房前,存在卡里的錢(老婆本)就不能動。等到夠錢買房了,錢從銀行卡取出來,開始買買買。。。面試

函數柯里化就像咱們往卡里存錢,存夠了,才能執行買房操做,存不夠,接着存。編程

函數柯里化公式

先上幾個公式(左邊是普通函數,右邊就是轉化後柯里化函數支持的調用方式):segmentfault

// 公式類型一
fn(a,b,c,d) => fn(a)(b)(c)(d);
fn(a,b,c,d) => fn(a, b)(c)(d);
fn(a,b,c,d) => fn(a)(b,c,d);

// 公式類型二
fn(a,b,c,d) => fn(a)(b)(c)(d)();
fn(a,b,c,d) => fn(a);fn(b);fn(c);fn(d);fn();

兩種公式類型的區別 —— 函數觸發執行的機制不一樣:數組

  • 公式一當傳入參數等於函數參數數量時開始執行
  • 公式二當沒有參數傳入時(且參數數量知足)開始執行

經過公式,咱們先來理解這行代碼 fn(a)(b)(c)(d), 執行 fn(a) 時返回的是一個函數,而且支持傳參。什麼時候返回的是值而不是函數的觸發機制控制權在咱們手裏,咱們能夠爲函數制定不一樣的觸發機制。瀏覽器

普通的函數調用,一次性傳入參數就執行。而經過柯里化,它能夠幫咱們實現函數部分參數傳入執行(並未當即執行原始函數,錢沒存夠接着存),這就是函數柯里化的特色:"延遲執行和部分求值"

"函數柯里化:指封裝一個函數,接收原始函數做爲參數傳入,並返回一個可以接收並處理剩餘參數的函數"

函數柯里化的例子

// 等待咱們柯里化實現的方法add
function add(a, b, c, d) {
    return a + b + c + d;
};
// 最簡單地實現函數add的柯里化
// 有點low,有助於理解
function add(a, b, c, d) {
    return function(a) {
        return function(b) {
            return function(c) {
                return a + b + c + d;
            }
        }
    }
}

分析代碼知識點:

  1. 函數做爲返回值返回,閉包造成,外部環境可訪問函數內部做用域
  2. 子函數可訪問父函數的做用域,做用域由內而外的做用域鏈查找規則,做用域嵌套造成
  3. 在函數參數數量不知足時,返回一個函數(該函數可接收並處理剩餘參數)
  4. 當函數數量知足咱們的觸發機制(可自由制定),觸發原始函數執行

前幾篇文章的知識點此時恰好。可見基礎知識的重要性,高階的東西始終要靠小磚頭堆砌出來。

弄清原理後,接下來就是將代碼寫得更通用些(高大上些)。

// 公式類型一: 參數數量知足函數參數要求,觸發執行
// fn(a,b,c,d) => fn(a)(b)(c)(d);

const createCurry = (fn, ...args) => {
    let _args = args || [];
    let length = fn.length; // fn.length代碼函數參數數量

    return (...rest) => {
        let _allArgs = _args.slice(0);  
        // 深拷貝閉包共用對象_args,避免後續操做影響(引用類型)
        _allArgs.push(...rest);
        if (_allArgs.length < length) {
            // 參數數量不知足原始函數數量,返回curry函數
            return createCurry.call(this, fn, ..._allArgs);
        } else {
            // 參數數量知足原始函數數量,觸發執行
            return fn.apply(this, _allArgs);
        }
    }
}

const curryAdd = createCurry(add, 2);
let sum = curryAdd(3)(4)(5);    // 14

// ES5寫法
function createCurry() {
    var fn = arguments[0];
    var _args = [].slice.call(arguments, 1);
    var length = fn.length;
    
    return function() {
        var _allArgs = _args.slice(0);
        _allArgs = _allArgs.concat([].slice.call(arguments));
        if (_allArgs.length < length) {
            _allArgs.unshift(fn);
            return createCurry.apply(this, _allArgs);
        } else {
            return fn.apply(this, _allArgs);
        }
    }
}
// 公式類型二: 無參數傳入時而且參數數量已經知足函數要求
// fn(a, b, c, d) => fn(a)(b)(c)(d)();
// fn(a, b, c, d) => fn(a);fn(b);fn(c);fn(d);fn();

const createCurry = (fn, ...args) => {
    let all = args || [];
    let length = fn.length;

    return (...rest) => {
        let _allArgs = all.slice(0);
        _allArgs.push(...rest);
        if (rest.length > 0 || _allArgs.length < length) {
            // 調用時參數不爲空或存儲的參數不知足原始函數參數數量時,返回curry函數
            return createCurry.call(this, fn, ..._allArgs);
        } else {
            // 調用參數爲空(),且參數數量知足時,觸發執行
            return fn.apply(this, _allArgs);
        }
    }
}

const curryAdd = createCurry(2);
let sum = curryAdd(3)(4)(5)();  // 14

// ES5寫法
function createCurry() {
    var fn = arguments[0];
    var _args = [].slice.call(arguments, 1);
    var length = fn.length;
    
    return function() {
        var _allArgs = _args.slice(0);
        _allArgs = _allArgs.concat([].slice.call(arguments));
        if (arguments.length > 0 || _allArgs.length < length) {
            _allArgs.unshift(fn);
            return createCurry.apply(this, _allArgs);
        } else {
            return fn.apply(this, _allArgs);
        }
    }
}

爲實現公式中不一樣的兩種調用公式,兩個createCurry方法制定了兩種不一樣的觸發機制。記住一個點,函數觸發機制可根據需求自行制定。

偏函數與柯里化的區別

先上個公式看對比:

// 函數柯里化:參數數量完整
fn(a,b,c,d) => fn(a)(b)(c)(d);
fn(a,b,c,d) => fn(a,b)(c)(d);

// 偏函數:只執行了部分參數
fn(a,b,c,d) => fn(a);
fn(a,b,c,d) => fn(a, b);

"函數柯里化中,當你傳入部分參數時,返回的並非原始函數的執行結果,而是一個能夠繼續支持後續參數的函數。而偏函數的調用方式更像是普通函數的調用方式,只調用一次,它經過原始函數內部來實現不定參數的支持。"

若是已經看懂上述柯里化的代碼例子,那麼改寫支持偏函數的代碼,並不難。

// 公式:
// fn(a, b, c, d) => fn(a);
// fn(a, b, c, d) => fn(a,b,c);

const partialAdd = (a = 0, b = 0, c = 0, d = 0) => {
    return a + b + c +d;
}

partialAdd(6);      // 6
partialAdd(2, 3);   // 5

使用ES6函數參數默認值,爲沒有傳入參數,指定默認值爲0,支持無參數或不定參數傳入。

柯里化的特色:

  1. 參數複用(固定易變因素)
  2. 延遲執行
  3. 提早返回

柯里化的缺點

柯里化是犧牲了部分性能來實現的,可能帶來的性能損耗:

  1. 存取 arguments 對象要比存取命名參數要慢一些
  2. 老版本瀏覽器在 arguments.lengths 的實現至關慢(新版本瀏覽器忽略)
  3. fn.apply()fn.call() 要比直接調用 fn()
  4. 大量嵌套的做用域和閉包會帶來開銷,影響內存佔用和做用域鏈查找速度

柯里化的應用

  • 利用柯里化制定約束條件,管控觸發機制
  • 處理瀏覽器兼容(參數複用實現一次性判斷)
  • 函數節流防抖(延遲執行)
  • ES5前bind方法的實現

一個應用例子:瀏覽器事件綁定的兼容處理

// 普通事件綁定函數
var addEvent = function(ele, type, fn, isCapture) {
    if(window.addEventListener) {
        ele.addEventListener(type, fn, isCapture)
    } else if(window.attachEvent) {

        ele.attachEvent("on" + type, fn)
    }
}
// 弊端:每次調用addEvent都會進行判斷

// 柯里化事件綁定函數
var addEvent = (function() {
    if(window.addEventListener) {
        return function(ele, type, fn, isCapture) {
            ele.addEventListener(type, fn, isCapture)
        }
    } else if(window.attachEvent) {
        return function(ele, type, fn) {
             ele.attachEvent("on" + type, fn)
        }
    }
})()
// 優點:判斷只執行一次,經過閉包保留了父級做用域的判斷結果

秒懂反柯里化

先上公式,歷來沒有這麼喜歡寫公式,簡明易懂。

// 反柯里化公式:
curryFn(a)(b)(c)(d) = fn(a, b, c, d);
curryFn(a) = fn(a);

看完公式,是否是似曾相識,這不就是咱們平常敲碼的普通函數麼?沒錯的,函數柯里化就是把普通函數變成成一個複雜的函數,而反柯里化其就是柯里化的逆反,把複雜變得簡單。

函數柯里化是把支持多個參數的函數變成接收單一參數的函數,並返回一個函數能接收處理剩餘參數:fn(a,b,c,d) => fn(a)(b)(c)(d),而反柯里化就是把參數所有釋放出來:fn(a)(b)(c)(d) => fn(a,b,c,d)

// 反柯里化:最簡單的反柯里化(普通函數)
function add(a, b, c, d) {
    return a + b + c + d;
}

反思:爲什麼要使用柯里化

函數柯里化是函數編程中的一個重要的基礎,它爲咱們提供了一種編程的思惟方式。顯然,它讓咱們的函數處理變得複雜,代碼調用方式並不直觀,還加入了閉包,多層做用域嵌套,會有一些性能上的影響。

但在一些複雜的業務邏輯封裝中,函數柯里化可以爲咱們提供更好的應對方案,讓咱們的函數更具自由度和靈活性。

實際開發中,若是你的邏輯處理相對複雜,不妨換個思惟,用函數柯里化來實現,技能包不嫌多。
說到底,程序員就是解決問題的那羣人。


寫在結尾

本篇函數柯里化知識點的理解確實存在難度,暫時跳過這章也無妨,能夠先了解再深刻。耐得主寂寞的小夥伴回頭多啃幾遍,沒準春季面試就遇到了。


參考文檔:

本文首發Github,期待Star!
https://github.com/ZengLingYong/blog

做者:以樂之名 本文原創,有不當的地方歡迎指出。轉載請指明出處。

相關文章
相關標籤/搜索