大家好,又见面了,我是你们的朋友全栈君。
这一篇是函数部分的最后一篇。我们来聊聊Curry化。
十、Curry
这部分我们主要讨论Curry化和部分函数应用的内容。但是在深入讨论之前,我们需要先了解一下函数应用的含义。
函数应用
在一些纯粹的函数式编程语言中,函数并不描述为被调用(即called或invoked),而是描述为应用(applied)。在JavaScript中,我们可以做同样的事情,使用方法Function.prototype.apply()来应用函数,这是由于JavaScript中的函数实际上是对象,并且它们还具有如下方法。
// 定义函数 var sayHi = function(who) { console.log("Hello" + (who? ", " + who : "") + "!"); }; // 调用函数 sayHi(); // 输出"Hello" sayHi('world'); // 输出"Hello, world!" // 应用函数 sayHi.apply(null, ["hello"]); // 输出"Hello, hello!"
正如上面的例子所看到的,调用(invoking)函数和应用(applying)函数可以得到完全相同的结果。apply()带有两个参数:第一个参数为将要绑定到该函数内部this的一个对象,而第二个参数是一个数组或多个参数变量,这些参数将变成可用于该函数内部的类似数组的arguments对象。如果第一个参数为null(空),那么this将指向全局对象,此时得到的结果就恰好如同调用一个非指定对象时的方法。
当函数是一个对象的方法时,此时不能传递null引用。这种情况下,这里的对象将成为apply()的第一个参数:
// 定义函数 var alien= { sayHi: function(who) { console.log("Hello" + (who? ", " + who : "") + "!"); } } alien.sayHi('world'); // 输出"Hello, world!" sayHi.apply(alien, ["humans"]); // 输出"Hello, humans!"
在上面的代码中,sayHi()内部的this指向了alien对象。而在之前的例子中,this指向了全局对象。
正如上面的两个例子所展示的那样,这些都表明我们考虑的“调用函数”并不只是“句法糖(syntactic sugar)”,而是等价于函数应用。
请注意,除了apply()以外,Function.prototype对象还有一个call()方法,但是这仍然只是建立在apply()之上的语法糖而已。有时候最好使用该语法糖:即当函数仅带有一个参数时,可以根据实际情况避免创建只有一个元素的数组的工作。
// 在这种情况下,第二种更有效率,节省了一个数组 sayHi.apply(alien,["humans"]); sayHi.call(alien,"humans");
部分应用
现在我们知道,调用函数实际上就是将一个参数集合应用到一个函数中,那有没有可能只传递部分参数,而不是所有参数?这种情况就和手动处理一个数学函数所常采用的方法是相似的。假定有一个函数add()用以将两个数字加在一起:x和y。下面的代码片段展示了给定x值为5,且y值为4的情况下的解决方案。
// 出于演示的目的 // 并不是合法的JavaScript function add(x,y) { return x + y; } // 有以下函数 add(5,4); // 第1步,替换一个参数 function add(5, y){ return 5 + y; } // 第2步,替换其他参数 function add(5, 4) { return 5 + 4; }
再提醒一遍,第1、2步的代码是不合法的,仅演示目的。
上面的代码段演示了如何手工解决部分函数应用的问题。可以获取第一个参数的值,并且在整个函数中用已知的值5替代未知的x,然后重复同样的步骤直至用完了所有的参数。
对这个例子中的步骤1可以称为部分应用(partial application),即我们金鹰用了第一个参数。当执行部分应用时,并不会获得结果,相反会获得另一个函数。
下面的代码片段演示了家乡的partialApply()方法的使用示例:
var add = function (x,y) { return x + y; }; // 完全应用 add.apply(null,[5,4]); // 9 // 部分应用 var newadd = add.partialApply(null,[5]); // 应用一个参数到新函数中 newadd.apply(null,[4]); // 9
如上面的代码所示,部分应用向我们提供了另一个新函数,随后再以其他参数调用该函数。这种运行方式实际上与add(5)(4)有一些类似,这是由于add(5)返回了一个可在后来用(4)来调用的函数。
此外,我们所熟悉的add(5, 4)调用方式可能并不像是“句法糖(syntactic sugar)”,相反,使用add(5)(4)才像是“句法糖(syntactic sugar)”。
现在,返回到现实,JavaScript中并没有partialApply()方法和函数,默认情况下也并不会出现与上面类似的行为。但是可以构造出这些函数,因为JavaScript的动态性足够支持这种行为。
使函数理解并处理部分应用的过程就成为Curry过程(Currying)。
Curry化
这里的curry源于数学家Haskell Curry的名字。Curry化是一个转换过程,即我们执行函数转换的过程。那么,我们如何Curry化一个函数?其他的函数式语言可能已经将这种Curry化转换构建到语言本身中,并且所有的函数已经默认转换过,在JavaScript中,可以将add()函数修改成一个用于处理部分应用的Curry化函数。
下面,我们来看个例子:
// curry化的add()函数 // 接受部分参数列表 function add(x,y) { var oldx = x,oldy = y; if(typeof oldy === 'undefined') { // 部分 return function(newy) { return oldx + newy; }; } // 完全应用 return x + y; } // 测试 console.log(typeof add(5)); // 输出“function” add(3)(4); // 7 // 创建并存储一个新函数 var add2000 = add(2000); add2000(19); //输出2010
在上面的代码段中,当第一次调用add()时,它为返回的内部函数创建了一个闭包。该闭包将原始的x和y值存储到私有变量oldx和oldy中。第一个私有变量oldx将在内部函数执行的时候使用。如果没有部分应用,并且同时传递x和y值,该函数则继续执行,并简单将其相加。这种add()实现与实际需求相比显得比较冗长,在这里只是出于演示的目的这样实现。下面将显示一个更为精简的实现版本。其中并没有oldx和oldy,仅是因为原始x隐式的存储在闭包中,并且还将y作为局部变量复用,而不是像之前那样创建一个新的变量newy:
// curry化的add()函数 // 接受部分参数列表 function add(x, y) { if(typeof y === 'undefined') { //部分 return function(y) { return x + y; }; } // 完全应用 return x + y; }
在这些例子中,函数add()本身负责处理部分应用。但是能够以更通用的方式执行相同给的任务么?也就是说,是否可以将任意的函数转换成一个新的可以接收部分参数的函数?
function schonfinkelize(fn) { var slice = Array.prototype.slice, stored_args = slice.call(arguments,1); return function () { var new_args = slice.call(arguments), args = stored_args.concat(new_args); return fn.apply(null,args); } }
schonfinkelize()函数可能不应该有这么复杂,只是由于JavaScript中arguments并不是一个真实的数组。从Array.prototype中借用slice()方法可以帮助我们将arguments变成一个数组,并且使用该数组更加方便。当schonfinkelize()第一次调用时,它存储了一个指向slice()方法的私有引用(名为slice),并且还存储了调用该方法后的参数(存入stored_args中),该方法仅剥离了第一个参数,这是因为第一个参数是将被curry化的函数。然后,schonfinkelize()返回了一个新函数。当这个新函数被调用时,它访问了已经私有存储的参数stored_args以及slice引用。这个新函数必须将原有的部分应用参数(stored_args)合并到新参数(new_args),然后再将它们应用到原始函数fn中(也仅在闭包中私有可用)。
我们来测试下上面的转换方法:
function schonfinkelize(fn) { var slice = Array.prototype.slice, stored_args = slice.call(arguments,1); return function () { var new_args = slice.call(arguments), args = stored_args.concat(new_args); return fn.apply(null,args); } } // 普通函数 function add(x, y){ return x + y; } // 将一个函数curry化并获得一个新的函数 var newadd = schonfinkelize(add,5); console.log(newadd(4)); //输出9 // 另一种选择,直接调用新函数 console.log(schonfinkelize(add,6)(7)); //输出13 // 转换函数并不局限于单个参数或者单步Curry化 // 普通函数 function addSome(a, b, c, d, e) { return a + b + c + d + e; } // 可运行于任意数量的参数 console.log(schonfinkelize(addSome,1,2,3)(5,5)); // 两步curry化 var addOne = schonfinkelize(addSome,1); console.log(addOne(10,10,10,10)); //41 var addSix = schonfinkelize(addOne,2,3); console.log(addSix(5,5)); // 16
上面是完整的例子和测试。
那什么时候适合使用Curry化呢?当发现正在调用同一个函数,并且传递的参数绝大多数都是相同的,那么该函数可能是用于Curry化的一个很好的候选参数。可以通过将一个函数集合部分应用到函数中,从而动态创建一个新函数。这个新函数将会保存重复的参数(因此,不必每次都传递这些参数),并且还会使用预填充原始函数所期望的完整参数列表。
小结
在JavaScript中,有关函数的部分是十分重要的,我们本系列文章相关的主要函数部分已经到此告一段落了。本篇讨论了有关函数的背景和术语。学习了JavaScript中两个重要的特征。即:
- 函数是第一类对象,可以作为带有属性和方法的值以及参数进行传递。
- 函数提供了局部作用域,而其他打括号并不能提供这种局部作用域(当然现在的let是可以的)。此外还需要记住的是,声明的局部变量可被提升到局部作用域的顶部。
创建函数的语法包括:
- 1. 函数命名表达式。
- 2. 函数表达式(与上面的相同,但是缺少一个名字),通常也称为匿名函数。
- 3. 函数声明,与其他语言中的函数的语法类似。
在涵盖了函数的背景和语法之后,我们学习了一些有用的模式:
1、API模式,它们可以帮助您为函数提供更好且更整洁的接口:
回调模式:将函数作为参数进行传递。
配置对象:有助于保持受到控制的函数的参数数量。
返回函数:当一个函数的返回值是另一个函数时。
Curry化:当新函数是基于现有函数,并加上部分参数列表创建时。
2、初始化模式,它们可以帮助您在不污染全局命名空间的情况下,使用临时变量以一种更加整洁、结构化的方式执行初始化以及设置任务(当涉及web网页和应用程序时是非常普遍的)。这些模式包括:
即时函数:只要定义之后就立即执行。
即时对象初始化:匿名对象组织了初始化任务,提供了可被立即调用的方法。
初始化时分支:帮助分支代码在初始化代码执行过程中仅检测一次,这与以后在程序生命周期内多次检测相反。
3、性能模式,可以帮助加速代码运行,这些模式包括:
备忘模式:使用函数属性以便使得计算过的值无须再次计算。
自定义模式:以新的主体重写本身,以使得在第二次或以后调用时仅需执行更少的工作。
好了,函数部分到此结束了。我们下面会开始学习对象模式部分。加油!fighting!
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/124389.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...