Rashomon511/MyBlog

函数柯里化

Opened this issue · 0 comments

前言

函数式编程是一种将函数作为参数传递和返回,并且没有副作用的一种编程方式。JavaScript,Haskell,Clojure,Scala 和 Erlang 是部分实现了函数式编程的语言。函数式编程也带来了很多概念

  • 纯函数(Pure Functions): 只是返回新的值,不改变系统变量
  • 柯里化(Currying): 接受多个参数的函数转换成接受单一参数的函数的操作
  • 高阶函数(Higher-order functions): 一个可以接收函数作为参数,甚至返回一个函数的函数
    不过我们今天只是来了解柯里化函数是如何使用的

什么是函数柯里化

函数柯里化(currying)又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

Currying is the process of turning a function with multiple arity into a function with less arity(柯里化是将一个多元函数转换为低元函数的操作)—Kristina Brainwave

举一个简单的例子

		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的功能。还有大神提供了最简版本

		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)

在计算货物体积时我们通常需要参入三个参数来计算体积,但是计算同样长和宽而不同高的货物,长度和宽度没有必要每次都输入。这时候我们的柯里化就可以运用于此,这样就减少重复传递不变的部分参数

var getvolume = currying(volume)(100, 100)
volumeA(50)
volumeB(60)

动态创建函数

有一种典型的应用情景是这样的,每次调用函数都需要进行一次判断,但其实第一次判断计算之后,后续调用并不需要再次判断,这种情况下就非常适合使用柯里化方案来处理。即第一次判断之后,动态创建一个新函数用于处理后续传入的参数,并返回这个新函数。

下面的例子中,在 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 操作

参考链接