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 相关文章推荐
解密效果
Jun 23 Javascript
Div自动滚动到末尾的代码
Oct 26 Javascript
multiSteps 基于Jquery的多步骤滑动切换插件
Jul 22 Javascript
showModalDialog模态对话框的使用详解以及浏览器兼容
Jan 11 Javascript
javascript生成随机数方法汇总
Nov 12 Javascript
BootStrap glyphicon图标无法显示的解决方法
Sep 06 Javascript
vue3.0 搭建项目总结(详细步骤)
May 20 Javascript
微信小程序webview 脚手架使用详解
Jul 22 Javascript
详解基于原生JS验证表单组件xy-form
Aug 20 Javascript
JavaScript 截取字符串代码实例
Sep 05 Javascript
vue组件系列之TagsInput详解
May 14 Javascript
JavaScript获取URL参数的方法分享
Apr 07 Javascript
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自定义函数收代码
2010/08/01 PHP
PHP的异常处理类Exception的使用及说明
2012/06/13 PHP
PHP输出时间差函数代码
2013/01/28 PHP
php in_array() 检查数组中是否存在某个值详解
2016/11/23 PHP
经常用的图片在容器中的水平垂直居中实例
2007/06/10 Javascript
JavaScript 三种创建对象的方法
2009/10/16 Javascript
从数据结构的角度分析 for each in 比 for in 快的多
2013/07/07 Javascript
我的Node.js学习之路(一)
2014/07/06 Javascript
处理文本部分内容的TextRange对象应用实例
2014/07/29 Javascript
AngularJS 使用 UI Router 实现表单向导
2016/01/29 Javascript
javascript面向对象程序设计高级特性经典教程(值得收藏)
2016/05/19 Javascript
轻松实现jquery选项卡切换效果
2016/10/10 Javascript
Javascript别踩白块儿(钢琴块儿)小游戏实现代码
2017/07/20 Javascript
vuejs+element-ui+laravel5.4上传文件的示例代码
2017/08/12 Javascript
Vue引入jquery实现平滑滚动到指定位置
2018/05/09 jQuery
angular6.0使用教程之父组件通过url传递id给子组件的方法
2018/06/30 Javascript
vue使用video.js进行视频播放功能
2019/07/18 Javascript
jquery实现的分页显示功能示例
2019/08/23 jQuery
原生js实现滑块区间组件
2021/01/20 Javascript
[26:50]2018完美盛典DOTA2表演赛
2018/12/17 DOTA
Python实现Kmeans聚类算法
2020/06/10 Python
python日期时间转为字符串或者格式化输出的实例
2018/05/29 Python
python使用tornado实现简单爬虫
2018/07/28 Python
Python使用Flask-SQLAlchemy连接数据库操作示例
2018/08/31 Python
python根据多个文件名批量查找文件
2019/08/13 Python
CSS3教程(6):创建网站多列
2009/04/02 HTML / CSS
纯CSS3实现自定义Tooltip边框涂鸦风格的教程
2014/11/05 HTML / CSS
HTML5+CSS3应用详解
2014/02/24 HTML / CSS
澳大利亚香水在线:Price Rite Mart
2017/12/28 全球购物
Skyscanner加拿大:全球旅行搜索平台
2018/11/19 全球购物
公司董事长助理工作职责
2014/07/12 职场文书
村级个人对照检查材料
2014/08/22 职场文书
创先争优承诺书
2015/01/20 职场文书
2015年组织部工作总结
2015/04/03 职场文书
导游词之广西漓江
2019/11/02 职场文书
Windows server 2012 配置Telnet以及用法详解
2022/04/28 Servers