✨ JavaScript柯里化的实现

345 阅读4分钟

著有《React 设计原理》《React 用到的一些算法》《javascript地月星》等多个专栏。欢迎关注。

欢迎收看我的专栏,文章不好写,要是有帮助别忘了点赞👍、收藏⭐️、评论📝,你的鼓励是我继续挖干货的动力🔥🔥。

另外,本文为原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解~

大家好,我是骑自行车,开奔驰,住别墅的码农。本文简单介绍柯里化的定义,从「静态作用域-参数」和「调用时机」的角度分析柯里化。

定义

柯里化是将多个参数的函数分解成多个一个参数进行调用的方式。

curry(function (a, b, c) {return [a, b, c]})("a")("b")("c")

柯里化的“高级”实现还可以有各种传参方法:

let curfn = curry(function (a, b, c) {return [a, b, c]});
curfn("a", "b", "c") // ["a", "b", "c"]
curfn("a", "b")("c") // ["a", "b", "c"]
curfn("a")("b")("c") // ["a", "b", "c"]
curfn("a")("b", "c") // ["a", "b", "c"]

柯里化的实现

  • 普通实现:
function curry(fn) {
  let args = Array.prototype.slice.call(arguments, 1)
  return function() {
    let newargs = args.concat(Array.prototype.slice.apply(arguments));
    return fn.apply(this, newargs);
  }
}

普通实现的问题是只能分2次调用curry()()

let name = curry(function (key, obj) {return obj[key]})('name',{name: 'jack'});
console.log(name);

不能分1次、3次或更多次curry()()()

let name1 = curry(function (key, obj) {return obj[key]})('name')({name: 'mark'})
console.log(name1);

因为curry已经返回一个回调函数了,到('name')已经执行掉这个回调,没有其他回调了,再次({name:'mark'})就报错。

  • 递归实现:适应任意层次的调用
function sub_curry(fn) {
  var args = [].slice.call(arguments, 1);
  return function() {
      return fn.apply(this, args.concat([].slice.call(arguments)));
  };
}

function curry(fn, length) {
  length = length || fn.length;
  var slice = Array.prototype.slice;
  return function() {
      if (arguments.length < length) {
          var combined = [fn].concat(slice.call(arguments));
          //sub_curry.apply(this, combined)把外层的参数拼接到fn,curry(fn),curry(fn,a),curry(fn,a,b),curry(fn,a,b,c)
          return curry(sub_curry.apply(this, combined), length - arguments.length);
      } else {
          return fn.apply(this, arguments);
      }
  };
}

length初始值是fn.length, 然后return curry(cb, length-arguments.length)更新为剩下的参数个数。
例如:curry(fn(a,b,c))('a')('b','c'),fn.length是3,lenght初始值是3。 curry(fn(a,b,c)('a')后length更新为2。 curry(fn(a,b,c))('a')('b','c')时argument.length=2,length=2, 不会执行“curry(cb, length - arguments.lenght)更新length,减2变成0”,而是结束递归,执行fn.apply(this,arguments)。

curry

curry(fn)返回的是return function(){if(arguments.length<length){} else {})
curry(fn)(参数1)执行这个匿名函数,返回return curry(sub_curry.apply(this,[fn,参数1]),)

curry和sub_curry的fn

carry3.png

最内层的fn是什么?
js是静态作用域,因此,fn在curry声明的时候就确定是function curry(fn,length){}的形参fn。 沿着作用域链向上找(从下往上),
fn红色——>
...蓝色——>
sub3 = sub_curry.apply(this,combined)紫色——>
sub2= sub_curry.apply(this,combined)绿色——>
sub1 = sub_curry.apply(this,combined)黄色——>
原函数例如function (key, obj) { return obj[key] }

为了简化,省略蓝色:

原函数组装3次,就是红色的fn

sub1 = sub_curry.apply(this,[原函数])
sub2 = sub_curry.apply(this,[sub1])
sub3 = sub_curry.apply(this,[sub2])

sub1 = function(){ 
        return 原函数.apply(this,args.concat([].slice.arguments)))
     }    
sub2 = function(){ 
        return sub1.apply(this,args.concat([].slice.arguments)))
     }
sub3 = function(){ 
        return sub2.apply(this,args.concat([].slice.arguments)))
     }
     
     
sub2 = function(){ 
    return function(){ 
        return 原函数.apply(this,args.concat([].slice.arguments)))
     }.apply(this,args.concat([].slice.arguments)))
}
sub3 = function(){ 
        return function(){ 
            return function(){ 
                return 原函数.apply(this,args.concat([].slice.arguments)))
             }.apply(this,args.concat([].slice.arguments)))
        }.apply(this,args.concat([].slice.arguments)))
     }

结论:最内层的fn就是sub3。sub3一旦执行就能一层层拼接参数。

大概可能也许是这样...

结论

递归长长的一段源码,例如科里化,最终组成的就是一段调用方式的拼接,如上的短短的一段调用的拼接。
function curry{}()()() === function(){ return function(){}.apply(this,)}.apply(this,)

原函数最后才执行,一层递归拼接一部分参数,执行到原函数时参数全部拼接好了,就可以执行原函数了。

执行函数——>返回匿名函数,称为柯里化也好,称为拼接模式也好,把原函数放到返回的匿名函数中,具有「延迟执行」原函数的作用,匿名函数执行时还能利用arguments把它的参数拼接到原函数。所以它的本质是,利用这种模式,不断的把参数拼接到原函数中。即:
fn1=function(){ return 原函数.apply() } 把参数拼接到原函数
fn2=function(){ return fn1.apply() } 把参数拼接到函数fn1
...
fn2把参数拼接给fn1,fn1把全部参数拼接给原函数。
柯里化的本质是执行函数——>返回匿名函数实现的参数拼接算法。

这里还有更好理解的写法,原理是相同的,做函数参数的拼接

function curry(fn, length) {
  length = length || fn.length;
  return function() {
    if(arguments.length < length) {
      let args = [].slice.call(arguments);
      let cb = function() {//同样的原理换一种写法,把外层的参数拼接到fn,curry(fn),curry(fn,a),curry(fn,a,b),curry(fn,a,b,c)
        return fn.apply(this, args.concat([...arguments]));
      }
      return curry(cb, length - arguments.length);
    } else {
      return fn.apply(this, arguments)
    }
  }
}