这一篇是函数部分的最后一篇. 我们来聊聊 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!
来源: https://www.cnblogs.com/zaking/p/12592217.html