《JavaScript 模式》读书笔记(4)— 函数5

  • 2020 年 3 月 30 日
  • 笔记

  这一篇是函数部分的最后一篇。我们来聊聊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!