函数柯里化

在闭包函数的应用当中,有一个重要的场景就是柯里化。

1. 定义

柯里化是指这样一个函数(假设叫做createCurry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。
假如有一个接收三个参数的函数A

function A(a, b, c) {
    // TODO something
}

又假如我们有一个已经封装好了的柯里化通用函数createCurry。他接收bar作为参数,能够将A转化为柯里化函数,返回结果就是这个被转化之后的函数。

var _A = createCurry(A);

那么_A作为createCurry运行的返回函数,他能够处理A的剩余参数。因此下面的运行结果都是等价的。

_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);
A(1, 2, 3);

函数AcreateCurry转化之后得到柯里化函数_A_A能够处理A的所有剩余参数。因此柯里化也被称为部分求值。
在简单的场景下,我们可以不用借助柯里化通用式来转化得到柯里化函数,我们可以凭借眼力自己封装。
例如有一个简单的加法函数,他能够将自身的两个参数加起来并返回计算结果。

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

那么add函数的柯里化函数_add则可以如下:

function _add(x) {
  return function (y) {
    return x + y
  }
}

因此下面的运算方式是等价的。

add(1, 2);
_add(1)(2);

2. 柯里化函数封装

首先通过_add可以看出,柯里化函数的运行过程其实是一个参数的收集过程,我们将每一次传入的参数收集起来,并在最里层里面处理。因此我们在实现createCurry时,可以借助这个思路来进行封装。

//ES5实现
function curry(fn) {
  var args = [],  //装总的参数的数组
    n = fn.length;   //传入的函数的参数个数
  return function core() {
    var arg = [].slice.call(arguments);  //将任意的类数组对象转化为数组,返回的函数传入的参数
    args = args.concat(arg);  //收集传入的参数
    n -= arg.length;  //n的值在不断递归传入参数的过程中逐渐减小,用于判断是否继续递归core
    return n===0 ? fn.apply(null, args) : core;
  }
}

var add4 = curry(function (a, b, c, d) {
  return a + b + c + d;
});
console.log(add4(1, 2)(2)(3));  //8

ES6中使用展开运算符"..."可以不用手动指定this的绑定,还有箭头函数的特性自行了解,简化了封装的复杂性:

//ES6实现
const curry = (fn, ...args) => 
    args.length < fn.length
        // 参数长度不足时 重新柯里化该函数 等待接受新的参数
        ? (...arguments) => curry(fn, ...args, ...arguments)
        // 参数长度满足时 执行函数
        : fn(...args);


function sumFn(a, b, c) {
    return a + b + c;
}
// 举例
console.log(curry(sumFn, 7, 8, 6))  // 21
console.log(curry(sumFn, 7, 8)(6))  // 21
console.log(curry(sumFn)(7)(8)(6));  // 21
console.log(curry(sumFn, 7, 8)(6));  // 21

3. 柯里化函数的应用

  • 参数记忆,即对于多参数的函数我可以记忆前面的参数:
let add1 = add4(1,2,3)
add1(4)//输出10
add1(5)//输出11
add1(6)//输出12

可以看到,我把add4的参数1,2,3给记忆下来了,然后如果其他地方得用到参数1,2,3的话就没必要写了。

  • Function.prototype.bind的实现
    Funtion.prototype.bind的实现也利用了curry化的原理,bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:f.bind(obj),实际上可以理解为obj.f(),这时f函数体内的this自然指向的是obj
var a = {
    b: function() {
      var func = function() {
        console.log(this.c);
      }
      func();
    },
    c: 'hello'
  }
  a.b();   // undefined 这里的this指向的是全局作用域
  console.log(a.c);   // hello

利用bind方法:

var a = {
    b: function() {
      var func = function() {
        console.log(this.c);
      }.bind(this);
      func();
    },
    c: 'hello'
  }
  a.b();  // hello
  console.log(a.c);  // hello
// 分析:这里的bind方法会把它的第一个实参绑定给f函数体内的this,所以里的this即指向{x:1}对象;
// 从第二个参数起,会依次传递给原始函数,这里的第二个参数2即是f函数的y参数;
// 最后调用m(3)的时候,这里的3便是最后一个参数z了,所以执行结果为1+2+3=6
// 分步处理参数的过程其实是一个典型的函数柯里化的过程(Curry)
function f(y,z){
  return this.x+y+z;
}
var m = f.bind({x:1},2);
console.log(m(3)); // 6

实现bind方法:

Function.prototype.my_bind = function() {
  var self = this, // 保存需要绑定的this上下文
    context = Array.prototype.shift.call(arguments),
    // arguments 是类数组对象,它没有 shift 等数组独有的方法,想要弹出传入的参数中的第一个参数,就只有用这种方式了。
    args = Array.prototype.slice.call(arguments); // 剩余的参数转为数组
  return function() {
    self.apply(context, Array.prototype.concat.call(args, Array.prototype.slice.call(arguments)));
  }
};

//样例
function a(m, n, o) {
  console.log(this.name + ' ' + m + ' ' + n + ' ' + o);
}

var b = {
  name: 'kong'
};

a.my_bind(b, 7, 8)(9); // kong 7 8 9

不过往原型上加东西通常是不好的,所有还是写个bind 函数吧:

function bind(fn) {
  var args = [].slice.call(arguments);
  args.shift();  //除去fn
  var that = args.shift();//this参数
  return function () {
    var args1 = [].slice.call(arguments);//第二部分参数
    return fn.apply(that, args.concat(args1));
  }
}
 
var getById = bind(document.getElementById, document);
getById("name");

4. 总结

在前面的几个例子中,我们可以总结一下柯里化的特点:

  • 接收单一参数,将更多的参数通过回调函数来搞定
  • 返回一个新函数,用于处理所有的想要传入的参数;
  • 需要利用call/apply与arguments对象收集参数;
  • 返回的这个函数正是用来处理收集起来的参数。

转载自
https://blog.csdn.net/daydream13580130043/article/details/83718978;
https://www.jianshu.com/p/5e1899fe7d6b
https://www.jianshu.com/p/7030376af23c

你可能感兴趣的:(JavaScript,函数式编程,柯里化)