JavaScript偏函数与柯里化实例详解


Posted in Javascript onMarch 27, 2019

本文实例讲述了JavaScript偏函数与柯里化。分享给大家供大家参考,具体如下:

到目前为止我们仅讨论绑定this,现在让我们更深入学习。
我们不仅能绑定this,也可以是参数,这较少使用,但有时很方便。

bind完整的语法为:

let bound = func.bind(context, arg1, arg2, ...);

可以绑定上下文this和函数的初始参数。举例,我们有个乘法函数mul(a,b):

function mul(a, b) {
 return a * b;
}

我们可以在该函数的基础上使用绑定创建一个double函数:

let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

调用mul.bind(null, 2)创建新函数double,传递调用mul函数,固定第一个参数上下文为null,第二个参数为2,多个参数传递也是如此。

这称为偏函数应用——我们创造一个新函数,让现有的一些参数值固定。

注意,这里确实不用this,但bind需要,所以必须使用null。

在下面代码中函数triple实现乘以3的功能:

let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15

为什么我们通常使用偏函数?

这里我们偏函数的好处是:通过创建一个名称易懂的独立函数(double,triple),调用是无需每次传入第一个参数,因为第一个参数通过bind提供了固定值。

另一种使用偏函数情况是,当我们有一个很通用的函数,为了方便提供一个较常用的变体。

举例,我们有一个函数send(from, to, text),那么使用偏函数可以创建一个从当前用户发送的变体:sendTo(to, text)

使用没有上下文的偏函数

如果想固定一些参数,但不绑定this呢?

内置的bind不允许这样,我们不能忽略上下文并跳转到参数。幸运的是,可以仅绑定参数partial函数容易实现。

如下:

function partial(func, ...argsBound) {
 return function(...args) { // (*)
  return func.call(this, ...argsBound, ...args);
 }
}
// Usage:
let user = {
 firstName: "John",
 say(time, phrase) {
  alert(`[${time}] ${this.firstName}: ${phrase}!`);
 }
};
// add a partial method that says something now by fixing the first argument
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Something like:
// [10:00] Hello, John!

调用partial(func[, arg1, arg2...])函数的结果为调用func的包装器(*号行):

  • this一致(因为user.sayNow是通过user调用的)
  • 然后给其...garsBound—— partial使用该参数("10:00")进行调用。
  • 然后提供参数...gars——提供给包装器的参数(“Hello“)

所以使用spread运算符很容易实现,是吗?

loadash库也提供了—.partial实现。

柯里化

有时人们混淆上面提及的偏函数和另一个名称为“柯里化”函数功能,柯里化是另一个有趣的处理函数技术,这里我们必须要涉及。

柯里化(Currying):转换一个调用函数f(a,b,c)f(a)(b)(c)方式调用。

让我们实现柯里化函数,执行一个两元参数函数,即转换f(a,b)f(a)(b):

function curry(func) {
 return function(a) {
  return function(b) {
   return func(a, b);
  };
 };
}
// usage
function sum(a, b) {
 return a + b;
}
let carriedSum = curry(sum);
alert( carriedSum(1)(2) ); // 3

上面是通过一系列包装器实现的。

  • curry(func)的结果是function(a)的一个包装器。
  • 当调用sum(1)是,参数被保存在词法环境中,然后返回新的包装器function(b)
  • 然后sum(1)(2)提供2并最终调用function(b),然后传递调用给原始多参数函数sum

有一些柯里化的高级实现,如lodash库中_.curry可以实现更复杂功能。其返回一个包装器,它允许函数提供全部参数被正常调用或返回偏函数。

function curry(f) {
 return function(..args) {
  // if args.length == f.length (as many arguments as f has),
  //  then pass the call to f
  // otherwise return a partial function that fixes args as first arguments
 };
}

柯里化?应用场景?

高级柯里化允许函数正常调用,也可以容易以偏函数方式调用。为了理解其优势,我们需要一个实际的示例说明。

举例,我们有日志函数log(date,importance,message),格式化输出信息。实际项目中这些函数也有许多其他有用的特性,如:通过网络发送或过滤:

function log(date, importance, message) {
 alert(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}

让我们使用柯里化!

log = _.curry(log);

柯里化后仍然可以正常调用:log(new Date(), "DEBUG", "some debug");

我们也可以使用柯里化方式调用:log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)

这里定义一个便捷函数,记录当天日志:

// todayLog will be the partial of log with fixed first argument
let todayLog = log(new Date());
// use it
todayLog("INFO", "message"); // [HH:mm] INFO message

现在再定义一个便捷函数:记录当天debug信息:

let todayDebug = todayLog("DEBUG");
todayDebug("message"); // [HH:mm] DEBUG message

所以:

1. 柯里化后没有失去任何东西,log仍然可以正常调用。
2. 我们能生成在多个场景使用的便捷偏函数。

高级柯里化实现

如果你感兴趣,这里提供了上面提到的高级柯里化实现:

function curry(func) {
 return function curried(...args) {
  if (args.length >= func.length) {
   return func.apply(this, args);
  } else {
   return function(...args2) {
    return curried.apply(this, args.concat(args2));
   }
  }
 };
}
function sum(a, b, c) {
 return a + b + c;
}
let curriedSum = curry(sum);
// still callable normally
alert( curriedSum(1, 2, 3) ); // 6
// get the partial with curried(1) and call it with 2 other arguments
alert( curriedSum(1)(2,3) ); // 6

这里实现看上去有点复杂,但确实很容易理解。curry(func)的结果是包装器curried,如下所示:

// func is the function to transform
function curried(...args) {
 if (args.length >= func.length) { // (1)
  return func.apply(this, args);
 } else {
  return function pass(...args2) { // (2)
   return curried.apply(this, args.concat(args2));
  }
 }
};

当我们运行时,有两个分支:

1. 如果传递args数与原函数已经定义的参数个数一样或更长,那么直接调用。
2. 获得偏函数:否则,不调用func函数,返回另一个包装器pass,提供连接之前的参数一起做为新参数重新应用curried。然后再次执行一个新调用,返回一个新偏函数(如果参数不够)或最终结果。

举例,让我们看sum(a, b, c)会怎样,三个参数,所以sum.length=3.

如果调用curried(1)(2)(3):

1. 第一次调用curried(1),在词法环境中记住1,返回包装器pass
2. 使用(2)调用包装器pass:其带着前面的参数(1),连接他们然后调用curried(1,2),因为参数数量仍然小于3,返回pass。
3. 再次使用(3)被调用包装器pass,带着之前的参数(1,2),然后增加3,并调用curried(1,2,3)——最终有三个参数,传递给原始函数。

如果仍然不清除,可以按顺序在脑子里或纸上跟踪调用过程。

仅针对函数参数长度固定

柯里化需要函数有已知的参数数量固定。

比柯里化多一点

根据柯里化定义,转换sum(a,b,c)sum(a)(b)(c).

但在Javascript中大多数实现是超越定义,也可以让函数使用多个参数变量执行。

总结

当把已知函数的一些参数固定,结果函数被称为偏函数,通过使用bind获得偏函数,也有其他方式实现。

当我们不想一次一次重复相同的参数时,偏函数是很便捷的。如我们有send(from,to)函数,如果from总是相同的,可以使用偏函数简化调用。

柯里化是转换函数调用从f(a,b,c)f(a)(b)(c).Javascript通常既实现正常调用,也实现参数数量不足时的偏函数方式调用。

当我们想容易的偏函数时,柯里化非常好。如我们已经看到的日志示例:通用的函数是log(date,importance,message),柯里化之后获得偏函数为,一个参数如log(date),或两个参数log(date,importance).

更多关于JavaScript相关内容可查看本站专题:《JavaScript常用函数技巧汇总》、《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
jquery插件 cluetip 关键词注释
Jan 12 Javascript
按下回车键指向下一个位置的一个函数代码
Mar 10 Javascript
json与jsonp知识小结(推荐)
Aug 16 Javascript
JS中BOM相关知识点总结(必看篇)
Nov 22 Javascript
Bootstrap select多选下拉框实现代码
Dec 23 Javascript
TypeScript学习之强制类型的转换
Dec 27 Javascript
vuejs响应用户事件(如点击事件)
Mar 14 Javascript
JavaScript使用readAsDataURL读取图像文件
May 10 Javascript
Vue 实现前端权限控制的示例代码
Jul 09 Javascript
JavaScript实现模态对话框实例
Jan 13 Javascript
vue 封装 Adminlte3组件的实现
Mar 18 Javascript
vue实现图书管理系统
Dec 29 Vue.js
vue实现鼠标移入移出事件代码实例
Mar 27 #Javascript
JavaScript惰性载入函数实例分析
Mar 27 #Javascript
微信小程序实现获取准确的腾讯定位地址功能示例
Mar 27 #Javascript
详解JS浏览器事件循环机制
Mar 27 #Javascript
详解如何更好的使用module vuex
Mar 27 #Javascript
原生js实现获取form表单数据代码实例
Mar 27 #Javascript
JQueryDOM之样式操作
Mar 27 #jQuery
You might like
php处理文件的小例子(解压缩,删除目录)
2013/02/03 PHP
php无限极分类递归排序实现方法
2014/11/11 PHP
php支付宝在线支付接口开发教程
2016/09/19 PHP
php利用gd库为图片添加水印
2016/11/09 PHP
javascript引用对象的方法
2007/01/11 Javascript
在JavaScript中通过URL传递汉字的方法
2007/04/09 Javascript
javascript数组使用调用方法汇总
2007/12/08 Javascript
jQuery 可以拖动的div实现代码 脚本之家修正版
2009/06/26 Javascript
基于jquery实现拆分姓名的方法(纯JS版)
2013/05/08 Javascript
js中精确计算加法和减法示例
2014/03/28 Javascript
Enter回车切换输入焦点实现思路与代码兼容各大浏览器
2014/09/01 Javascript
jscript读写二进制文件的方法
2015/04/22 Javascript
js淡入淡出的图片轮播效果代码分享
2015/08/24 Javascript
用js制作淘宝放大镜效果
2020/10/28 Javascript
JavaScript 判断对象中是否有某属性的常用方法
2018/06/14 Javascript
vant(ZanUi)结合async-validator实现表单验证的方法
2018/12/06 Javascript
详解vue 不同环境配置不同的打包命令
2019/04/07 Javascript
Vue+webpack实现懒加载过程解析
2020/02/17 Javascript
js实现随机抽奖
2020/03/19 Javascript
Nuxt.js的路由跳转操作(页面跳转nuxt-link)
2020/11/06 Javascript
nodejs中内置模块fs,path常见的用法说明
2020/11/07 NodeJs
Python实现Linux的find命令实例分享
2017/06/04 Python
基于PyQt4和PySide实现输入对话框效果
2019/02/27 Python
Python 中使用 PyMySQL模块操作数据库的方法
2019/11/10 Python
Python log模块logging记录打印用法解析
2020/01/20 Python
Django ModelForm操作及验证方式
2020/03/30 Python
工地资料员岗位职责
2013/12/31 职场文书
党的群众路线教育实践活动个人剖析材料
2014/10/07 职场文书
大班下学期个人总结
2015/02/13 职场文书
2015年公共机构节能宣传周活动总结
2015/03/26 职场文书
小学教师岗位职责
2015/04/02 职场文书
2015年节能减排工作总结
2015/05/14 职场文书
欠款证明
2015/06/24 职场文书
汶川大地震感悟
2015/08/10 职场文书
JavaScript 与 TypeScript之间的联系
2021/11/27 Javascript
CSS元素定位之通过元素的标签或者元素的id、class属性定位详解
2022/09/23 HTML / CSS