Skip to content
^finished

什么是函数柯理化?

观察如下代码,调用函数的时候可以连着写好几个括号参数。

js
function add_1(a) {
    return function add_2(b) {
        return function add_3(c){
            console.log(a,b,c)
        }
    }
}
add_1(3)(2)(1)
function add_1(a) {
    return function add_2(b) {
        return function add_3(c){
            console.log(a,b,c)
        }
    }
}
add_1(3)(2)(1)

在函数里ruturn另一个函数本身即可。

函数柯理化 与 将函数赋值给变量

首先提一句,函数是可以直接赋值给变量的。

js
let VariableOfFn = function(val) {
    console.log(val)
    return val
}
console.log(VariableOfFn) // 这里会将函数本身打印出来
VariableOfFn(VariableOfFn(1)) // 1 这里打印的是函数return的值
let VariableOfFn = function(val) {
    console.log(val)
    return val
}
console.log(VariableOfFn) // 这里会将函数本身打印出来
VariableOfFn(VariableOfFn(1)) // 1 这里打印的是函数return的值

那么,结合函数柯理化,怎么实现呢?

js
function add_1(a) {
	let add_2 = function(b) {
		let add_3 = function(c) {
			console.log(a,b,c)
		}
		return add_3
	}
	return add_2
}
add_1(3)(2)(1) // 3,2,1
function add_1(a) {
	let add_2 = function(b) {
		let add_3 = function(c) {
			console.log(a,b,c)
		}
		return add_3
	}
	return add_2
}
add_1(3)(2)(1) // 3,2,1

WARNING

需要注意的是,return的时候要仅return变量名字,不要带括号参数,否则无法实现函数柯理化。

如果你真的想return变量名字+括号+参数,那么你可以这样做:

js
function add_1(a) {
	let add_2 = function(b) {
		let add_3 = function(c) {
			console.log(a,b,c)
		}
		return add_3(1)
	}
	return add_2
}
add_1(3)(2) // 3,2,1 == 上面的add_1(3)(2)(1)
function add_1(a) {
	let add_2 = function(b) {
		let add_3 = function(c) {
			console.log(a,b,c)
		}
		return add_3(1)
	}
	return add_2
}
add_1(3)(2) // 3,2,1 == 上面的add_1(3)(2)(1)

这样的话中间会产生断层

js
function add_1(a) {
	let add_2 = function(b) {
		let add_3 = function(c) {
			console.log(a,b,c)
		}
		return add_3
	}
	return add_2(2)
}
add_1(3)(1) // 3,2,1 == 上上面的add_1(3)(2)(1)
function add_1(a) {
	let add_2 = function(b) {
		let add_3 = function(c) {
			console.log(a,b,c)
		}
		return add_3
	}
	return add_2(2)
}
add_1(3)(1) // 3,2,1 == 上上面的add_1(3)(2)(1)

根据函数参数个数判断

要判断当前传入函数的参数个数 (args.length) 是否大于等于原函数所需参数个数 (fn.length) ,如果是,则执行当前函数;如果是小于,则返回一个函数。

js
const curry = (fn, ...args) => 
    // 函数的参数个数可以直接通过函数数的.length属性来访问
    args.length >= fn.length // 这个判断很关键!!!
    // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
    ? fn(...args)
    /**
     * 传入的参数小于原始函数fn的参数个数时
     * 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
    */
    : (..._args) => curry(fn, ...args, ..._args);

function add1(x, y, z) {
    return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
const curry = (fn, ...args) => 
    // 函数的参数个数可以直接通过函数数的.length属性来访问
    args.length >= fn.length // 这个判断很关键!!!
    // 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
    ? fn(...args)
    /**
     * 传入的参数小于原始函数fn的参数个数时
     * 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数
    */
    : (..._args) => curry(fn, ...args, ..._args);

function add1(x, y, z) {
    return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));

偏函数

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

偏函数(局部应用)则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。本质上可以将偏函数看成是柯里化的一种特殊情况。bind函数的实现过程就是偏函数。

它两的区别在于偏函数会固定你传入的几个参数,再一次性接受剩下的参数,而函数柯里化会根据你传入的参数不停的返回函数,直到参数个数满足被柯里化前函数的参数个数

js
//偏函数的基本实现,可以看出跟柯里化的基本实现是一样的
function partial(fn){
    let args=[].slice.call(arguments,1);
    return function(){
        return fn.apply(this,[...args,...arguments]);
    }
}
//进阶版 占位符
function partial(fn){
    let args=[].slice.call(arguments,1);
    return function(){
        let index=0;//用于统计占位符的个数
        let len=args.length;
        for(let i=0;i<len;i++){
            args[i]=args[i]==="_"?arguments[index++]:args[i];
        }
        for(;index<arguments.length;index++){
            args.push(arguments[index]);
        }
        return fn.apply(this,args);
    }
}
测试
function foo(a,b){
    console.log(a+b);
}
let f=partial(foo,2);
f(3);//5
function foo(a,b){
    console.log(a+b);
}
let f=partial(foo,2,"_");
f(3);//5
//偏函数的基本实现,可以看出跟柯里化的基本实现是一样的
function partial(fn){
    let args=[].slice.call(arguments,1);
    return function(){
        return fn.apply(this,[...args,...arguments]);
    }
}
//进阶版 占位符
function partial(fn){
    let args=[].slice.call(arguments,1);
    return function(){
        let index=0;//用于统计占位符的个数
        let len=args.length;
        for(let i=0;i<len;i++){
            args[i]=args[i]==="_"?arguments[index++]:args[i];
        }
        for(;index<arguments.length;index++){
            args.push(arguments[index]);
        }
        return fn.apply(this,args);
    }
}
测试
function foo(a,b){
    console.log(a+b);
}
let f=partial(foo,2);
f(3);//5
function foo(a,b){
    console.log(a+b);
}
let f=partial(foo,2,"_");
f(3);//5

反柯里化

柯里化其实就是偏函数的特殊情况,所以在反柯里化这里就之说偏函数,我觉得这样更合适

对比 —— 偏函数、反柯里化

偏函数:偏函数是对高阶函数的降阶处理,再朴素点的描述就是,降低函数的通用性,创建一个针对性更强的函数,比如上面讲的偏函数部分的ajax和partialAjax

反柯里化:和偏函数刚好相反,增加方法的适用范围(即通用性)

  • 通用代码
js
Function.prototype.uncurrie = function (obj) {
  // 参数obj是需要操作的对象
  // 这里的this是指obj对象需要借用的方法,比如示例中的Array.prototype.push
  const fnObj = this
  return function (...args) {
    // 难点,以下代码相当于:fnObj.call(obj, ...args), 没理解请看下面的 “代码解析” 部分
    return Function.prototype.call.apply(fnObj, [obj, ...args])
  }
}

// 示例,导出Array.prototype.push方法给对象使用
const obj = { a: 'aa' }
const push = Array.prototype.push.uncurrie(obj)
push('b')
push('c')
console.log(obj)  // {0: "b", 1: "c", a: "aa", length: 2}
Function.prototype.uncurrie = function (obj) {
  // 参数obj是需要操作的对象
  // 这里的this是指obj对象需要借用的方法,比如示例中的Array.prototype.push
  const fnObj = this
  return function (...args) {
    // 难点,以下代码相当于:fnObj.call(obj, ...args), 没理解请看下面的 “代码解析” 部分
    return Function.prototype.call.apply(fnObj, [obj, ...args])
  }
}

// 示例,导出Array.prototype.push方法给对象使用
const obj = { a: 'aa' }
const push = Array.prototype.push.uncurrie(obj)
push('b')
push('c')
console.log(obj)  // {0: "b", 1: "c", a: "aa", length: 2}
代码解析

这部分内容负责解析上面的通用代码

首先声明,个人觉得这个通用代码是没必要的,因为这段通用代码的本质就是call、apply,通过call、apply改变方法的this上下文,使得对象可以使用不属于它的方法,这也是反柯里化的本质,增强方法的使用范围

这段通用代码的难点在于Function.prototype.call.apply(fnObj, [obj, ...args])这句,以下解析采用通用代码中的示例代码 以下解释需要你熟悉apply、call方法的源码实现,如果不熟悉请参考 javascript源码解析,里面的call、apply两部分的源码解析会回答你的疑问

正式开始解析通用代码,通过通用代码中的示例代码进行讲解

通用代码其实就是个闭包,执行Array.prototype.push.uncurrie(obj),传递一个需要操作的对象(const obj = {a: 'aa'}),其中fnObj = Array.prototype.push,这时向外面return一个接收参数的函数 返回的函数中就一句代码: return Function.prototype.call.apply(fnObj, [obj, ...args]), 上面的代码可以翻译为: return Function.prototype.call.apply(Array.prototype.push, [{a: 'aa'}, ...args]) 再进一步翻译(需要了解call、apply的原理,不明白请参考javascript源码解析): return Array.prototype.push.call({a: 'aa'}, ...args),这句就等同于: Arrray.prototype.push.call(obj, 'b'),看到这里就会明白我开始说的 “声明” 部分的意思了

INFO

偏函数都用在哪些地方 需要减少参数的地方 需要延迟计算的地方 Function.prototype.bind其实就是偏函数的应用 反柯里化都用在哪些地方 一个对象需要借用其它对象的方法时用反柯里化