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 相关文章推荐
JS Excel读取和写入操作(模板操作)实现代码
Apr 11 Javascript
关于全局变量和局部变量的那些事
Jan 11 Javascript
js数组循环遍历数组内所有元素的方法
Jan 18 Javascript
简介JavaScript中Boolean.toSource()方法的使用
Jun 05 Javascript
JS实现网页顶部向下滑出的全国城市切换导航效果
Aug 22 Javascript
JavaScript获取当前url根目录(路径)
Jun 17 Javascript
jquery利用json实现页面之间传值的实例解析
Dec 12 Javascript
Bootstrap警告(Alerts)的实现方法
Mar 22 Javascript
Node.js学习之查询字符串解析querystring详解
Sep 28 Javascript
利用babel将es6语法转es5的简单示例
Dec 01 Javascript
vue 登录滑动验证实现代码
Aug 24 Javascript
谈谈JavaScript令人迷惑的==与+
Aug 31 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 数据结构 算法描述 冒泡排序 bubble sort
2011/07/10 PHP
php+mysqli实现将数据库中一张表信息打印到表格里的方法
2015/01/28 PHP
PHP使用PDO操作数据库的乱码问题解决方法
2016/04/08 PHP
详解WordPress中添加友情链接的方法
2016/05/21 PHP
PHP获取当前时间不准确问题解决方案
2020/08/14 PHP
jQuery构造函数init参数分析
2015/05/13 Javascript
JS与HTML结合使用marquee标签实现无缝滚动效果代码
2016/07/05 Javascript
jQuery的ready方法实现原理分析
2016/10/26 Javascript
JavaScript如何实现图片懒加载(lazyload) 提高用户体验(增强版)
2016/11/30 Javascript
深入理解jQuery.data() 的实现方式
2016/11/30 Javascript
浅谈Angular2 ng-content 指令在组件中嵌入内容
2017/08/18 Javascript
Vue路由钩子之afterEach beforeEach的区别详解
2018/07/15 Javascript
原生js封装的ajax方法示例
2018/08/02 Javascript
详解nuxt路由鉴权(express模板)
2018/11/21 Javascript
JQuery animate动画应用示例
2019/05/14 jQuery
[01:04:09]DOTA2-DPC中国联赛 正赛 iG vs VG BO3 第二场 2月2日
2021/03/11 DOTA
Python程序员鲜为人知但你应该知道的17个问题
2014/06/04 Python
Python扫描IP段查看指定端口是否开放的方法
2015/06/09 Python
总结python爬虫抓站的实用技巧
2016/08/09 Python
python list转矩阵的实例讲解
2018/08/04 Python
pyhanlp安装介绍和简单应用
2019/02/22 Python
Django组件content-type使用方法详解
2019/07/19 Python
pip 安装库比较慢的解决方法(国内镜像)
2019/10/06 Python
Python基于time模块表示时间常用方法
2020/06/18 Python
美国领先的户外服装与装备用品店:Moosejaw
2016/08/25 全球购物
宝拉珍选美国官网:Paula’s Choice美国
2018/01/07 全球购物
美丽的现代设计家具:2Modern
2018/07/26 全球购物
2019年分享net面试的经历和题目
2016/08/07 面试题
会计学财务管理专业个人的自我评价
2013/10/19 职场文书
护理专业的自荐信
2013/10/22 职场文书
师范大学应届生求职信
2013/11/21 职场文书
医学院校毕业生自荐信范文
2014/01/01 职场文书
农民工创业典型事迹
2014/01/25 职场文书
2015年电工工作总结
2015/04/10 职场文书
电影开国大典观后感
2015/06/04 职场文书
postman中form-data、x-www-form-urlencoded、raw、binary的区别介绍
2022/01/18 HTML / CSS