首頁 >web前端 >js教程 >詳解JavaScript中的函數柯理化

詳解JavaScript中的函數柯理化

青灯夜游
青灯夜游轉載
2020-12-18 17:53:246559瀏覽

詳解JavaScript中的函數柯理化

相關推薦:《javascript影片教學

#最近在社群閱讀技術部落格的時候偶然間看到了函數柯里化幾個字,還有要求手寫js函數柯里化,心想是柯里化是什麼高階的東西?沒聽過啊?

就帶著問題出發,專門去學習了一下,做了一些整理。

什麼是函數柯里化?

什麼是函數柯里化?先看看維基百科如何解釋:

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

這個技術由克里斯托弗·斯特雷奇以邏輯學家哈斯凱爾·加里命名的,儘管它是Moses Schönfinkel和戈特洛布·弗雷格發明的。

在直覺上,柯里化聲稱「如果你固定某些參數,你將得到接受餘下參數的一個函數」。所以對於有兩個變數的函y^x,如果固定了y=2,則得到有一個變數的函數2^x。

Currying的概念其實並不複雜,用簡單易懂的話來說:只傳遞給函數一部分參數來呼叫它,讓它回傳一個函數去處理剩下的參數。

如果文字解釋還是有一點抽象,我們就拿add函數,來做一個簡單的函數柯里化的實作。

// 普通的add函数
function add(x, y) {
    return x + y
}

// add函数柯里化后
var curryingAdd = function(x) {
  return function(y) {
    return x + y;
  };
};

// 函数复用
var increment = curryingAdd(1);
var addTen = curryingAdd(10);

increment(2);
// 3
addTen(2);
// 12

其實就是把add函數的x,y兩個參數變成了先用一個函數接收x然後回傳一個函數去處理y參數。現在思路應該就比較清楚了,就是只傳遞給函數一部分參數來呼叫它,讓它回傳一個函數去處理剩下的參數。

為什麼要函數柯里化?

看完上面的關於add函數的柯里化,問題來了,費這麼大勁封裝一層,到底有什麼用處呢?

一、參數複用

其實剛剛第一個add函數的柯里化例子中已經涉及到了函數柯里化所帶來的函數復用的便捷,我們透過add函數柯里化,很快捷地實現了increment函數和addTen函數,再來看個例子:

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

上面的範例是一個正規的校驗,正常來說直接呼叫check函數就可以了,但是如果我有很多地方都要校驗是否有數字,其實就是需要將第一個參數reg進行複用,這樣別的地方就能夠直接呼叫hasNumber,hasLetter等函數,讓參數能夠複用,呼叫起來也比較方便。

二、提前確認
var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

換一種寫法可能比較好理解一點,上面就是把isSupport這個參數給先確定下來了

var on = function(isSupport, element, event, handler) {
    isSupport = isSupport || document.addEventListener;
    if (isSupport) {
        return element.addEventListener(event, handler, false);
    } else {
        return element.attachEvent('on' + event, handler);
    }
}

我們在做專案的過程中,封裝一些dom操作可以說再常見不過,上面第一種寫法也是比較常見,但是我們看看第二種寫法,它相對一第一種寫法就是自執行然後返回一個新的函數,這樣其實就是事先確定了會走哪一個方法,避免每次都進行判斷

三、延遲計算/運行
Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)

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

像我們js中經常使用的bind,實現的機制就是Currying.

如何實作函數柯裡化?

通用的封裝方法:

// 初步封装
var currying = function(fn) {
    // args 获取第一个方法内的全部参数
    var args = Array.prototype.slice.call(arguments, 1)
    return function() {
        // 将后面方法里的全部参数和args进行合并
        var newArgs = args.concat(Array.prototype.slice.call(arguments))
        // 把合并后的参数通过apply作为fn的参数并执行
        return fn.apply(this, newArgs)
    }
}

這邊首先是初步封裝,透過閉包把初步參數給保存下來,然後透過取得剩下的arguments進行拼接,最後執行需要currying的函數。

但是上面的函數還是有些缺陷,這樣傳回的話其實只能多擴充一個參數,currying(a)(b)(c)這樣的話,貌似就不支援了(不支援多參數呼叫) ,一般這種情況都會想到使用遞迴再進行封裝一層。

// 支持多参数传递
function progressCurrying(fn, args) {

    var _this = this
    var len = fn.length;
    var args = args || [];

    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

這邊其實是在初步的基礎上,加上了遞歸的調用,只要參數個數小於最初的fn.length,就會繼續執行遞歸。

函數柯里化的效能怎麼樣?

關於Currying的效能,我們應該知道下面幾點:

  • 訪問arguments物件通常要比存取命名參數慢一點
  • 一些舊版本的瀏覽器在arguments.length的實作上是相當慢的
  • 使用fn.apply( … ) 和fn.call( … )通常比直接呼叫fn( … ) 稍微慢點
  • 創建大量巢狀作用域和閉包函數會帶來花銷,無論是在記憶體還是速度上

#其實在大部分應用中,主要的效能瓶頸是在操作DOM節點上,這js的效能損耗基本上是可以忽略的,所以curry是可以直接放心的使用。

柯里化面試題

// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(…arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9

總結

透過簡單地傳遞幾個參數,就能動態建立實用的新函數;而且還能帶來一個額外好處,那就是保留了數學的函數定義,儘管參數不只一個。

Currying函數用起來非常得心應手,每天使用它對我來說簡直就是一種享受。它堪稱手邊必備工具,能夠讓函數式程式設計不那麼繁瑣和沈悶。

更多程式相關知識,請造訪:程式設計入門! !

以上是詳解JavaScript中的函數柯理化的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除