六、函数式编程

函数式编程是一种不同于我们目前所关注的面向对象的方法的开发方法。 面向对象编程是解决大量问题的神奇工具,但它也存在一些问题。 在面向对象上下文中进行并行编程是很困难的,因为状态可能会被各种不同的线程更改,而这些线程的副作用是未知的。 函数式编程不允许状态或可变变量。 函数在函数式编程中充当主要的构建块。 以前可能使用变量的地方现在将使用函数。

即使在单线程程序中,函数也可能有改变全局状态的副作用。 这意味着,当调用一个未知函数时,它可以改变程序的整个流程。 这使得程序调试相当困难。

JavaScript 不是函数式编程语言,但我们仍然可以将一些函数式原则应用到代码中。 我们将看看功能空间中的一些模式:

  • 函数传递
  • 过滤器和管道
  • 蓄电池
  • 记忆有关
  • 不变性
  • 延迟实例化

功能功能无副作用

函数式编程的一个核心特点是函数不应该改变状态。 可以设置函数局部的值,但函数外部的值不能改变。 这种方法对于提高代码的可维护性非常有用。 不再需要担心将数组传递给函数会对其内容造成破坏。 当使用不在您控制范围内的库时,这尤其值得关注。

JavaScript 中没有阻止你改变全局状态的机制。 相反,您必须依赖开发人员编写无副作用的函数。 这可能是困难的,也可能不是,取决于团队的成熟度。

将应用中的所有代码放入函数中可能并不可取,但是尽可能地分离是可取的。 有一种叫做命令查询分离的模式,它建议方法应该分为两类。 它们要么是一个读取值的函数,要么是一个设置值的命令。 两个人永远不应该见面。 这样对方法进行分类,可以简化调试和代码重用。

无副作用函数的一个结果是,它们可以以相同的输入被调用任意次数,结果也将是相同的。 此外,由于没有对状态进行更改,多次调用函数不会导致任何不良副作用,只会使其运行速度变慢。

功能传递

在函数式编程语言中,函数是头等公民。 函数可以被赋值给变量并传递,就像你对待其他变量一样。 这并不是一个完全陌生的概念。 甚至像 C 这样的语言也有函数指针,可以像处理其他变量一样处理。 c#有委托,在最近的版本中还有 lambda。 Java 的最新版本还增加了对 lambdas 的支持,因为事实证明它们非常有用。

JavaScript 允许函数被当作变量,甚至对象和字符串来处理。 这样,JavaScript 本质上就是函数式的。

由于 JavaScript 的单线程特性,回调是一种常见的约定,你几乎可以在任何地方找到它们。 考虑以后在网页上调用一个函数。 这是通过在窗口对象上设置一个超时来实现的,如下所示:

setTimeout(function(){alert("Hello from the past")}, 5 * 1000);

设置的 timeout 函数的参数是要调用的函数和延迟时间(以毫秒为单位)。

不管您在哪个 JavaScript 环境中工作,都几乎不可能避免以回调形式出现的函数。 Node.js 的异步处理模型高度依赖于能够调用一个函数并传入一些稍后要完成的内容。 在浏览器中调用外部资源也依赖于回调来通知调用者某些异步操作已经完成。 在基本的 JavaScript 中如下所示:

let xmlhttp = new XMLHttpRequest()
xmlhttp.onreadystatechange = function()
if (xmlhttp.readyState==4 && xmlhttp.status==200){
  //process returned data
}
};
xmlhttp.open("GET", http://some.external.resource, true);
xmlhttp.send();

您可能注意到,我们甚至在发送请求之前就分配了onreadystatechange函数。 这是因为稍后赋值可能会导致竞态条件,即服务器在将函数附加到就绪状态更改之前作出响应。 在本例中,我们使用内联函数来处理返回的数据。 因为函数是第一类公民,我们可以将其更改为如下所示:

let xmlhttp;
function requestData(){
  xmlhttp = new XMLHttpRequest()
  xmlhttp.onreadystatechange=processData;
  xmlhttp.open("GET", http://some.external.resource, true);
  xmlhttp.send();
}

function processData(){
  if (xmlhttp.readyState==4 &&xmlhttp.status==200){
    //process returned data
  }
}

这通常是一种更干净的方法,并避免按照另一个函数执行复杂的处理。

然而,你可能更熟悉这个 jQuery 版本,它看起来像以下内容:

$.getJSON('http://some.external.resource', function(json){
  //process returned data
});

在这种情况下,处理就绪状态更改的锅炉板将为您处理。 如果你的数据请求失败,我们甚至会为你提供方便:

$.ajax('http://some.external.resource',
  { success: function(json){
      //process returned data
    },
    error: function(){
      //process failure
    },
    dataType: "json"
});

在本例中,我们将一个对象传递给了ajax调用,该调用定义了一些属性。 在这些属性中有用于成功和失败的函数回调。 这种将大量函数传递给另一个函数的方法是为类提供扩展点的好方法。

很可能你之前在没有意识到的情况下就看到过这个模式。 将函数作为 options 对象的一部分传递给构造函数是在 JavaScript 库中提供扩展钩子的常用方法。 我们在前面的章节第 5 章行为模式中看到了对函数的处理,当将函数传递给观察者时。

实施

在维斯特洛,旅游业几乎不存在。 土匪杀害游客和游客卷入地区冲突是非常困难的。 尽管如此,一些有胆识的人已经开始宣传免费的维斯特洛之旅,他们将带着那些有能力的人游览所有主要景点。 从君临到鹰巢城,再到多恩的群山,行程将涵盖这一切。 事实上,旅游委员会一位颇有数学倾向的成员称这是一次汉密尔顿式的旅行,因为它到任何地方都只访问一次。

类提供了一个选项对象,它允许定义一个选项对象。 这个对象包含可以附加回调的各个位置。 在我们的例子中,它的接口看起来像下面这样:

export class HamiltonianTourOptions{
  onTourStart: Function;
  onEntryToAttraction: Function;
  onExitFromAttraction: Function;
  onTourCompletion: Function;
}

完整的HamiltonianTour类如下所示:

class HamiltonianTour {
  constructor(options) {
    this.options = options;
  }
  StartTour() {
    if (this.options.onTourStart && typeof (this.options.onTourStart) === "function")
      this.options.onTourStart();
      this.VisitAttraction("King's Landing");
      this.VisitAttraction("Winterfell");
      this.VisitAttraction("Mountains of Dorne");
      this.VisitAttraction("Eyrie");
    if (this.options.onTourCompletion && typeof (this.options.onTourCompletion) === "function")
      this.options.onTourCompletion();
  }
  VisitAttraction(AttractionName) {
    if (this.options.onEntryToAttraction && typeof (this.options.onEntryToAttraction) === "function")
      this.options.onEntryToAttraction(AttractionName);
      //do whatever one does in a Attraction
    if (this.options.onExitFromAttraction && typeof (this.options.onExitFromAttraction) === "function")
      this.options.onExitFromAttraction(AttractionName);
  }
}

您可以在突出显示的代码中看到我们如何检查选项,然后根据需要执行回调。 这可以通过简单地做以下操作来使用:

var tour = new HamiltonianTour({
  onEntryToAttraction: function(cityname){console.log("I'm delighted to be in " + cityname)}});
      tour.StartTour();

运行这段代码的输出如下:

I'm delighted to be in King's Landing
I'm delighted to be in Winterfell
I'm delighted to be in Mountains of Dorne
I'm delighted to be in Eyrie

传递函数是解决 JavaScript 中许多问题的一种很好的方法,往往被 jQuery 等库和 express 等框架广泛使用。 它是如此普遍地被采用,以至于使用它为代码的可读性增加了障碍。

过滤器和管道

如果您熟悉 Unix 命令行,或者熟悉 Windows 命令行,那么您可能已经使用过管道了。 由|字符表示的管道是“接受程序 A 的输出并将其放入程序 B”的简写。 这个相对简单的想法使 Unix 命令行变得异常强大。 例如,如果您想列出一个目录中的所有文件,然后对它们进行排序,并过滤任何以字母bg开头并以f结尾的文件,那么命令可能如下所示:

ls|sort|grep "^[gb].*f$"

ls命令列出所有文件和目录,sort命令对它们进行排序,grep命令根据正则表达式匹配文件名。 在/etc的 Ubuntu 框的etc目录下运行此命令会得到如下结果:

stimms@ubuntu1:/etc$ ls|sort|grep "^[gb].*f$"

blkid.conf
bogofilter.cf
brltty.conf
gai.conf
gconf
groff
gssapi_mech.conf

一些函数式编程语言(如 f#)为函数之间的管道提供了特殊的语法。 在 f#中,过滤列表中的偶数可以通过以下方式完成:

[1..10] |>List.filter (fun n -> n% 2 = 0);;

这种语法非常好看,特别是用于长函数链时。 举个例子,取一个数字,将其转换为浮点数,将其平方,然后四舍五入,如下所示:

10.5 |> float |>Math.Sqrt |>Math.Round

这是一个比 c 风格语法更清晰的语法,看起来更像以下:

Math.Round(Math.Sqrt((float)10.5))

不幸的是,没有能力使用漂亮的 f#风格语法在 JavaScript 中编写管道,但我们仍然可以通过使用方法链接来改进前面代码中所示的普通方法。

JavaScript 中的所有东西都是对象,这意味着我们可以为现有对象添加一些真正有趣的功能,以改善它们的外观。 对对象集合的操作是函数式编程提供一些强大功能的领域。 让我们从向数组对象添加一个简单的过滤方法开始。 您可以将这些查询视为以函数式方式编写的 SQL 数据库查询。

实施

我们希望提供一个函数,对数组中的每个成员执行匹配,并返回一组结果:

Array.prototype.where = function (inclusionTest) {
  let results = [];
  for (let i = 0; i<this.length; i++) {
    if (inclusionTest(this[i]))
      results.push(this[i]);
  }
  return results;
};

这个看起来相当简单的函数允许我们快速过滤数组:

var items = [1,2,3,4,5,6,7,8,9,10];
items.where(function(thing){ return thing % 2 ==0;});

我们返回的也是一个对象,在这里是一个数组对象。 我们可以继续像下面这样将方法链接到它上:

items.where(function(thing){ return thing % 2 ==0;})
  .where(function(thing){ return thing % 3 == 0;});

这样做的结果是一个只包含数字 6 的数组,因为它是 1 到 10 之间唯一的是偶数且能被 3 整除的数字。 这种返回原始对象的修改版本而不更改原始对象的方法称为流畅接口。 通过不改变原始的 item 数组,我们为变量引入了少量的不变性。

如果我们在数组扩展库中添加另一个函数,就可以看到这些管道有多有用了:

Array.prototype.select=function(projection){
  let results = [];
  for(let i = 0; i<this.length;i++){
    results.push(projection(this[i]));
  }
  return results;
};

这种扩展允许基于任意投影函数的原始项目的投影。 给定一组包含 id 和名称的对象,我们可以使用我们的 fluent 数组扩展来执行复杂的操作:

let children = [{ id: 1, Name: "Rob" },
{ id: 2, Name: "Sansa" },
{ id: 3, Name: "Arya" },
{ id: 4, Name: "Brandon" },
{ id: 5, Name: "Rickon" }];
let filteredChildren = children.where(function (x) {
  return x.id % 2 == 0;
}).select(function (x) {
  return x.Name;
});

这段代码将构建一个新的数组,它只包含偶数 id 的子对象,而不是完整的对象,数组将只包含它们的名称:SansaBrandon。 对于那些熟悉。net 的人来说,这些函数看起来可能非常熟悉。 . net 上的Language Integrated Queries(LINQ)库提供了类似命名的受函数启发的函数,用于操作集合。

与其他方法相比,以这种方式链接函数更容易理解,也更容易构建:避免了临时变量,代码更简洁。 考虑前面的例子,使用循环和临时变量重新实现:

let children = [{ id: 1, Name: "Rob" },
{ id: 2, Name: "Sansa" },
{ id: 3, Name: "Arya" },
{ id: 4, Name: "Brandon" },
{ id: 5, Name: "Rickon" }];
let evenIds = [];
for(let i=0; i<children.length;i++)
{
  if(children[i].id%2==0)
    evenIds.push(children[i]);
}
let names = [];
for(let i=0; i< evenIds.length;i++)
{
  names.push(evenIds[i].name);
}

许多 JavaScript 库(如 d3)都是为了鼓励这种编程而构建的。 乍一看,按照这种约定创建的代码似乎很糟糕,因为行很长。 我认为这是行长度的函数,不是衡量复杂性的好工具,而不是方法的实际问题。

蓄电池

我们已经了解了一些简单的数组函数,它们为数组添加了过滤和管道。 另一个有用的工具是蓄电池。 累加器通过在集合上迭代来帮助构建单个结果。 许多常见的操作,比如对数组元素求和,都可以使用累加器而不是循环来实现。

递归在函数式编程语言中很流行,其中许多语言实际上提供了一种称为“尾部调用优化”的优化。 支持此功能的语言为重用堆栈框架的递归函数提供了优化。 这是非常有效的,可以很容易地替换大多数循环。 关于任何 JavaScript 解释器是否支持尾部调用优化的细节是粗略的。 在大多数情况下,它看起来不是这样的但我们仍然可以使用递归。

for循环的问题是,通过循环的控制流是可变的。 这是一个很容易犯的错误:

let result = "";
let multiArray = [[1,2,3], ["a", "b", "c"]];
for(vari=0; i<multiArray.length; i++)
  for(var j=0; i<multiArray[i].length; j++)
    result += multiArray[i][j];

你发现错误了吗? 我尝试了好几次,才找到可以破解的代码的工作版本。 问题在第二个循环的循环计数器中,它应该读如下:

let result = "";
let multiArray = [[1,2,3], ["a", "b", "c"]];
for(let i=0; i<multiArray.length; i++)
  for(let j=0; j<multiArray[i].length; j++)
    result +=multiArray[i][j];

显然,通过更好的变量命名可以在一定程度上缓解这一问题,但我们希望完全避免这个问题。

相反,我们可以使用累加器,这是一种将集合中的多个值组合为单个值的工具。 我们已经错过了维斯特洛的一些模式,所以让我们回到我们的神话例子。 战争需要大量的金钱,但幸运的是,有大量的农民支付税收,并资助领主在他们的游戏王位。

实施

我们的农民被一个简单的模型所代表,如下所示:

let peasants = [
  {name: "Jory Cassel", taxesOwed: 11, bankBalance: 50},
  {name: "VardisEgen", taxesOwed: 15, bankBalance: 20}];

在这组农民中,我们有一个累加器,它看起来如下所示:

TaxCollector.prototype.collect = function (items, value, projection) {
  if (items.length> 1)
    return projection(items[0]) + this.collect(items.slice(1), value, projection);
  return projection(items[0]);
};

此代码接受项目列表、累加器值和一个函数,该函数投射要集成到累加中的值。

投影函数是这样的:

function (item) {
  return Math.min(item.moneyOwed, item.bankBalance);
}

为了给这个函数加素数,我们只需要传入一个累加器的初始值以及数组和投影。 启动值会变化,但通常是一个恒等式; 字符串累加器为空字符串,数学累加器为 0 或 1。

每次遍历累加器都会缩小正在操作的数组的大小。 所有这些都是在没有单个可变变量的情况下完成的。

内部累加实际上可以是任何你喜欢的函数:字符串追加、加法或更复杂的东西。 累加器有点像访问器模式,只是在累加器内部修改集合中的值是不允许的。 记住函数式编程是没有副作用的。

记忆

不要与记忆混淆,记忆是一个特定的术语,用来保留函数中先前计算的数值。

正如我们前面看到的,可以多次调用无副作用的函数而不会引起问题。 由此推论,函数的调用次数也可以少于需要的次数。 考虑一个昂贵的函数,它执行一些复杂的或者至少是耗时的数学运算。 我们知道函数的结果完全取决于函数的输入。 所以相同的输入总是会产生相同的输出。 那么,为什么我们需要多次调用该函数呢? 如果我们保存函数的输出,我们可以检索它,而不是重新做耗时的计算。

用空间换时间是一个经典的计算科学问题。 通过缓存结果,我们使应用更快,但会消耗更多内存。 决定什么时候执行缓存,什么时候简单地重新计算结果是一个困难的问题。

实施

在维斯特洛大陆上,有学问的人,被称为学士,长期以来一直对自然世界中经常出现的数字序列着迷。 奇怪的巧合是,他们称这个序列为斐波那契序列。 它是通过将序列中的前两项相加得到下一项来定义的。 序列通过将前几项定义为 0,1,1 来引导。 为了得到下一项我们只要把 1 加 1 得到 2。 下一项是 2 加 1 得到 3,以此类推。 寻找序列中的任意成员需要找到前两个成员,因此可能需要进行一些计算。

在我们的世界里,我们已经发现了一个封闭的形式,它避免了很多这样的计算,但在维斯特洛没有这样的发现。

naïve 的方法是简单地计算每个术语,如下所示:

let Fibonacci = (function () {
  function Fibonacci() {
  }
  Fibonacci.prototype.NaieveFib = function (n) {
    if (n == 0)
      return 0;
    if (n <= 2)
      return 1;
    return this.NaieveFib(n - 1) + this.NaieveFib(n - 2);
  };
  return Fibonacci;
})();

对于像 10 这样的小数字,这个解决方案工作得非常快。 然而,对于更大的数字,比如超过 40 个,则会出现大幅放缓。 这是因为基本情况被调用了 102,334,155 次。

让我们看看我们是否可以通过记住一些值来改进:

let Fibonacci = (function () {
  function Fibonacci() {
    this.memoizedValues = [];
  }

  Fibonacci.prototype.MemetoFib = function (n) {
    if (n == 0)
      return 0;
    if (n <= 2)
      return 1;
    if (!this. memoizedValues[n])
      this. memoizedValues[n] = this.MemetoFib(n - 1) + this.MemetoFib(n - 2);
    return this. memoizedValues[n];
  };
  return Fibonacci;
})();

我们刚刚记住了我们遇到的每一个项目。 结果证明,对于这个算法,我们存储n+1项,这是一个很好的权衡。 在没有记忆的情况下,计算第 40 个斐波那契数需要 963 毫秒,而记忆版本只需要 11 毫秒。 当函数的计算变得更加复杂时,这种差异就会更加明显。 140 的斐波那契值在 memoization 版本中花了 12 毫秒,而 naïve 版本花了……好吧,它已经用了一天了,现在还在运行。

这种记忆的最好部分是,对具有相同参数的函数的后续调用将以闪电般的速度进行,因为结果已经计算出来了。

在我们的示例中,只需要很小的缓存。 在更复杂的例子中,很难知道缓存应该有多大,或者需要重新计算一个值的频率。 理想情况下,缓存应该足够大,这样总有足够的空间放入更多的结果。 然而,这可能不现实,需要做出艰难的决定,即应该删除缓存中的哪些成员以节省空间。 有很多方法可以执行缓存失效。 有人说缓存失效是计算科学中最棘手的问题之一,原因是我们在有效地预测未来。 如果有人有一种完美的预知未来的方法,那么很可能他们将自己的技能应用到了比缓存失效更重要的领域。 两种选择是利用最近使用最少的缓存成员或使用最少的缓存成员。 问题的形状可能决定了更好的策略。

记忆是一个极好的工具,可以加速需要执行多次的计算,甚至是有公共子计算的计算。 可以把记忆看作是缓存的一种特殊情况,缓存是构建 web 服务器或浏览器时常用的技术。 在更复杂的 JavaScript 应用中,这当然是值得探索的。

不变性

函数式编程的基石之一是所谓的变量只能被赋值一次。 这就是所谓的不变性。 ECMAScript 2015 支持一个新的关键字const。 关键字const的使用方式与var相同,只是使用const赋值的变量是不可变的。 例如,下面的代码显示了一个变量和一个常量,它们都是以相同的方式操作的:

let numberOfQueens = 1;
const numberOfKings = 1;
numberOfQueens++;
numberOfKings++;
console.log(numberOfQueens);
console.log(numberOfKings);

运行此命令的输出如下:

2
1

如您所见,常量和变量的结果是不同的。

如果你使用的是没有支持的旧浏览器,那么const对你来说是不可用的。 一个可能的解决方案是利用更广泛采用的Object.freeze功能:

let consts = Object.freeze({ pi : 3.141});
consts.pi = 7;
console.log(consts.pi);//outputs 3.141

如您所见,这里的语法不是很友好。 还有一个问题是,尝试分配一个已经分配的const只会默默地失败,而不是抛出一个错误。 以这种方式默默失败根本不是一种可取的行为; 应该抛出一个完整的异常。 如果你启用了严格模式,ECMAScript 5 会添加一个更严格的解析模式,并抛出一个异常:

"use strict";
var consts = Object.freeze({ pi : 3.141});
consts.pi = 7;

前面的代码将抛出以下错误:

consts.pi = 7;
          ^
TypeError: Cannot assign to read only property 'pi' of #<Object>

另一种方法是我们前面提到的object.Create语法。 当在对象上创建属性时,可以指定writable: false使属性不可变:

var t = Object.create(Object.prototype,
{ value: { writable: false,
  value: 10}
});
t.value = 7;
console.log(t.value);//prints 10

然而,即使在严格模式中,当试图写入不可写属性时也不会引发异常。 因此,我认为const关键字对于实现不可变对象并不完美。 你最好使用冷冻。

惰性实例化

如果你走进一家高端的咖啡店,点了一些过于复杂的饮料(大杯印度茶拿铁,3 水泵,脱脂牛奶,淡水,无泡沫,特热的饮料,有人吗?),那么饮料将是即时制作的,而不是提前制作的。 即使咖啡店知道当天会有哪些订单,他们也不会提前做好所有的饮料。 首先,因为这会导致大量的冷饮被毁。其次,如果第一个顾客要等待当天所有的订单完成,那么他们得到订单的时间就会很长。

取而代之的是,咖啡店采用准时制的方法来制作饮料。 他们点的时候才做。 通过使用一种称为延迟实例化或延迟初始化的技术,我们可以对代码应用类似的方法。

考虑一个创建开销很大的对象; 也就是说,创建对象需要大量的时间。 如果我们不确定是否需要对象的值,我们可以将其完整的创建推迟到以后。

实施

让我们进入一个的例子。 维斯特洛不太喜欢昂贵的咖啡店,但他们喜欢好的面包店。 这家面包店会提前收到不同种类的面包的订单,一旦接到订单,就会立即全部烘焙。 然而,创建面包对象是一个昂贵的操作,所以我们想要推迟,直到有人真的来取面包:

class Bakery {
  constructor() {
    this.requiredBreads = [];
  }
  orderBreadType(breadType) {
    this.requiredBreads.push(breadType);
  }
}

我们开始创建一个列表的面包类型要创建的需要。 这个列表通过订购面包类型被附加到:

var Bakery = (function () {
  function Bakery() {
    this.requiredBreads = [];
  }
  Bakery.prototype.orderBreadType = function (breadType) {
    this.requiredBreads.push(breadType);
  };

这使得面包可以迅速添加到所需的面包清单中,而不必为每一个面包的制作支付价格。

现在,当pickUpBread被调用时,我们实际上会制作面包:

pickUpBread(breadType) {
  console.log("Picup of bread " + breadType + " requested");
  if (!this.breads) {
    this.createBreads();
  }
  for (var i = 0; i < this.breads.length; i++) {
    if (this.breads[i].breadType == breadType)
      return this.breads[i];
  }
}
createBreads() {
  this.breads = [];
  for (var i = 0; i < this.requiredBreads.length; i++) {
    this.breads.push(new Bread(this.requiredBreads[i]));
  }
}

这里我们称之为一系列操作:

let bakery = new Westeros.FoodSuppliers.Bakery();
bakery.orderBreadType("Brioche");
bakery.orderBreadType("Anadama bread");
bakery.orderBreadType("Chapati");
bakery.orderBreadType("Focaccia");

console.log(bakery.pickUpBread("Brioche").breadType + "picked up");

这将导致以下的:

Pickup of bread Brioche requested.
Bread Brioche created.
Bread Anadama bread created.
Bread Chapati created.
Bread Focaccia created.
Brioche picked up

您可以看到,实际面包的收集被保留到被要求领取后。

惰性实例化可以用来简化异步编程。 承诺是一种简化 JavaScript 中常见回调的方法。 承诺不是构建复杂的回调函数,而是一个包含状态和结果的对象。 当第一次呼唤时,承诺处于未解决的状态; 一旦async操作完成,状态更新为 complete,并填写结果。 您可以将结果看作是惰性实例化的。 我们将在第 9 章Web Patterns中详细介绍 promise 和 promise 库。

懒惰可以为您节省大量时间,以创建昂贵的对象,而这些对象最终都不会被使用。

提示和提示

尽管回调是 JavaScript 中处理异步方法的标准方法,但它们很容易失控。 有很多方法可以解决这种意大利式的代码:promise 库提供了一种更流畅的方法来处理回调,未来的 JavaScript 版本可能会采用类似于 c#async/await语法的方法。

我非常喜欢累加器,但它们在内存使用方面效率很低。 缺少尾部递归意味着每次通过都要添加另一个堆栈帧,因此这种方法可能会导致内存压力。 在这种情况下,所有事情都是内存和代码可维护性之间的权衡。

小结

JavaScript 不是函数式编程语言。 这并不是说不可能将函数式编程中的一些思想应用到它。 这些方法使代码更清晰、更容易调试。 有些人甚至可能会说,问题的数量将会减少,尽管我从未见过任何令人信服的研究。

在本章中,我们研究了六种不同的模式。 惰性实例化、记忆化和不变性都是创建模式。 函数传递是一种结构模式,也是一种行为模式。 累加器在本质上也是行为的。 过滤器和管道实际上并不属于任何 GoF 类别,所以人们可能会把它们看作一种样式模式。

在下一章中,我们将讨论在应用中划分逻辑和表示的一些模式。 随着 JavaScript 应用的发展,这些模式变得越来越重要。