函數柯里化

前言

函數式編程是一種將函數做爲參數傳遞和返回,而且沒有反作用的一種編程方式。JavaScript,Haskell,Clojure,Scala 和 Erlang 是部分實現了函數式編程的語言。函數式編程也帶來了不少概念javascript

  • 純函數(Pure Functions): 只是返回新的值,不改變系統變量
  • 柯里化(Currying): 接受多個參數的函數轉換成接受單一參數的函數的操做
  • 高階函數(Higher-order functions): 一個能夠接收函數做爲參數,甚至返回一個函數的函數 不過咱們今天只是來了解柯里化函數是如何使用的

什麼是函數柯里化

函數柯里化(currying)又稱部分求值。一個 currying 的函數首先會接受一些參數,接受了這些參數以後,該函數並不會當即求值,而是繼續返回另一個函數,剛纔傳入的參數在函數造成的閉包中被保存起來。待到函數被真正須要求值的時候,以前傳入的全部參數都會被一次性用於求值。java

Currying is the process of turning a function with multiple arity into a function with less arity(柯里化是將一個多元函數轉換爲低元函數的操做)—Kristina Brainwavegit

舉一個簡單的例子github

function adds(a, b, c) {
			return a + b + c
		}

		function currying(a) {
			return function (b) {
				return function(c){
					return a + b + c
				}
			}
		}

		adds(2, 3, 4)  // 9
		currying(2)(3)(4) // 9
複製代碼

優化currying函數

上面currying函數能夠實現較爲簡單的功能,結構也比較清晰,可是針對於更多參數的傳遞,咱們不能無限的嵌套下去,或者針對於不一樣的傳參方式,例如編程

currying(2, 3)(4)
		currying(2, 3, 4)
		currying(2)(3, 4)
複製代碼

上面的函數就不能支持了,所以須要對currying函數進行下一步優化。瀏覽器

function adds(a, b, c) {
			return a + b + c
		}
		function currying(fn, length){
			length = length || fn.length;  // 第一次調用獲取函數 fn 參數的長度,後續調用獲取 fn 剩餘參數的長度
			return function(){
				var args = [].slice.call(arguments)  // currying返回函數接受的參數
				if (args.length < length) {   // 判斷參數的長度是否小於 fn 剩餘參數須要接收的長度
					return curry(fn.bind(this, ...args), length - args.length)  // 遞歸 currying 函數,新的 fn 爲 bind 返回的新函數(bind 綁定了 ...args 參數,未執行),新的 length 爲 fn 剩餘參數的長度
				}else {
					return fn.call(this, ...args)   // 執行 fn 函數,傳入新函數的參數
				}
			}
		}

		var addCurry = currying(adds);
		addCurry(2)(3)(4) // 9
		addCurry(2, 3)(4)  // 9
		addCurry(2, 3, 4)  // 9
		addCurry(2)(3, 4) // 9
複製代碼

咱們經過判斷是否傳入足夠數量的參數長度決定是執行函數仍是遞歸currying返回新的函數。其中用到了call和bind。若是不想用call\apply\bind的功能。還有大神提供了最簡版本bash

const currying = fn =>
			judge = (...args) =>
				args.length >= fn.length
					? fn(...args)
					: (...arg) => judge(...args, ...arg)

		var addCurry = currying(adds);
		addCurry(2)(3)(4) // 9
		addCurry(2, 3)(4)  // 9
		addCurry(2, 3, 4)  // 9
		addCurry(2)(3, 4) // 9
複製代碼

上述兩個currying函數的實現原理都是「用閉包把傳入參數保存起來,當傳入參數的數量足夠執行函數時,就開始執行函數」。閉包

實際應用場景

減小重複傳遞不變的部分參數

function volume(l, w, h) {
			return l * w * h
		}

		let volumeA = volume(100, 100, 50)
		let volumeB = volume(100, 100, 90)
複製代碼

在計算貨物體積時咱們一般須要參入三個參數來計算體積,可是計算一樣長和寬而不一樣高的貨物,長度和寬度沒有必要每次都輸入。這時候咱們的柯里化就能夠運用於此,這樣就減小重複傳遞不變的部分參數app

var getvolume = currying(volume)(100, 100)
volumeA(50)
volumeB(60)
複製代碼

動態建立函數

有一種典型的應用情景是這樣的,每次調用函數都須要進行一次判斷,但其實第一次判斷計算以後,後續調用並不須要再次判斷,這種狀況下就很是適合使用柯里化方案來處理。即第一次判斷以後,動態建立一個新函數用於處理後續傳入的參數,並返回這個新函數。less

下面的例子中,在 DOM 中添加事件時須要兼容現代瀏覽器和 IE 瀏覽器(IE < 9),方法就是對瀏覽器環境進行判斷,看瀏覽器是否支持。通常狀況下每次添加事件都會調用作一次判斷。

function addEvent (type, el, fn, capture = false) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, capture);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}
複製代碼

可是咱們能夠以利用閉包和當即調用函數表達式(IIFE)來使判斷只用執行一次,動態建立新的函數用於處理後續傳入的參數,這樣作的好處就是以後調用就不須要再次計算了

const addEvent = (function(){
    if (window.addEventListener) {
        return function (type, el, fn, capture) {
            el.addEventListener(type, fn, capture);
        }
    }
    else if(window.attachEvent){
        return function (type, el, fn) {
            el.attachEvent('on' + type, fn);
        }
    }
})();
複製代碼

bind的模擬實現

bind 的模擬實現自己就是一種柯里化,bind是用來改變函數執行時候的上下文,可是函數自己並不執行,因此本質上是延遲計算

var o = { color: "blue" };

function sayColor(){

    alert(this.color);

}

var objectSayColor = sayColor.bind(o);

objectSayColor();    //blue
複製代碼

擴展

經過一個函數實現下列功能

add(1) // 1
		add(1)(2);  // 3
		add(1)(2)(3)// 6
		add(1)(2)(3)(4) // 10 
複製代碼

初級版

function add(a) {
			function sum(b) { // 使用閉包
				a = a + b; // 累加
				return sum;
			}
			sum.toString = function () { // 重寫toString()方法
				return a;
			}
			return sum; // 返回一個函數
		}
複製代碼

進階版本

function add() {
			let data = [].concat(Array.prototype.slice.call(arguments))
			function tmp() { // 使用閉包
			    data = data.concat(Array.prototype.slice.call(arguments))
				return tmp;
			}
			tmp.valueOf = function () {
				return data.reduce(((source, item) => source + item), 0);
			}
			tmp.toString = function () {
				return data.reduce(((source, item) => source + item), 0);
			}
			return tmp
		}
		add(1) // 1
		add(1,4)(2);  // 7
		add(1)(2, 5)(3)// 11
		add(1,5,6)(2,5)(3)(4, 4) // 30

複製代碼

性能

  • 存取 arguments 對象一般要比存取命名參數要慢一些。
  • 一些老版本的瀏覽器在 arguments.length 的實現上至關慢。
  • 使用 fn.apply() 和 fn.call() 要比直接調用 fn() 要慢點。
  • 建立大量嵌套做用域和閉包會帶來開銷,不管是內容仍是速度上。
  • 大多數瓶頸來自 DOM 操做

參考連接

相關文章
相關標籤/搜索