Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入js之函数柯里化 #10

Open
FE-Sadhu opened this issue Apr 23, 2019 · 1 comment
Open

深入js之函数柯里化 #10

FE-Sadhu opened this issue Apr 23, 2019 · 1 comment
Labels
深入js笔记 about javascript

Comments

@FE-Sadhu
Copy link
Owner

什么是柯里化

wiki上这样定义:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

柯里化又称部分求值,字面意思就是不会立刻求值,而是到了需要的时候再去求值。

举个例子:

function add(a, b) {
  return a + b;
}

function curryingAdd(a) {
  return function(b) {
    return a + b;
  }
}

add(1, 2); // 3
curryingAdd(1)(2); // 3

函数add是一般的一个函数,就是将传进来的参数a和b相加;函数curryingAdd就是对函数add进行柯里化的函数;这样一来,原来我们需要直接传进去两个参数来进行运算的函数,现在需要分别传入参数a和b。

为什么要这样做?

  • 提前绑定好函数里面的某些参数,达到参数复用的效果,提高了适用性.
  • 固定易变因素
  • 延迟计算

总之,函数的柯里化能够让你重新组合你的应用,把你的复杂功能拆分成一个一个的小部分,每一个小的部分都是简单的,便于理解的,而且是容易测试的。

如何对函数进行柯里化

由浅入深讲解如何对一个多参数的函数进行柯里化。

第一步(浅)

假如我们要实现一个功能,就是输出语句name喜欢song,其中name和song都是可变参数;那么一般情况下我们会这样写:

function print(name, song) {
  console.log(name + '喜欢的歌曲是:' + song);
}

对print函数进行柯里化后的函数应该是这样:

function curryingPrint(name) {
  return function(song) {
    console.log(name + '喜欢的歌曲是:' + song);
  }
}

var tomLike = curryingPrint('Tom');
tomLike('七里香');
var jerryLike = curryingPrint('Jerry');
jerryLike('雅俗共赏');

// Tom喜欢的歌曲是:七里香
// Jerry喜欢的歌曲是:雅俗共赏

第二步(中)

上面我们虽然对函数print进行了柯里化,但是我们可不想在需要柯里化的时候,都像上面那样不断地进行函数的嵌套,这样代码会很难看,也不容易维护了。所以我们要创造一些帮助其它函数进行柯里化的函数来解决这个问题,我们暂且叫它为curryingHelper吧,一个简单的curryingHelper函数如下所示:

function curryingHelper(fn) {
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    var traceArgs = Array.prototype.slice.call(arguments);
    var resArgs = args.concat(traceArgs);
    return fn.apply(null, resArgs); //返回执行结果
  }
}

验证一下?

function showMsg(name, age, fruit) {
  console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit);
}

var curryingShowMsg1 = curryingHelper(showMsg, 'sadhu');
curryingShowMsg1(21, 'apple');

console.log('---');

var curryingShowMsg2 = curryingHelper(showMsg, 'sadhu', 21);
curryingShowMsg2('apple');

console.log('---');

var curryingShowMsg3 = curryingHelper(showMsg);
curryingShowMsg3('sadhu', 21, 'apple');

/* 输出
 My name is sadhu, I'm 21 years old,  and I like eat apple
---
My name is sadhu, I'm 21 years old,  and I like eat apple
---
My name is sadhu, I'm 21 years old,  and I like eat apple
*/

证明,我们这个柯里化的函数是🙆的。这里的curryingHelper就是一个高阶函数,函数为参数,返回值也是函数。

第三步(深)

上面的柯里化帮助函数确实已经能够达到我们的一般性需求了,但是它还不够好,我们希望那些经过柯里化后的函数可以每次只传递进去一个参数,然后可以进行多次参数的传递,拿上面的例子来说,就是在能实现curryingHelper的返回函数的调用方式的基础上再实现如下这样传:

betterShowMsg('sadhu')(21)('apple');

动下脑筋,写一个betterCurryingHelper函数来实现:

function curryingHelper(fn) {
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    var traceArgs = Array.prototype.slice.call(arguments);
    var resArgs = args.concat(traceArgs);
    return fn.apply(null, resArgs); //返回执行结果
  }
}

function betterCurryingHelper(fn, len) {
  var length = len || fn.length // 可以指出fn总形参的个数
  return function() {
    var allArgsFulfilled = (arguments.length >= length);
    
    // 如果参数全部满足,就可以终止递归调用
    if (allArgsFulfilled) {
      return fn.apply(null, arguments);
    } else {
      var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments));
      return betterCurryingHelper(curryingHelper.apply(null, argsNeedFulfilled), length - arguments.length);
    }
  }
}

curryingHelper()是第二步里的那个函数。这里的代码其实没啥难度,关键在于你能不能理清递归的过程。理解不了的仔细多看看。

验证一下?

// 验证一下
function showMsg(name, age, fruit) {
  console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit);
}

var betterShowMsg = betterCurryingHelper(showMsg);

betterShowMsg('sadhu', 21, 'apple');
console.log('---');
betterShowMsg('sadhu', 21)('apple');
console.log('---');
betterShowMsg('sadhu')(21, 'apple');
console.log('---');
betterShowMsg('sadhu')(21)('apple');

/* 输出
My name is sadhu, I'm 21 years old,  and I like eat apple
---
My name is sadhu, I'm 21 years old,  and I like eat apple
---
My name is sadhu, I'm 21 years old,  and I like eat apple
---
My name is sadhu, I'm 21 years old,  and I like eat apple
*/

成功,刺激。

一个应用场景:给setTimeout传递地进来的函数添加参数。

一般情况下,我们如果想给一个setTimeout传递进来的函数添加参数的话,一般会使用这种方法:

function hello(name) {
    console.log('Hello, ' + name);
}
setTimeout(hello('dreamapple'), 3600); //立即执行,不会在3.6s后执行
setTimeout(function() {
    hello('dreamapple');
}, 3600); // 3.6s 后执行

我们使用了一个新的匿名函数包裹我们要执行的函数,然后在函数体里面给那个函数传递参数值.

当然,在ES5里面,我们也可以使用函数的bind方法,如下所示:

setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后执行函数

这样也是非常的方便快捷,并且可以绑定函数执行的上下文.

我们本篇文章是讨论函数的柯里化,当然我们这里也可以使用函数的柯里化来达到这个效果:

setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已经提及过的

这样也是可以的,是不是很酷.其实函数的bind方法也是使用函数的柯里化来完成的。可以看这里:深入js之造bind轮子

关于柯里化的性能

当然,使用柯里化意味着有一些额外的开销;这些开销一般涉及到这些方面,首先是关于函数参数的调用,操作arguments对象通常会比操作命名的参数要慢一点;还有,在一些老的版本的浏览器中arguments.length的实现是很慢的;直接调用函数fn要比使用fn.apply()或者fn.call()要快一点;产生大量的嵌套作用域还有闭包会带来一些性能还有速度的降低.但是,大多数的web应用的性能瓶颈时发生在操作DOM上的,所以上面的那些开销比起DOM操作的开销还是比较小的。

本文完。

希望看到各位技术人对这篇文章有不同的有“证据”,符合逻辑的分析、看法~

文章会第一时间更新在GitHub,觉得写得还不错的,可以点个star支持下作者🍪


参考:

  1. 掌握JavaScript函数的柯里化
  2. JavaScript 中的函数式编程实践
  3. 函数式JavaScript(4):函数柯里化
  4. 前端开发者进阶之函数柯里化Currying
  5. js基础篇之——JavaScript的柯里化函数详解
  6. JavaScript专题之函数柯里化
@FE-Sadhu FE-Sadhu added the 深入js笔记 about javascript label Apr 23, 2019
@PutinCake
Copy link

踏破铁鞋搞不懂,在你这里看懂了!必须好评!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
深入js笔记 about javascript
Projects
None yet
Development

No branches or pull requests

2 participants