JS專題之函式柯里化

南波發表於2019-03-04

前言

在電腦科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。

一、為什麼會有函式柯里化?

Currying 的重要意義在於可以把函式完全變成「接受一個引數;返回一個值」的固定形式,這樣對於討論和優化會更加方便。

將關注的重點聚焦到函式本身,而不因冗餘的資料引數分散注意力。

有這樣的說法,並非柯里化有什麼意義,而是,當函式可以作為函式的引數和返回值,成為函數語言程式設計語言後,就會不可避免地產生函式柯里化。

二、具體實現

先來一個簡單的 add 函式

function add(x, y) {
    return x + y;
}

add(2, 3);  // 5
複製程式碼

重要的概念多說一遍:函式柯里化就是接收多個引數的函式變換為接收一個函式,並返回接收餘下引數,最終能返回結果的技術 。

那麼,繼續:

function add(x) {
    return function(y) {
        return x + y;
    }
}

add(2)(3);  // 5
複製程式碼

所以,曾經的一個函式,因為閉包操作(返回函式並訪問了自由變數的行為),變成了多個接收一個引數的函式。

所以簡單來講:函式柯里化就是意圖將函式的引數變成一個。讓函式可以輸入一個值,就返回一個相對應的值,從而實現純函式化。

為什麼函數語言程式設計要求函式必須是純的,不能有副作用?因為它是一種數學運算,原始目的就是求值,不做其他事情,否則就無法滿足函式運演算法則了。在函數語言程式設計中,函式就是一個管道(pipe)。這頭進去一個值,那頭就會出來一個新的值,沒有其他作用。

所以良好的程式設計規範是儘可能讓函式塊做一個事情,實現可複用性,可維護性。

上面的例子中,如果有很多個引數怎麼辦,難道一層層巢狀?

我們繼續:

function plus(value) {
    "use strict";
    var add = function () {
        var args = [];
        var adder = function adder() {
            Array.prototype.push.apply(args,Array.prototype.slice.apply(arguments))
            return adder;
        }
        adder.toString = function () {
            return args.reduce(function(a, b) {
                return a + b;
            })
        }
        return adder;
    }
    return add()(value);
}

plus(2)(3)(5).toString();  // 10;
複製程式碼

上面的程式碼看起來不那麼優雅,如果是減法,我們就得又重新為減法寫這麼多的程式碼。像 lodash, underscore 這些工具庫,都提供了柯里化的工具函式。

我們一起來試著實現:

function curry(fn, args) {
    var length = fn.length;  // 函式引數的長度

    // 閉包儲存引數列表
    args = args || [];

    return function() {
        // 獲取引數列表。
        var _args = args.slice(0);
        
            Array.prototype.push.apply(_args, Array.prototype.slice.call(arguments))

        if (_args.length < length) {
        // 如果傳入的引數列表長度還沒有超過函式定義時的引數長度,就 push 新的引數到引數列表中儲存起來。
        
            // 自己呼叫自己,將儲存的引數傳遞到下一個柯里化函式。
            return curry.call(this, fn, _args);
        }
        else {
        // 如果傳入的引數列表長度已經超過函式定義時的引數長度,就執行。
            return fn.apply(this, _args);
        }
    }
}
複製程式碼

三、應用場景

函式柯里化的好處有幾個:

  1. 引數複用;
  2. 提前返回;
  3. 延遲計算/執行。

函式柯里化允許和鼓勵你分隔複雜功能變成更小更容易分析的部分。這些小的邏輯單元顯然是更容易理解和測試的,然後你的應用就會變成乾淨而整潔的組合,由一些小單元組成的組合。

文章開篇的 add 函式,假如,每次呼叫加法有一個初始值會怎樣?

var add = curry(function(a, b, c) {
    return a + b + c;
})

var addTen = add(10);

var addSix = add(6);

addTen(2)(3);  // 15;

addSix(7)(8);  // 21;
複製程式碼

以上程式碼就實現了引數複用,儲存固定引數的函式。

看一個經典的例子: 元素繫結事件監聽器:

var addEvent = function(el, type, fn, capture) {
    if (window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    } 
};
複製程式碼

以上程式碼是為了相容 IE 瀏覽器對 DOM 事件繫結做的函式封裝。

問題在於,每次對 DOM 元素進行事件繫結時,函式內部都會走一遍 if else。那麼用函式柯里化就能實現提前返回

var addEvent = (function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();
複製程式碼

總結

函式柯里化是“函式是一等公民”的程式語言環境形成的程式設計風格,利用了函式能作為引數一級返回值以及利用了閉包儲存變數的特點,是將多個引數的函式轉換為接收一個引數,最後返回結果的技術。

掘金專欄 JavaScript 系列文章

  1. JavaScript之變數及作用域
  2. JavaScript之宣告提升
  3. JavaScript之執行上下文
  4. JavaScript之變數物件
  5. JavaScript之原型與原型鏈
  6. JavaScript之作用域鏈
  7. JavaScript之閉包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值傳遞
  11. JavaScript之例題中徹底理解this
  12. JavaScript專題之模擬實現call和apply
  13. JavaScript專題之模擬實現bind
  14. JavaScript專題之模擬實現new
  15. JS專題之事件模型
  16. JS專題之事件迴圈
  17. JS專題之去抖函式
  18. JS專題之節流函式

歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。

JS專題之函式柯里化

相關文章