JavaScript-函数式编程-全-

JavaScript 函数式编程(全)

原文:zh.annas-archive.org/md5/14CAB13674AB79FC040D2749FA52D757

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

函数式编程是一种强调和使智能化代码编写的风格,可以最大程度地减少复杂性并增加模块化。这是一种通过巧妙地改变、组合和使用函数来编写更清洁的代码的方式。JavaScript 为这种方法提供了一个极好的媒介。互联网的脚本语言 JavaScript 实际上是一种本质上的函数式语言。通过学习如何暴露它作为函数式语言的真实身份,我们可以实现功能强大、更易于维护和更可靠的 Web 应用程序。通过这样做,JavaScript 的奇怪习惯和陷阱将突然变得清晰,整个语言将变得更加有意义。学习如何使用函数式编程将使您成为终身更好的程序员。

本书是为对学习函数式编程感兴趣的新老 JavaScript 开发人员而编写的指南。本书侧重于函数式编程技术、风格的发展以及 JavaScript 库的详细信息,将帮助您编写更智能的代码并成为更好的程序员。

本书涵盖的内容

第一章, JavaScript 函数式一面的力量-演示,通过使用传统方法和函数式编程来创建一个小型 Web 应用程序来开启本书的节奏。然后比较这两种方法,以突出函数式编程的重要性。

第二章, 函数式编程基础,向您介绍了函数式编程的核心概念以及内置的 JavaScript 函数。

第三章, 设置函数式编程环境,探讨了不同的 JavaScript 库以及它们如何优化用于函数式编程。

第四章, 在 JavaScript 中实现函数式编程技术,解释了 JavaScript 中的函数式范式。它涵盖了几种函数式编程风格,并演示了它们如何在不同场景中使用。

第五章, 范畴论,详细解释了范畴论的概念,然后在 JavaScript 中实现它。

第六章, JavaScript 中的高级主题和陷阱,强调了在 JavaScript 编程中可能遇到的各种缺点,以及成功处理它们的各种方法。

第七章, JavaScript 中的函数式和面向对象编程,将函数式编程和面向对象编程与 JavaScript 联系起来,并向您展示这两种范式如何相辅相成并共存。

附录 A, JavaScript 中函数式编程的常用函数,包含了在 JavaScript 中执行函数式编程所使用的常用函数。

附录 B, 术语表,包括本书中使用的术语表。

本书需要什么

只需要一个浏览器就可以让您立即开始运行。

本书适合谁

如果您是一名对学习函数式编程感兴趣的 JavaScript 开发人员,希望在掌握 JavaScript 语言方面迈出一大步,或者只是想成为一名更好的程序员,那么这本书非常适合您。本指南旨在面向开发响应式前端应用程序、处理可靠性和并发性的服务器端应用程序以及其他各种应用程序的程序员。

约定

在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:"我们可以通过使用include指令包含其他上下文。"

代码块设置如下:

Function.prototype.partialApply = function() {
  var func = this;
  args = Array.prototype.slice.call(arguments);
  return function() {
    return func.apply(this, args.concat(
      Array.prototype.slice.call(arguments)
    ));
  };
};

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

var messages = ['Hi', 'Hello', 'Sup', 'Hey', 'Hola'];
messages.map(function(s,i){
  return **printSomewhere**(s, i*10, i*10);
}).forEach(document.body.appendChild);

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:"单击下一步按钮将您移至下一个屏幕。"

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会以这种方式出现。

第一章:JavaScript 函数式编程的力量-演示

介绍

几十年来,函数式编程一直是计算机科学爱好者的宠儿,因其数学纯粹性和令人费解的特性而备受推崇,这使它隐藏在数据科学家和博士候选人占据的尘封的计算机实验室中。但现在,它正在复兴,这要归功于现代语言,如PythonJuliaRubyClojure和——最后但并非最不重要的——JavaScipt

你说 JavaScript?网络的脚本语言?是的!

JavaScript 已被证明是一种重要的技术,它不会很快消失。这在很大程度上是因为它能够通过新的框架和库(如backbone.jsjQueryDojounderscore.js等)得到重生和扩展。这直接关系到 JavaScript 作为一种函数式编程语言的真正身份。对 JavaScript 的函数式编程的理解将长期受到欢迎,并对任何技能水平的程序员都将是有用的。

为什么这样?函数式编程非常强大、健壮和优雅。它在大型数据结构上非常有用和高效。将 JavaScript——一种客户端脚本语言,作为一种函数式手段来操作 DOM、对 API 响应进行排序或在日益复杂的网站上执行其他任务,可能非常有利。

在本书中,您将学习有关 JavaScript 函数式编程的一切:如何通过函数式编程增强 JavaScript 网络应用程序,如何解锁 JavaScript 的潜在力,以及如何编写更强大、更易于维护、下载速度更快、开销更小的代码。您还将学习函数式编程的核心概念,如何将其应用于 JavaScript,如何避开在使用 JavaScript 作为函数式语言时可能出现的注意事项和问题,以及如何在 JavaScript 中将函数式编程与面向对象编程相结合。

但在我们开始之前,让我们进行一个实验。

演示

也许一个快速的演示将是介绍 JavaScript 函数式编程的最佳方式。我们将使用 JavaScript 执行相同的任务——一次使用传统的本地方法,一次使用函数式编程。然后,我们将比较这两种方法。

应用程序-电子商务网站

在追求真实世界应用的过程中,假设我们需要为一家邮购咖啡豆公司开发一个电子商务网站应用程序。他们销售几种不同类型和不同数量的咖啡,这两者都会影响价格。

命令式方法

首先,让我们按照程序化的路线进行。为了使这个演示接地气,我们将创建保存数据的对象。这允许从数据库中获取值的能力,如果需要的话。但现在,我们假设它们是静态定义的:

// create some objects to store the data.
var columbian = {
  name: 'columbian',
  basePrice: 5
};
var frenchRoast = {
  name: 'french roast',
  basePrice: 8
};
var decaf = {
  name: 'decaf',
  basePrice: 6
};

// we'll use a helper function to calculate the cost 
// according to the size and print it to an HTML list
function printPrice(coffee, size) {
  if (size == 'small') {
    var price = coffee.basePrice + 2;
  }
  else if (size == 'medium') {
    var price = coffee.basePrice + 4;
  }
  else {
    var price = coffee.basePrice + 6;
  }

// create the new html list item
  var node = document.createElement("li");
  var label = coffee.name + ' ' + size;
  var textnode = document.createTextNode(label+' price: $'+price);
  node.appendChild(textnode);
  document.getElementById('products').appendChild(node);
}

// now all we need to do is call the printPrice function
// for every single combination of coffee type and size
printPrice(columbian, 'small');
printPrice(columbian, 'medium');
printPrice(columbian, 'large');
printPrice(frenchRoast, 'small');
printPrice(frenchRoast, 'medium');
printPrice(frenchRoast, 'large');
printPrice(decaf, 'small');
printPrice(decaf, 'medium');
printPrice(decaf, 'large');

提示

下载示例代码

您可以从www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

正如你所看到的,这段代码非常基础。如果这里不只是三种咖啡风格呢?如果有 20 种?50 种?如果除了大小之外,还有有机和非有机的选择。那将极大地增加代码行数!

使用这种方法,我们告诉机器为每种咖啡类型和每种大小打印什么。这基本上就是命令式代码的问题所在。

函数式编程

命令式代码告诉机器,逐步地,它需要做什么来解决问题,而函数式编程则试图以数学方式描述问题,以便机器可以做其余的工作。

采用更加函数式的方法,同样的应用可以写成如下形式:

// separate the data and logic from the interface
var printPrice = function(price, label) {
  var node = document.createElement("li");
  var textnode = document.createTextNode(label+' price: $'+price);
  node.appendChild(textnode);
  document.getElementById('products 2').appendChild(node);
}

// create function objects for each type of coffee
var columbian = function(){
  this.name = 'columbian'; 
  this.basePrice = 5;
};
var frenchRoast = function(){
  this.name = 'french roast'; 
  this.basePrice = 8;
};
var decaf = function(){
  this.name = 'decaf'; 
  this.basePrice = 6;
};

// create object literals for the different sizes
var small = {
  getPrice: function(){return this.basePrice + 2},
  getLabel: function(){return this.name + ' small'}
};
var medium = {
  getPrice: function(){return this.basePrice + 4},
  getLabel: function(){return this.name + ' medium'}
};
var large = {
  getPrice: function(){return this.basePrice + 6},
  getLabel: function(){return this.name + ' large'}
};

// put all the coffee types and sizes into arrays
var coffeeTypes = [columbian, frenchRoast, decaf];
var coffeeSizes = [small, medium, large];

// build new objects that are combinations of the above
// and put them into a new array
var coffees = coffeeTypes.reduce(function(previous, current) {
  var newCoffee = coffeeSizes.map(function(mixin) {
    // `plusmix` function for functional mixins, see Ch.7
    var newCoffeeObj = plusMixin(current, mixin);
    return new newCoffeeObj();
  });
  return previous.concat(newCoffee);
},[]);

// we've now defined how to get the price and label for each
// coffee type and size combination, now we can just print them
coffees.forEach(function(coffee){
  printPrice(coffee.getPrice(),coffee.getLabel());
});

首先显而易见的是它更加模块化。这使得添加新的大小或新的咖啡类型就像下面的代码片段中所示的那样简单:

var peruvian = function(){
  this.name = 'peruvian'; 
  this.basePrice = 11;
};

var extraLarge = {
  getPrice: function(){return this.basePrice + 10},
  getLabel: function(){return this.name + ' extra large'}
};

coffeeTypes.push(Peruvian);
coffeeSizes.push(extraLarge);

咖啡对象和大小对象的数组被“混合”在一起,也就是说,它们的方法和成员变量与一个名为plusMixin的自定义函数结合在一起(参见第七章,“JavaScript 中的函数式和面向对象编程”)。咖啡类型类包含成员变量,大小包含计算名称和价格的方法。 “混合”发生在map操作中,它对数组中的每个元素应用纯函数,并在reduce()操作中返回一个新函数——另一个类似于map函数的高阶函数,不同之处在于数组中的所有元素都合并成一个。最后,所有可能的类型和大小组合的新数组通过forEach()方法进行迭代。forEach()方法是另一个高阶函数,它对数组中的每个对象应用回调函数。在这个例子中,我们将其作为一个匿名函数提供,该函数实例化对象并调用printPrice()函数,其中包括对象的getPrice()getLabel()方法作为参数。

实际上,我们可以通过移除coffees变量并将函数链接在一起使这个例子更加函数化——这是函数式编程中的另一个小技巧。

coffeeTypes.reduce(function(previous, current) {
  var newCoffee = coffeeSizes.map(function(mixin) {
    // `plusMixin` function for functional mixins, see Ch.7
    var newCoffeeObj = plusMixin(current, mixin);
    return new newCoffeeObj();
  });
  return previous.concat(newCoffee);
},[]).forEach(function(coffee) {
  printPrice(coffee.getPrice(),coffee.getLabel());
});

此外,控制流不像命令式代码那样自上而下。在函数式编程中,map()函数和其他高阶函数取代了forwhile循环,执行顺序的重要性很小。这使得新手更难阅读代码,但一旦掌握了,就会发现其实并不难跟踪,而且会发现它更好。

这个例子只是简单介绍了在 JavaScript 中函数式编程可以做什么。在本书中,你将看到更强大的函数式编程的例子。

总结

首先,采用函数式风格的好处是明显的。

其次,不要害怕函数式编程。是的,它经常被认为是以计算机语言形式的纯逻辑,但我们不需要理解Lambda 演算就能将其应用到日常任务中。事实上,通过允许我们的程序被分解成更小的部分,它们更容易理解,更简单维护,更可靠。map()reduce()函数是 JavaScript 中较少为人知的内置函数,但我们会看一下它们。

JavaScript 是一种脚本语言,交互性强,易于接近。不需要编译。我们甚至不需要下载任何开发软件,你喜欢的浏览器可以作为解释器和开发环境。

感兴趣吗?好的,让我们开始吧!

第二章:函数式编程基础

到目前为止,你已经看到了函数式编程可以做些什么的一小部分。但函数式编程到底是什么?什么使一种语言是函数式的而另一种不是?什么使一种编程风格是函数式的而另一种不是?

在本章中,我们将首先回答这些问题,然后介绍函数式编程的核心概念:

  • 使用函数和数组进行控制流

  • 编写纯函数、匿名函数、递归函数等

  • 像对象一样传递函数

  • 利用 map()filter()reduce() 函数

函数式编程语言

函数式编程语言是促进函数式编程范式的语言。冒昧地说,我们可以说,如果一种语言包括函数式编程所需的特性,那么它就是一种函数式语言——就是这么简单。在大多数情况下,真正决定一个程序是否是函数式的是编程风格。

什么使一种语言是函数式的?

C 语言无法进行函数式编程。Java 语言也无法进行函数式编程(没有大量繁琐的“几乎”函数式编程的变通方法)。这些以及许多其他语言根本就不包含支持函数式编程的结构。它们纯粹是面向对象的,严格来说不是函数式语言。

同时,面向对象编程无法在纯函数式语言中进行,比如 Scheme、Haskell 和 Lisp,仅举几例。

然而,有些语言支持两种模型。Python 就是一个著名的例子,但还有其他的:Ruby、Julia,还有我们感兴趣的 JavaScript。这些语言如何支持两种非常不同的设计模式?它们包含了两种编程范式所需的特性。然而,在 JavaScript 的情况下,函数式特性有些隐藏。

但实际上,情况要复杂一些。那么什么使一种语言是函数式的呢?

特征 命令式 函数式
编程风格 执行逐步任务和管理状态变化 定义问题是什么以及需要哪些数据转换来实现解决方案
状态变化 重要 不存在
执行顺序 重要 不重要
主要流程控制 循环、条件和函数调用 函数调用和递归
主要操作单元 结构和类对象 函数作为一等对象和数据集

语言的语法必须允许某些设计模式,比如隐式类型系统和使用匿名函数的能力。基本上,语言必须实现 Lambda 演算。此外,解释器的评估策略应该是非严格的和按需调用(也称为延迟执行),这允许不可变的数据结构和非严格的惰性评估。

优势

你可以说,当你最终“领悟”时所经历的深刻启示将使学习函数式编程变得值得。这样的经历将使你成为终身更好的程序员,无论你是否真的成为全职的函数式程序员。

但我们不是在谈论学习冥想;我们在谈论学习一种极其有用的工具,这将使你成为一个更好的程序员。

从形式上讲,使用函数式编程的实际优势是什么?

更清晰的代码

函数式程序更清洁、更简单、更小。这简化了调试、测试和维护。

例如,假设我们需要一个将二维数组转换为一维数组的函数。只使用命令式技术,我们可以这样写:

function merge2dArrayIntoOne(arrays) {
  var count = arrays.length;
  var merged = new Array(count); 
  var c = 0;
  for (var i = 0; i < count; ++i) {
    for (var j = 0, jlen = arrays[i].length; j < jlen; ++j) {
      merged[c++] = arrays[i][j];
    }
  }
  return merged
}

并且使用函数式技术,可以写成如下形式:

varmerge2dArrayIntoOne2 = function(arrays) {
  return arrays.reduce( function(p,n){
    return p.concat(n);
  });
};

这两个函数都接受相同的输入并返回相同的输出。然而,函数示例要简洁清晰得多。

模块化

函数式编程迫使将大问题分解为要解决的相同问题的较小实例。这意味着代码更加模块化。模块化的程序具有明确定义,更容易调试,更简单维护。测试更容易,因为每个模块化代码片段都可以潜在地检查正确性。

可重用性

由于函数式编程的模块化,函数式程序共享各种常见的辅助函数。您会发现这些函数中的许多函数可以被重用于各种不同的应用程序。

本章后面将介绍许多常见的函数。然而,作为函数式程序员,您将不可避免地编写自己的小函数库,这些函数可以反复使用。例如,一个设计良好的函数,用于搜索配置文件的行,也可以用于搜索哈希表。

减少耦合

耦合是程序中模块之间的依赖程度。因为函数式程序员致力于编写独立于彼此的一流、高阶、纯函数,不对全局变量产生副作用,所以耦合大大减少。当然,函数将不可避免地相互依赖。但只要输入到输出的一对一映射保持正确,修改一个函数不会改变另一个函数。

数学上的正确性

最后一个是在更理论的层面上。由于其源自 Lambda 演算,函数式程序可以在数学上被证明是正确的。这对需要证明程序的增长率、时间复杂度和数学正确性的研究人员来说是一个巨大优势。

让我们来看看斐波那契数列。尽管它很少用于除了概念验证之外的任何其他用途,但它很好地说明了这个概念。评估斐波那契数列的标准方法是创建一个递归函数,表达式为fibonnaci(n) = fibonnaci(n-2) + fibonnaci(n–1),并且有一个基本情况,即当 n < 2 时返回 1,这样可以停止递归并开始在递归调用堆栈的每一步返回的值上进行求和。

这描述了计算序列所涉及的中间步骤。

var fibonacci = function(n) {
  if (n < 2) {
    return 1;
  }
  else {
    return fibonacci(n - 2) + fibonacci(n - 1);
  }
}
console.log( fibonacci(8) );
// Output: 34

然而,借助实现惰性执行策略的库,可以生成一个无限序列,该序列陈述了定义整个数字序列的数学方程。只有需要计算的数字才会被计算。

var fibonacci2 = Lazy.generate(function() {
  var x = 1,
  y = 1;
  return function() {
    var prev = x;
    x = y;
    y += prev;
    return prev;
  };
}());

console.log(fibonacci2.length());// Output: undefined

console.log(fibonacci2.take(12).toArray());// Output: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144] 

var fibonacci3 = Lazy.generate(function() {
  var x = 1,
  y = 1;
  return function() {
    var prev = x;
    x = y;
    y += prev;
    return prev;
  };
}());

console.log(fibonacci3.take(9).reverse().first(1).toArray());// Output: [34]

第二个例子显然更具数学上的合理性。它依赖于 JavaScript 的Lazy.js库。还有其他可以帮助的库,比如Sloth.jswu.js。这些将在第三章中进行介绍,设置函数式编程环境

非函数式世界中的函数式编程

函数式和非函数式编程可以混合在一起吗?尽管这是第七章的主题,JavaScript 中的函数式和面向对象编程,但在我们继续之前,有几件事情需要搞清楚。

本书的目的不是教您如何实现严格遵循纯函数式编程严格要求的整个应用程序。这样的应用程序在学术界之外很少合适。相反,本书将教您如何在应用程序中使用函数式编程设计策略,以补充必要的命令式代码。

例如,如果您需要从某个文本中提取出的前四个只包含字母的单词,可以天真地写成这样:

var words = [], count = 0;
text = myString.split(' ');
for (i=0; count<4, i<text.length; i++) {
  if (!text[i].match(/[0-9]/)) {
    words = words.concat(text[i]);
    count++;
  }
}
console.log(words);

相比之下,函数式程序员可能会这样写:

var words = [];
var words = myString.split(' ').filter(function(x){
  return (! x.match(/[1-9]+/));
}).slice(0,4);
console.log(words);

或者,通过一个函数式编程工具库,它们甚至可以更简化:

var words = toSequence(myString).match(/[a-zA-Z]+/).first(4);

识别可以以更函数式方式编写的函数的关键是查找循环和临时变量,例如前面示例中的wordscount实例。通常我们可以通过用高阶函数替换它们来摆脱临时变量和循环,我们将在本章后面探讨这一点。

JavaScript 是一种函数式编程语言吗?

我们必须问自己最后一个问题。JavaScript 是一种函数式语言还是非函数式语言?

JavaScript 可以说是世界上最流行但最不被理解的函数式编程语言。JavaScript 是一种穿着 C 样式衣服的函数式编程语言。它的语法无可否认地类似于 C,意味着它使用 C 的块语法和中缀顺序。而且它是存在的最糟糕的命名语言之一。毫不费力地可以想象为什么这么多人会混淆 JavaScript 与 Java 有关;不知何故,它的名字暗示着它应该有关联!但实际上它与 Java 几乎没有共同之处。而且,为了真正巩固 JavaScript 是一种面向对象的语言的想法,诸如 Dojo 和ease.js等库和框架一直在努力将其抽象化,并使其适用于面向对象的编程。JavaScript 在 20 世纪 90 年代成熟起来,当时面向对象编程是所有人都在谈论的话题,我们被告知 JavaScript 是面向对象的,因为我们非常希望它是这样。但它并不是。

它的真正身份更与其祖先相一致:Scheme 和 Lisp,两种经典的函数式语言。JavaScript 是一种纯函数式语言。它的函数是一等公民,可以嵌套,具有闭包和组合,并且允许柯里化和单子。所有这些都是函数式编程的关键。以下是 JavaScript 是函数式语言的几个原因:

  • JavaScript 的词法语法包括将函数作为参数传递的能力,具有推断类型系统,并允许匿名函数、高阶函数、闭包等。这些事实对于实现函数式编程的结构和行为至关重要。

  • 它不是一种纯粹的面向对象语言,大多数面向对象的设计模式是通过复制原型对象来实现的,这是一种较弱的面向对象编程模型。欧洲计算机制造商协会脚本ECMAScript),JavaScript 的正式和标准化实现规范,在规范 4.2.1 中陈述了以下内容:

“ECMAScript 不包含像 C++、Smalltalk 或 Java 中那样的适当类,而是支持创建对象的构造函数。在基于类的面向对象语言中,一般来说,状态由实例承载,方法由类承载,继承仅涉及结构和行为。在 ECMAScript 中,状态和方法由对象承载,结构、行为和状态都是继承的。”

  • 它是一种解释性语言。有时被称为“引擎”,JavaScript 解释器通常与 Scheme 解释器非常相似。两者都是动态的,都具有灵活的数据类型,可以轻松组合和转换,都将代码评估为表达式块,并且都类似地处理函数。

也就是说,JavaScript 并不是一种纯函数式语言。缺少的是惰性求值和内置的不可变数据。这是因为大多数解释器是按名称调用而不是按需调用。由于它处理尾调用的方式,JavaScript 也不太擅长递归。然而,所有这些问题都可以通过一点注意来缓解。非严格求值,用于无限序列和惰性求值,可以通过一个名为Lazy.js的库来实现。不可变数据可以通过编程技术简单实现,但这需要更多的程序员纪律,而不是依赖语言来处理。递归尾调用消除可以通过一种称为Trampolining的方法来实现。这些问题将在第六章中得到解决,JavaScript 中的高级主题和陷阱

关于 JavaScript 是一种函数式语言、面向对象语言、两者都是还是两者都不是,已经进行了许多争论。而且这不会是最后一次辩论。

最终,函数式编程是通过巧妙地改变、组合和使用函数的方式来编写更清晰的代码。JavaScript 为这种方法提供了一个出色的媒介。如果您真的想要充分发挥 JavaScript 的潜力,您必须学会将其用作一种函数式语言。

使用函数

有时,优雅的实现是一个函数。不是一个方法。不是一个类。不是一个框架。只是一个函数。
--约翰·卡马克,末日游戏的首席程序员

函数式编程是将问题分解为一组函数的过程。通常,函数被链接在一起,嵌套在彼此内部,传递并被视为一等公民。如果您使用过 jQuery 和 Node.js 等框架,您可能已经使用了其中一些技术,只是您没有意识到!

让我们从一个小 JavaScript 困境开始。

我们需要编译一个分配给通用对象的值列表。对象可以是任何东西:日期、HTML 对象等等。

var
  obj1 = {value: 1},
  obj2 = {value: 2},
  obj3 = {value: 3};

var values = [];
function accumulate(obj) {
  values.push(obj.value);
}
accumulate(obj1);
accumulate(obj2);
console.log(values); // Output: [obj1.value, obj2.value]

它能工作,但它是不稳定的。任何代码都可以修改values对象,而不调用accumulate()函数。如果我们忘记将空集[]分配给values实例,那么代码将根本无法工作。

但是,如果变量在函数内声明,它就不能被任何不受控制的代码改变。

function accumulate2(obj) {
  var values = [];
  values.push(obj.value);
  return values;
}
console.log(accumulate2(obj1)); // Returns: [obj1.value]
console.log(accumulate2(obj2)); // Returns: [obj2.value]
console.log(accumulate2(obj3)); // Returns: [obj3.value]

它不起作用!只返回最后传入的对象的值。

我们可能可以通过在第一个函数内部嵌套一个函数来解决这个问题。

var ValueAccumulator = function(obj) {
  var values = []
  var accumulate = function() {
    values.push(obj.value);   
  };
  accumulate();
  return values;
};

但问题是一样的,现在我们无法访问accumulate函数或values变量。

我们需要的是一个自调用函数。

自调用函数和闭包

如果我们能返回一个函数表达式,该表达式反过来返回values数组呢?在函数中声明的变量对函数内的任何代码都是可用的,包括自调用函数。

通过使用自调用函数,我们解决了我们的困境。

var ValueAccumulator = function() {
  var values = [];
  var accumulate = function(obj) {
    if (obj) {
      values.push(obj.value);
      return values;
    }
    else {
      return values;
    }
  };
  return accumulate;
};

//This allows us to do this:
var accumulator = ValueAccumulator();
accumulator(obj1); 
accumulator(obj2); 
console.log(accumulator()); 
// Output: [obj1.value, obj2.value]

这都与变量作用域有关。values变量对内部的accumulate()函数是可用的,即使在作用域外部调用函数时也是如此。这就是闭包。

注意

JavaScript 中的闭包是指即使父函数已关闭,也可以访问父作用域的函数。

闭包是所有函数式语言的特性。传统的命令式语言不允许它们。

高阶函数

自调用函数实际上是一种高阶函数形式。高阶函数是要么将另一个函数作为输入,要么将一个函数作为输出的函数。

高阶函数在传统编程中并不常见。在命令式编程中,程序员可能会使用循环来迭代数组,而函数式编程者则会采取完全不同的方法。通过使用高阶函数,可以通过将该函数应用于数组中的每个项来对数组进行操作,从而创建一个新数组。

这是函数式编程范式的核心思想。高阶函数允许将逻辑传递给其他函数,就像对象一样。

在 JavaScript 中,函数被视为一等公民,这是 JavaScript 与 Scheme、Haskell 和其他经典函数式语言共享的特点。这听起来很奇怪,但这实际上意味着函数被视为原语,就像数字和对象一样。如果数字和对象可以被传递,那么函数也可以被传递。

为了看到这一点,让我们在前面部分的ValueAccumulator()函数中使用一个高阶函数:

// using forEach() to iterate through an array and call a 
// callback function, accumulator, for each item
var accumulator2 = ValueAccumulator();
var objects = [obj1, obj2, obj3]; // could be huge array of objects
objects.forEach(accumulator2);
console.log(accumulator2());

纯函数

纯函数返回仅使用传递给它的输入计算的值。不能使用外部变量和全局状态,也不能产生副作用。换句话说,它不能改变传递给它的输入变量。因此,纯函数只能用于它们的返回值。

一个简单的例子是数学函数。Math.sqrt(4)函数将始终返回2,不使用任何隐藏信息,如设置或状态,并且永远不会产生任何副作用。

纯函数是对数学术语“函数”的真正解释,它是输入和输出之间的关系。它们很容易理解,并且可以被很容易地重复使用。因为它们是完全独立的,所以纯函数更有可能被反复使用。

为了说明这一点,比较以下非纯函数和纯函数。

// function that prints a message to the center of the screen
var printCenter = function(str) {
  var elem = document.createElement("div");
  elem.textContent = str;
  elem.style.position = 'absolute';
  elem.style.top = window.innerHeight/2+"px";
  elem.style.left = window.innerWidth/2+"px";
  document.body.appendChild(elem);
};
printCenter('hello world');
// pure function that accomplishes the same thing
var printSomewhere = function(str, height, width) {
  var elem = document.createElement("div");
  elem.textContent = str;
  elem.style.position = 'absolute';
  elem.style.top = height;
  elem.style.left = width;
  return elem;
};
document.body.appendChild(printSomewhere('hello world', window.innerHeight/2)+10+"px",window.innerWidth/2)+10+"px")
);

非纯函数依赖于窗口对象的状态来计算高度和宽度,而纯自给自足的函数则要求传入这些值。实际上,这样做可以让消息在任何地方打印,这使得函数更加灵活。

虽然非纯函数可能看起来更容易,因为它自己执行附加而不是返回一个元素,但纯函数printSomewhere()及其返回值与其他函数式编程设计技术更配合。

var messages = ['Hi', 'Hello', 'Sup', 'Hey', 'Hola'];
messages.map(function(s,i){
  return printSomewhere(s, 100*i*10, 100*i*10);
}).forEach(function(element) {
  document.body.appendChild(element);
});

注意

当函数是纯的并且不依赖于状态或环境时,我们不关心它们实际上何时何地被计算。稍后我们将在惰性求值中看到这一点。

匿名函数

将函数视为一等公民的另一个好处是匿名函数的出现。

顾名思义,匿名函数是没有名称的函数。但它们不仅仅是这样。它们允许根据需要定义临时逻辑。通常是为了方便起见;如果函数只被引用一次,那么就不需要浪费一个变量名。

一些匿名函数的例子如下:

// The standard way to write anonymous functions
function(){return "hello world"};

// Anonymous function assigned to variable
var anon = function(x,y){return x+y};

// Anonymous function used in place of a named callback function, 
// this is one of the more common uses of anonymous functions.
setInterval(function(){console.log(new Date().getTime())}, 1000);
// Output:  1413249010672, 1413249010673, 1413249010674, ...

// Without wrapping it in an anonymous function, it immediately // execute once and then return undefined as the callback:
setInterval(console.log(new Date().getTime()), 1000)
// Output:  1413249010671

在高阶函数中使用匿名函数的更复杂的例子:

function powersOf(x) {
  return function(y) {
    // this is an anonymous function!
    return Math.pow(x,y);
  };
}
powerOfTwo = powersOf(2);
console.log(powerOfTwo(1)); // 2
console.log(powerOfTwo(2)); // 4
console.log(powerOfTwo(3)); // 8

powerOfThree = powersOf(3);
console.log(powerOfThree(3));  // 9
console.log(powerOfThree(10)); // 59049

返回的函数不需要被命名;它不能在powersOf()函数之外的任何地方使用,因此它是一个匿名函数。

记得我们的累加器函数吗?可以使用匿名函数来重写它。

var
  obj1 = {value: 1},
  obj2 = {value: 2},
  obj3 = {value: 3};

var values = (function() {
  // anonymous function
  var values = [];
  return function(obj) {
    // another anonymous function!
    if (obj) {
      values.push(obj.value);
      return values;
    }
    else {
      return values;
    }
  }
})(); // make it self-executing
console.log(values(obj1)); // Returns: [obj.value]
console.log(values(obj2)); // Returns: [obj.value, obj2.value]

太好了!一个纯的、高阶的、匿名的函数。我们怎么会这么幸运呢?实际上,它不仅仅是这样。它还是自执行的,如结构(function(){...})();所示。匿名函数后面的一对括号会立即调用函数。在上面的例子中,values实例被赋值为自执行函数调用的输出。

注意

匿名函数不仅仅是一种语法糖。它们是 Lambda 演算的体现。跟我一起来理解一下……Lambda 演算是在计算机或计算机语言出现之前发明的。它只是用于推理函数的数学概念。令人惊讶的是,尽管它只定义了三种表达式:变量引用、函数调用和匿名函数,它却是图灵完备的。今天,如果你知道如何找到它,Lambda 演算是所有函数式语言的核心,包括 JavaScript。

因此,匿名函数通常被称为 Lambda 表达式。

匿名函数的一个缺点是它们很难在调用堆栈中识别,这使得调试变得更加棘手。应该谨慎使用它们。

方法链

在 JavaScript 中链接方法是相当常见的。如果你使用过 jQuery,你可能已经使用过这种技术。有时被称为“构建器模式”。

这是一种用于简化代码的技术,其中多个函数依次应用于对象。

// Instead of applying the functions one per line...
arr = [1,2,3,4];
arr1 = arr.reverse();
arr2 = arr1.concat([5,6]);
arr3 = arr2.map(Math.sqrt);
// ...they can be chained together into a one-liner
console.log([1,2,3,4].reverse().concat([5,6]).map(Math.sqrt));
// parentheses may be used to illustrate
console.log(((([1,2,3,4]).reverse()).concat([5,6])).map(Math.sqrt) );

这只有在函数是对象的方法时才有效。如果你创建了自己的函数,例如,接受两个数组并返回一个将这两个数组合并在一起的数组,你必须将它声明为Array.prototype对象的成员。看一下以下代码片段:

Array.prototype.zip = function(arr2) {
  // ...
}

这将使我们能够做到以下几点:

arr.zip([11,12,13,14).map(function(n){return n*2});
// Output: 2, 22, 4, 24, 6, 26, 8, 28

递归

递归很可能是最著名的函数式编程技术。如果你还不知道,递归函数是调用自身的函数。

当一个函数调用它自己时,会发生一些奇怪的事情。它既像一个循环,执行相同的代码多次,又像一个函数堆栈。

递归函数必须非常小心地避免无限循环(在这种情况下是无限递归)。因此,就像循环一样,必须使用条件来知道何时停止。这被称为基本情况。

一个例子如下:

var foo = function(n) {
  if (n < 0) {
    // base case
    return 'hello';
  }
  else {
    // recursive case
    foo(n-1);
  }
}
console.log(foo(5));

可以将任何循环转换为递归算法,也可以将任何递归算法转换为循环。但是递归算法更适合,几乎是必要的,用于与适合使用循环的情况大不相同的情况。

一个很好的例子是树的遍历。虽然使用递归函数遍历树并不太难,但使用循环会更加复杂,并且需要维护一个堆栈。这与函数式编程的精神相违背。

var getLeafs = function(node) {
  if (node.childNodes.length == 0) {
    // base case
    return node.innerText;
  }
  else {
    // recursive case: 
    return node.childNodes.map(getLeafs);
  }
}

分而治之

递归不仅仅是一种在没有forwhile循环的情况下进行迭代的有趣方式。一种算法设计,称为分而治之,将问题递归地分解为相同问题的较小实例,直到它们足够小以便解决。

这方面的历史例子是欧几里得算法,用于找到两个数的最大公约数。

function gcd(a, b) {
  if (b == 0) {
    // base case (conquer)
    return a;
  }
  else {
    // recursive case (divide)
    return gcd(b, a % b);
  }
}

console.log(gcd(12,8));
console.log(gcd(100,20));

所以从理论上讲,分而治之的工作方式非常优雅,但它在现实世界中有用吗?是的!JavaScript 中用于对数组进行排序的函数并不是很好。它不仅会就地对数组进行排序,这意味着数据是不可变的,而且它也不可靠和灵活。通过分而治之,我们可以做得更好。

归并排序算法使用分而治之的递归算法设计,通过递归地将数组分成较小的子数组,然后将它们合并在一起来高效地对数组进行排序。

在 JavaScript 中的完整实现大约有 40 行代码。然而,伪代码如下:

var mergeSort = function(arr){
  if (arr.length < 2) {
    // base case: 0 or 1 item arrays don't need sorting
    return items;
  }
  else {
    // recursive case: divide the array, sort, then merge
    var middle = Math.floor(arr.length / 2);
    // divide
    var left = mergeSort(arr.slice(0, middle));
    var right = mergeSort(arr.slice(middle));
    // conquer
    // merge is a helper function that returns a new array
    // of the two arrays merged together
    return merge(left, right);
  }
}

惰性求值

惰性评估,也称为非严格评估、按需调用和延迟执行,是一种等到需要值时才计算函数结果的评估策略,对函数式编程非常有用。很明显,一行代码 x = func() 表示要将 x 赋值为 func() 返回的值。但是 x 实际上等于什么并不重要,直到需要它为止。等到需要 x 时再调用 func() 就是惰性评估。

这种策略可以大大提高性能,特别是在方法链和数组中使用时,这是函数式编程者最喜欢的程序流技术。

惰性评估的一个令人兴奋的好处是存在无限序列。因为直到不能再延迟才实际计算任何东西,所以这是可能的:

// wishful JavaScript pseudocode:
var infinateNums = range(1 to infinity);
var tenPrimes = infinateNums.getPrimeNumbers().first(10);

这为许多可能性打开了大门:异步执行、并行化和组合,仅举几例。

然而,有一个问题:JavaScript 本身不执行惰性评估。也就是说,存在着一些用于 JavaScript 的库,可以非常好地模拟惰性评估。这就是《第三章》《设置函数式编程环境》的主题。

函数式编程者的工具包

如果你仔细看了到目前为止呈现的几个例子,你会注意到使用了一些你可能不熟悉的方法。它们是 map()filter()reduce() 函数,对任何语言的函数式程序都至关重要。它们使你能够消除循环和语句,从而使代码更加清晰。

map()filter()reduce() 函数构成了函数式编程者工具包的核心,这是一组纯的高阶函数,是函数式方法的工作马。事实上,它们是纯函数和高阶函数应该具有的典范;它们接受一个函数作为输入,并返回一个没有副作用的输出。

虽然它们是 ECMAScript 5.1 实现的浏览器的标准,但它们只适用于数组。每次调用时,都会创建并返回一个新数组。现有数组不会被修改。但还有更多,它们接受函数作为输入,通常以匿名函数的形式作为回调函数;它们遍历数组并将函数应用于数组中的每个项目!

myArray = [1,2,3,4];
newArray = myArray.map(function(x) {return x*2});
console.log(myArray);  // Output: [1,2,3,4]
console.log(newArray); // Output: [2,4,6,8]

还有一点。因为它们只适用于数组,所以不能用于其他可迭代的数据结构,比如某些对象。不用担心,诸如 underscore.jsLazy.jsstream.js 等库都实现了自己的 map()filter()reduce() 方法,更加灵活。

回调

如果你以前从未使用过回调函数,可能会觉得这个概念有点费解。特别是在 JavaScript 中,因为 JavaScript 允许以多种方式声明函数。

callback() 函数用于传递给其他函数以供其使用。这是一种传递逻辑的方式,就像传递对象一样:

var myArray = [1,2,3];
function myCallback(x){return x+1};
console.log(myArray.map(myCallback));

为了简化简单的任务,可以使用匿名函数:

console.log(myArray.map(function(x){return x+1}));

它们不仅用于函数式编程,还用于 JavaScript 中的许多其他事情。仅举一个例子,这是在使用 jQuery 进行 AJAX 调用时使用的 callback() 函数:

function myCallback(xhr){
  console.log(xhr.status); 
  return true;
}
$.ajax(myURI).done(myCallback);

注意只使用了函数的名称。因为我们没有调用回调,只是传递了它的名称,所以写成这样是错误的:

$.ajax(myURI).fail(**myCallback(xhr)**);
// or
$.ajax(myURI).fail(**myCallback()**);

如果我们调用回调会发生什么?在这种情况下,myCallback(xhr) 方法将尝试执行——控制台将打印“undefined”,并返回 True。当 ajax() 调用完成时,它将以 'true' 作为要使用的回调函数的名称,这将引发错误。

这也意味着我们无法指定传递给回调函数的参数。如果我们需要与ajax()调用传递给它的参数不同的参数,我们可以将回调函数包装在匿名函数中:

function myCallback(status){
  console.log(status); 
  return true;
}
$.ajax(myURI).done(function(xhr){myCallback(xhr.status)});

Array.prototype.map()

map()函数是这一系列中的头目。它只是在数组中的每个项目上应用回调函数。

注意

语法:arr.map(callback [, thisArg]);

参数:

  • 回调(): 此函数为新数组生成一个元素,接收以下参数:

  • currentValue:此参数给出正在处理的数组中的当前元素

  • 索引:此参数给出数组中当前元素的索引

  • 数组:此参数给出正在处理的数组

  • thisArg(): 此函数是可选的。在执行回调时,该值将被用作this

示例:

var
  integers = [1,-0,9,-8,3],
  numbers = [1,2,3,4],
  str = 'hello world how ya doing?';
// map integers to their absolute values
console.log(integers.map(Math.abs));

// multiply an array of numbers by their position in the array
console.log(numbers.map(function(x, i){return x*i}) );

// Capitalize every other word in a string.
console.log(str.split(' ').map(function(s, i){
  if (i%2 == 0) {
    return s.toUpperCase();
  }
  else {
    return s;
  }
}) );

注意

虽然Array.prototype.map方法是 JavaScript 中数组对象的标准方法,但它也可以很容易地扩展到您的自定义对象。

MyObject.prototype.map = function(f) {
  return new MyObject(f(this.value));
};

Array.prototype.filter()

filter()函数用于从数组中取出元素。回调必须返回True(以在新数组中包含该项)或False(以删除该项)。使用map()函数并返回要删除的项目的null值也可以实现类似的效果,但filter()函数将从新数组中删除该项,而不是在其位置插入null值。

注意

语法:arr.filter(callback [, thisArg]);

参数:

  • 回调(): 此函数用于测试数组中的每个元素。返回True以保留该元素,否则返回False。具有以下参数:

  • currentValue:此参数给出正在处理的数组中的当前元素

  • 索引:此参数给出数组中当前元素的索引

  • 数组:此参数给出正在处理的数组。

  • thisArg(): 此函数是可选的。在执行回调时,该值将被用作this

示例:

var myarray = [1,2,3,4]
words = 'hello 123 world how 345 ya doing'.split(' ');
re = '[a-zA-Z]';
// remove all negative numbers
console.log([-2,-1,0,1,2].filter(function(x){return x>0}));
// remove null values after a map operation
console.log(words.filter(function(s){
  return s.match(re);
}) );
// remove random objects from an array
console.log(myarray.filter(function(){
  return Math.floor(Math.random()*2)})
);

Array.prototype.reduce()

有时称为折叠,reduce()函数用于将数组的所有值累积为一个值。回调需要返回要执行的逻辑以组合对象。对于数字,它们通常相加以获得总和或相乘以获得乘积。对于字符串,通常将字符串追加在一起。

注意

语法:arr.reduce(callback [, initialValue]);

参数:

  • 回调(): 此函数将两个对象合并为一个,并返回。具有以下参数:

  • previousValue:此参数给出上一次调用回调时返回的值,或者如果提供了initialValue,则给出initialValue

  • currentValue:此参数给出正在处理的数组中的当前元素

  • 索引:此参数给出数组中当前元素的索引

  • 数组:此参数给出正在处理的数组

  • initialValue(): 此函数是可选的。用作回调的第一个参数的对象。

示例:

var numbers = [1,2,3,4];
// sum up all the values of an array
console.log([1,2,3,4,5].reduce(function(x,y){return x+y}, 0));
// sum up all the values of an array
console.log([1,2,3,4,5].reduce(function(x,y){return x+y}, 0));

// find the largest number
console.log(numbers.reduce(function(a,b){
  return Math.max(a,b)}) // max takes two arguments
);

荣誉提及

map()filter()reduce()函数并不是我们辅助函数工具箱中的唯一函数。还有许多其他函数可以插入到几乎任何功能应用程序中。

Array.prototype.forEach

本质上是map()的非纯版本,forEach()遍历数组并在每个项目上应用回调()函数。但它不返回任何东西。这是执行for循环的更干净的方式。

注意

语法:arr.forEach(callback [, thisArg]);

参数:

  • 回调(): 此函数用于对数组的每个值执行。具有以下参数:

  • currentValue:此参数给出正在处理的数组中的当前元素

  • 索引:此参数给出数组中当前元素的索引

  • 数组:此参数给出正在处理的数组

  • thisArg:此函数是可选的。在执行回调时,该值将被用作this

示例:

var arr = [1,2,3];
var nodes = arr.map(function(x) {
  var elem = document.createElement("div");
  elem.textContent = x;
  return elem;
});

// log the value of each item
arr.forEach(function(x){console.log(x)});

// append nodes to the DOM
nodes.forEach(function(x){document.body.appendChild(x)});

Array.prototype.concat

在处理数组时,通常需要将多个数组连接在一起,而不是使用forwhile循环。另一个内置的 JavaScript 函数concat()可以为我们处理这个问题。concat()函数返回一个新数组,不会改变旧数组。它可以连接你传递给它的任意多个数组。

console.log([1, 2, 3].concat(['a','b','c']) // concatenate two arrays);
// Output: [1, 2, 3, 'a','b','c']

原始数组不受影响。它返回一个新数组,其中包含两个数组连接在一起。这也意味着concat()函数可以链接在一起。

var arr1 = [1,2,3];
var arr2 = [4,5,6];
var arr3 = [7,8,9];
var x = arr1.concat(arr2, arr3);
var y = arr1.concat(arr2).concat(arr3));
var z = arr1.concat(arr2.concat(arr3)));
console.log(x);
console.log(y);
console.log(z);

变量xyz都包含[1,2,3,4,5,6,7,8,9]

Array.prototype.reverse

另一个原生 JavaScript 函数有助于数组转换。reverse()函数颠倒了一个数组,使得第一个元素现在是最后一个,最后一个是第一个。

然而,它不会返回一个新数组;而是就地改变数组。我们可以做得更好。下面是一个纯方法的实现,用于颠倒一个数组:

var invert = function(arr) {
  return arr.map(function(x, i, a) {
    return a[a.length - (i+1)];
  });
};
var q = invert([1,2,3,4]);
console.log( q );

Array.prototype.sort

与我们的map()filter()reduce()方法类似,sort()方法接受一个定义数组中对象应如何排序的callback()函数。但是,与reverse()函数一样,它会就地改变数组。这样做不好。

arr = [200, 12, 56, 7, 344];
console.log(arr.sort(function(a,b){return a–b}) );
// arr is now: [7, 12, 56, 200, 344];

我们可以编写一个不会改变数组的纯sort()函数,但是排序算法是让人头疼的源泉。需要排序的大型数组应该被组织在专门设计用于此目的的数据结构中:quickStort、mergeSort、bubbleSort 等等。

Array.prototype.every 和 Array.prototype.some

Array.prototype.every()Array.prototype.some()函数都是纯函数和高阶函数,是Array对象的方法,用于对数组的元素进行测试,以便返回一个表示相应输入的布尔值的callback()函数。如果callback()函数对数组中的每个元素都返回True,则every()函数返回True,而some()函数返回True,如果数组中的一些元素为True

示例:

function isNumber(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}

console.log([1, 2, 3, 4].every(isNumber)); // Return: true
console.log([1, 2, 'a'].every(isNumber)); // Return: false
console.log([1, 2, 'a'].some(isNumber)); // Return: true

摘要

为了理解函数式编程,本章涵盖了一系列广泛的主题。首先,我们分析了编程语言为函数式编程意味着什么,然后评估了 JavaScript 的函数式编程能力。接下来,我们使用 JavaScript 应用了函数式编程的核心概念,并展示了一些 JavaScript 的内置函数用于函数式编程。

尽管 JavaScript 确实有一些用于函数式编程的工具,但其函数式核心仍然大多隐藏,还有很多需要改进的地方。在下一章中,我们将探索几个用于 JavaScript 的库,这些库揭示了其函数式的本质。

第三章:设置函数式编程环境

介绍

我们是否需要了解高级数学——范畴论、Lambda 演算、多态——才能使用函数式编程编写应用程序?我们是否需要重新发明轮子?对这两个问题的简短回答都是

在本章中,我们将尽力调查一切可能影响我们在 JavaScript 中编写函数式应用程序的方式。

  • 工具包

  • 开发环境

  • 编译为 JavaScript 的函数式语言

  • 等等

请理解,JavaScript 的函数库当前的格局是非常不确定的。就像计算机编程的所有方面一样,社区可能会在一瞬间发生变化;新的库可能会被采用,旧的库可能会被抛弃。例如,在撰写本书的过程中,流行且稳定的Node.js平台已被其开源社区分叉。它的未来是模糊的。

因此,从本章中获得的最重要的概念不是如何使用当前的库进行函数式编程,而是如何使用任何增强 JavaScript 函数式编程方法的库。本章不会专注于只有一两个库,而是将尽可能多地探索所有存在于 JavaScript 中的函数式编程风格。

JavaScript 的函数库

据说每个函数式程序员都会编写自己的函数库,函数式 JavaScript 程序员也不例外。如今的开源代码共享平台,如 GitHub、Bower 和 NPM,使得分享、合作和发展这些库变得更加容易。存在许多用于 JavaScript 函数式编程的库,从微小的工具包到庞大的模块库不等。

每个库都推广其自己的函数式编程风格。从严格的、基于数学的风格到轻松的、非正式的风格,每个库都不同,但它们都有一个共同的特点:它们都具有抽象的 JavaScript 函数功能,以增加代码重用、可读性和健壮性。

然而,在撰写本文时,尚未有一个库确立为事实上的标准。有人可能会认为underscore.js是其中一个,但正如你将在下一节中看到的,最好避免使用underscore.js

Underscore.js

在许多人眼中,Underscore 已成为标准的函数式 JavaScript 库。它成熟、稳定,并由Jeremy Ashkenas创建,他是Backbone.jsCoffeeScript库背后的人物。Underscore 实际上是 Ruby 的Enumerable模块的重新实现,这也解释了为什么 CoffeeScript 也受到 Ruby 的影响。

与 jQuery 类似,Underscore 不修改原生 JavaScript 对象,而是使用一个符号来定义自己的对象:下划线字符"_"。因此,使用 Underscore 的方式如下:

var x = _.map([1,2,3], Math.sqrt); // Underscore's map function
console.log(x.toString());

我们已经看到了 JavaScript 原生的Array对象的map()方法,它的工作方式如下:

var x = [1,2,3].map(Math.sqrt);

不同之处在于,在 Underscore 中,Array对象和callback()函数都作为参数传递给 Underscore 对象的map()方法(_.map),而不是仅将回调传递给数组的原生map()方法(Array.prototype.map)。

但 Underscore 不仅仅是map()和其他内置函数。它充满了非常方便的函数,比如find()invoke()pluck()sortyBy()groupBy()等等。

var greetings = [{origin: 'spanish', value: 'hola'}, 
{origin: 'english', value: 'hello'}];
console.log(_.pluck(greetings, 'value')  );
// Grabs an object's property.
// Returns: ['hola', 'hello']
console.log(_.find(greetings, function(s) {return s.origin == 
'spanish';}));
// Looks for the first obj that passes the truth test
// Returns: {origin: 'spanish', value: 'hola'}
greetings = greetings.concat(_.object(['origin','value'],
['french','bonjour']));
console.log(greetings);
// _.object creates an object literal from two merged arrays
// Returns: [{origin: 'spanish', value: 'hola'},
//{origin: 'english', value: 'hello'},
//{origin: 'french', value: 'bonjour'}]

它提供了一种将方法链接在一起的方式:

var g = _.chain(greetings)
  .sortBy(function(x) {return x.value.length})
  .pluck('origin')
  .map(function(x){return x.charAt(0).toUpperCase()+x.slice(1)})
  .reduce(function(x, y){return x + ' ' + y}, '')
  .value();
// Applies the functions 
// Returns: 'Spanish English French'
console.log(g);

注意

_.chain()方法返回一个包装对象,其中包含所有 Underscore 函数。然后使用_.value方法来提取包装对象的值。包装对象对于将 Underscore 与面向对象编程混合使用非常有用。

尽管它易于使用并被社区所接受,但underscore.js库因迫使你编写过于冗长的代码和鼓励错误的模式而受到批评。Underscore 的结构可能不是理想的,甚至不起作用!

直到版本 1.7.0 发布后不久,Brian Lonsdorf 的演讲嘿,Underscore,你做错了!在 YouTube 上发布,Underscore 明确阻止我们扩展map()reduce()filter()等函数。

_.prototype.map = function(obj, iterate, [context]) {
  if (Array.prototype.map && obj.map === Array.prototype.map) return obj.map(iterate, context);
  // ...
};

注意

您可以在www.youtube.com/watch?v=m3svKOdZij观看 Brian Lonsdorf 的演讲视频。

在范畴论的术语中,映射是一个同态函子接口(在第五章范畴论中有更多介绍)。我们应该能够根据需要为map定义一个函子。所以 Underscore 并不是非常函数式的。

由于 JavaScript 没有内置的不可变数据,一个函数库应该小心不要让它的辅助函数改变传递给它的对象。这个问题的一个很好的例子如下所示。片段的意图是返回一个新的selected列表,并将一个选项设置为默认值。但实际发生的是selected列表被就地改变。

function getSelectedOptions(id, value) {
  options = document.querySelectorAll('#' + id + ' option');
  var newOptions = _.map(options, function(opt){
    if (opt.text == value) {
      opt.selected = true;
      opt.text += ' (this is the default)';
    }
    else {
      opt.selected = false;
    }
    return opt;
  });
  return newOptions;
}
var optionsHelp = getSelectedOptions('timezones', 'Chicago');

我们必须在callback()函数中插入opt = opt.cloneNode();这一行,以便复制传递给函数的列表中的每个对象。Underscore 的map()函数作弊以提高性能,但这是以牺牲函数式的风水为代价。本地的Array.prototype.map()函数不需要这样做,因为它会复制,但它也不能用于nodelist集合。

Underscore 可能不太适合数学上正确的函数式编程,但它从来没有打算将 JavaScript 扩展或转换为纯函数式语言。它定义自己为一个提供大量有用的函数式编程辅助函数的 JavaScript 库。它可能不仅仅是一堆类似函数式的辅助函数,但它也不是一个严肃的函数库。

是否有更好的库?也许是基于数学的库?

Fantasy Land

有时,事实比小说更离奇。

Fantasy Land是一个功能基础库的集合,也是 JavaScript 中如何实现“代数结构”的正式规范。更具体地说,Fantasy Land 指定了常见代数结构或简称代数的互操作性:单子、幺半群、集合、函子、链等等。它们的名字听起来可能很可怕,但它们只是一组值、一组运算符和一些必须遵守的定律。换句话说,它们只是对象。

它是如何工作的。每个代数都是一个单独的 Fantasy Land 规范,并且可能依赖于需要实现的其他代数。

Fantasy Land

一些代数规范是:

  • 集合:

  • 实现反射、对称和传递定律

  • 定义equals()方法

  • 半群

  • 实现结合定律

  • 定义concat()方法

  • 幺半群

  • 实现右恒等和左恒等

  • 定义empty()方法

  • 函子

  • 实现恒等和组合定律

  • 定义map()方法

清单不断延续。

我们不一定需要确切知道每个代数的用途,但这肯定有所帮助,特别是如果你正在编写符合规范的自己的库。这不仅仅是抽象的胡言乱语,它概述了一种实现称为范畴论的高级抽象的方法。范畴论的完整解释可以在第五章范畴论中找到。

Fantasy Land 不仅告诉我们如何实现函数式编程,还为 JavaScript 提供了一组函数模块。然而,许多模块是不完整的,文档也相当稀少。但 Fantasy Land 并不是唯一一个实现其开源规范的库。其他库也有,比如:Bilby.js

Bilby.js

到底什么是 bilby?不,它不是一个可能存在于 Fantasy Land 中的神话生物。它存在于地球上,是一种奇怪/可爱的鼠和兔的混合物。尽管如此,bibly.js库符合 Fantasy Land 的规范。

事实上,bilby.js是一个严肃的函数库。正如其文档所述,它是严肃的,意味着它应用范畴论来实现高度抽象的代码。功能性的,意味着它实现了引用透明的程序。哇,这真的很严肃。文档位于bilby.brianmckenna.org/,并且提供了以下内容:

  • 用于特定多态性的不可变多方法

  • 函数式数据结构

  • 用于函数式语法的运算符重载

  • 自动化规范测试(ScalaCheckQuickCheck

迄今为止,符合 Fantasy Land 规范的最成熟的库是Bilby.js,它是致力于函数式风格的重要资源。

让我们试一个例子:

// environments in bilby are immutable structure for multimethods
var shapes1 = bilby.environment()
  // can define methods
  .method(
    'area', // methods take a name
    function(a){return typeof(a) == 'rect'}, // a predicate
    function(a){return a.x * a.y} // and an implementation
  )
  // and properties, like methods with predicates that always
  // return true
  .property(
     'name',   // takes a name
     'shape'); // and a function
// now we can overload it
var shapes2 = shapes1
  .method(
    'area', function(a){return typeof(a) == 'circle'},
    function(a){return a.r * a.r * Math.PI} );
var shapes3 = shapes2
  .method(
    'area', function(a){return typeof(a) == 'triangle'},
    function(a){return a.height * a.base / 2} );

// and now we can do something like this
var objs = [{type:'circle', r:5}, {type:'rect', x:2, y:3}];
var areas = objs.map(shapes3.area);

// and this
var totalArea = objs.map(shapes3.area).reduce(add);

这是范畴论和特定多态性的实践。再次强调,范畴论将在第五章中全面介绍,范畴论

注意

范畴论是函数式程序员最近振奋的数学分支,用于最大程度地抽象和提高代码的实用性。但有一个主要缺点:很难理解并快速上手。

事实上,Bilby 和 Fantasy Land 确实在 JavaScript 中推动了函数式编程的可能性。尽管看到计算机科学的发展是令人兴奋的,但世界可能还没有准备好接受 Bibly 和 Fantasy Land 所推动的那种硬核函数式风格。

也许这样一个位于函数式 JavaScript 前沿的宏伟库并不适合我们。毕竟,我们的目标是探索与 JavaScript 相辅相成的函数式技术,而不是建立函数式编程教条。让我们把注意力转向另一个新库,Lazy.js

Lazy.js

Lazy 是一个实用库,更接近于underscore.js库,但采用了惰性求值策略。因此,Lazy 通过函数式计算结果的方式实现了不会立即得到解释的系列。它还拥有显著的性能提升。

Lazy.js库仍然非常年轻。但它有很大的动力和社区热情支持。

Lazy 中的一切都是我们可以迭代的序列。由于库控制方法应用的顺序,可以实现许多非常酷的事情:异步迭代(并行编程),无限序列,函数式反应式编程等。

以下示例展示了一些内容:

// Get the first eight lines of a song's lyrics
var lyrics = "Lorem ipsum dolor sit amet, consectetur adipiscing eli
// Without Lazy, the entire string is first split into lines
console.log(lyrics.split('\n').slice(0,3)); 

// With Lazy, the text is only split into the first 8 lines
// The lyrics can even be infinitely long!
console.log(Lazy(lyrics).split('\n').take(3));

//First 10 squares that are evenly divisible by 3
var oneTo1000 = Lazy.range(1, 1000).toArray(); 
var sequence = Lazy(oneTo1000)
  .map(function(x) { return x * x; })
  .filter(function(x) { return x % 3 === 0; })
  .take(10)
  .each(function(x) { console.log(x); });

// asynchronous iteration over an infinite sequence
var asyncSequence = Lazy.generate(function(x){return x++})
  .async(100) // 0.100s intervals between elements
  .take(20) // only compute the first 20  
  .each(function(e) { // begin iterating over the sequence
    console.log(new Date().getMilliseconds() + ": " + e);
  });

更多示例和用例在第四章中有详细介绍,在 JavaScript 中实现函数式编程技术

但完全归功于Lazy.js库这个想法并不完全正确。它的前身之一,Bacon.js库,也是以类似的方式工作。

Bacon.js

Bacon.js库的标志如下:

Bacon.js

函数式编程库的有胡子的嬉皮士,Bacon.js本身是一个函数式响应式编程库。函数式响应式编程意味着使用函数式设计模式来表示具有反应性和不断变化的值,比如屏幕上鼠标的位置或公司股票的价格。就像 Lazy 可以通过在需要时不计算值来创建无限序列一样,Bacon 可以避免在最后一刻之前计算不断变化的值。

在 Lazy 中称为序列的东西在 Bacon 中称为 EventStreams 和 Properties,因为它们更适合处理事件(onmouseoveronkeydown等)和响应属性(滚动位置,鼠标位置,切换等)。

Bacon.fromEventTarget(document.body, "click")
  .onValue(function() { alert("Bacon!") });

Bacon 比 Lazy 要老一点,但它的功能集大约是一半大小,社区的热情也差不多。

荣誉提及

在这本书的范围内,有太多的库,无法对它们进行公正的评价。让我们再看看 JavaScript 中的一些函数式编程库。

  • Functional

  • 可能是 JavaScript 中第一个函数式编程库,Functional是一个包括全面的高阶函数支持以及string lambda 的库。

  • wu.js

  • 特别受欢迎的curryable()函数,wu.js库是一个非常好的函数式编程库。它是第一个(我知道的)实现惰性评估的库,为Bacon.jsLazy.js和其他库打下了基础

  • 是的,它是以臭名昭著的说唱组合Wu Tang Clan命名的

  • sloth.js

  • Lazy.js库非常相似,但比它小得多

  • stream.js

  • stream.js库支持无限流,除此之外没有太多功能

  • 绝对微小

  • Lo-Dash.js

  • 顾名思义,lo-dash.js库受到了underscore.js库的启发

  • 高度优化

  • Sugar

  • Sugar是 JavaScript 中函数式编程技术的支持库,类似于 Underscore,但在实现方式上有一些关键的不同。

  • 在 Underscore 中执行_.pluck(myObjs, 'value'),在 Sugar 中只需myObjs.map('value')。这意味着它修改了原生 JavaScript 对象,因此有一定风险,可能无法与其他执行相同操作的库(如 Prototype)很好地配合。

  • 非常好的文档,单元测试,分析器等。

  • from.js

  • 一个新的函数库和 JavaScript 的LINQ语言集成查询)引擎,支持大部分.NET 提供的相同 LINQ 函数

  • 100%的惰性评估和支持 lambda 表达式

  • 非常年轻,但文档非常好

  • JSLINQ

  • JavaScript 的另一个函数式 LINQ 引擎

  • from.js库更老,更成熟

  • Boiler.js

  • 另一个实用库,将 JavaScript 的函数方法扩展到更多的原语:字符串,数字,对象,集合和数组

  • Folktale

  • Bilby.js库一样,Folktale 是另一个实现 Fantasy Land 规范的新库。和它的前辈一样,Folktale 也是一个用于 JavaScript 中的函数式编程的库集合。它还很年轻,但可能会有一个光明的未来。

  • jQuery

  • 看到 jQuery 被提到感到惊讶?尽管 jQuery 不是用于执行函数式编程的工具,但它本身也是函数式的。jQuery 可能是最广泛使用的库之一,它的根源是函数式编程。

  • jQuery 对象实际上是一个单子。jQuery 使用单子定律来实现方法链式调用:

$('#mydiv').fadeIn().css('left': 50).alert('hi!');

关于这一点的详细解释可以在第七章中找到,JavaScript 中的函数式和面向对象编程

  • 它的一些方法是高阶的:
$('li').css('left': function(index){return index*50});
  • 从 jQuery 1.8 开始,deferred.then参数实现了一种称为 Promises 的函数概念。

  • jQuery 是一个抽象层,主要用于 DOM。它不是一个框架或工具包,只是一种利用抽象来增加代码重用和减少丑陋代码的方法。这难道不正是函数式编程的全部意义吗?

开发和生产环境

从编程风格的角度来看,应用程序是在哪种环境中开发和部署的并不重要。但对于库来说却很重要。

浏览器

大多数 JavaScript 应用程序都设计为在客户端运行,也就是在客户端的浏览器中。基于浏览器的环境非常适合开发,因为浏览器无处不在,你可以在本地机器上直接编写代码,解释器是浏览器的 JavaScript 引擎,所有浏览器都有开发者控制台。Firefox 的 FireBug 提供非常有用的错误消息,并允许设置断点等,但在 Chrome 和 Safari 中运行相同的代码以交叉参考错误输出通常也很有帮助。即使是 Internet Explorer 也包含开发者工具。

浏览器的问题在于它们以不同的方式评估 JavaScript!虽然不常见,但有可能编写的代码在不同的浏览器中返回非常不同的结果。但通常差异在于它们处理文档对象模型的方式,而不是原型和函数的工作方式。显然,Math.sqrt(4)方法对所有浏览器和 shell 都返回2。但scrollLeft方法取决于浏览器的布局策略。

编写特定于浏览器的代码是浪费时间,这也是为什么应该使用库的另一个原因。

服务器端 JavaScript

Node.js库已成为创建服务器端和基于网络的应用程序的标准平台。函数式编程可以用于服务器端应用程序编程吗?可以!好吧,但是否存在专为这种性能关键环境设计的函数式库?答案也是:是的。

本章中概述的所有函数式库都可以在Node.js库中工作,并且许多依赖于browserify.js模块来处理浏览器元素。

服务器端环境中的函数式用例

在我们这个充满网络系统的新世界中,服务器端应用程序开发人员经常关注并且理所当然地关注并发性。经典的例子是允许多个用户修改同一个文件的应用程序。但如果他们同时尝试修改它,就会陷入一团糟。这是困扰程序员几十年的状态维护问题。

假设以下情景:

  1. 一天早晨,亚当打开一个报告进行编辑,但在离开吃午饭前没有保存。

  2. 比利打开同样的报告,添加了他的笔记,然后保存了。

  3. 亚当从午饭回来,添加了他的笔记到报告中,然后保存了,无意中覆盖了比利的笔记。

  4. 第二天,比利发现他的笔记不见了。他的老板对他大喊大叫;每个人都生气了,他们联合起来对那个误入歧途的应用程序开发人员进行了不公正的解雇。

很长一段时间,解决这个问题的方法是创建一个关于文件的状态。当有人开始编辑时,切换锁定状态为on,这样其他人就无法编辑它,然后在保存后切换为off。在我们的情景中,比利在亚当回来吃午饭之前无法完成工作。如果从未保存(比如说,亚当决定在午饭休息时辞职),那么就永远无法编辑它。

这就是函数式编程关于不可变数据和状态(或缺乏状态)的想法真正可以发挥作用的地方。与其让用户直接修改文件,采用函数式方法,他们会修改文件的副本,也就是一个新的版本。如果他们试图保存该版本,而新版本已经存在,那么我们就知道其他人已经修改了旧版本。危机得以避免。

现在之前的情景会是这样展开的:

  1. 一天早晨,亚当打开一个报告进行编辑。但他在午餐前没有保存它。

  2. 比利打开相同的报告,添加他的笔记,并将其保存为新的修订版本。

  3. 亚当从午餐回来添加他的笔记。当他试图保存新的修订版本时,应用程序告诉他现在存在一个更新的修订版本。

  4. 亚当打开新的修订版本,添加了他的笔记,并保存了另一个新的修订版本。

  5. 通过查看修订历史,老板发现一切都运行顺利。每个人都很高兴,应用程序开发人员得到了晋升和加薪。

这被称为事件溯源。没有明确的状态需要维护,只有事件。这个过程更加清洁,有一个可以审查的明确事件历史。

这个想法和许多其他想法是为什么服务器端环境中的功能性编程正在兴起。

CLI

尽管 Web 和node.js库是两个主要的 JavaScript 环境,一些务实和冒险的用户正在寻找方法在命令行中使用 JavaScript。

将 JavaScript 用作命令行界面(CLI)脚本语言可能是应用函数编程的最佳机会之一。想象一下,当搜索本地文件或将整个 bash 脚本重写为功能性 JavaScript 一行时,能够使用惰性评估。

与其他 JavaScript 模块一起使用功能库

Web 应用程序由各种组件组成:框架、库、API 等。它们可以作为依赖项、插件或并存对象一起工作。

  • Backbone.js

  • 一个具有 RESTful JSON 接口的 MVP(模型-视图-提供者)框架

  • 需要underscore.js库,Backbone 的唯一硬依赖

  • jQuery

  • Bacon.js库具有与 jQuery 混合的绑定

  • Underscore 和 jQuery 非常好地互补了彼此

  • 原型 JavaScript 框架

  • 提供 JavaScript 与 Ruby 的 Enumerable 最接近的集合函数

  • Sugar.js

  • 修改本地对象及其方法

  • 在与其他库混合使用时必须小心,特别是 Prototype

编译为 JavaScript 的功能语言

有时,JavaScript 的内部功能上的 C 样式厚重外观足以让你想切换到另一种功能性语言。好吧,你可以!

  • Clojure 和 ClojureScript

  • 闭包是现代 Lisp 实现和功能齐全的功能语言

  • ClojureScript 将 Clojure 转译为 JavaScript

  • CoffeeScript

  • CoffeeScript 既是一种功能性语言的名称,也是一种将该语言转译为 JavaScript 的编译器。

  • CoffeeScript 中的表达式与 JavaScript 中的表达式之间存在一对一的映射

还有许多其他选择,包括 Pyjs,Roy,TypeScript,UHC 等。

总结

你选择使用哪个库取决于你的需求。需要功能性反应式编程来处理事件和动态值吗?使用Bacon.js库。只需要无限流而不需要其他东西吗?使用stream.js库。想要用功能性助手补充 jQuery 吗?试试underscore.js库。需要一个结构化环境来进行严肃的特定多态性吗?看看bilby.js库。需要一个全面的功能性编程工具吗?使用Lazy.js库。对这些选项都不满意吗?自己写一个!

任何库的好坏取决于它的使用方式。尽管本章概述的一些库存在一些缺陷,但大多数故障发生在键盘和椅子之间的某个地方。你需要正确使用库来满足你的需求。

如果我们将代码库导入 JavaScript 环境,也许我们也可以导入想法和原则。也许我们可以借鉴Python 之禅,由Tim Peter

美丽胜于丑陋

显式胜于隐式

简单胜于复杂

复杂胜于复杂

平面胜于嵌套

稀疏胜于密集

可读性很重要。

特殊情况并不特别到足以打破规则。

尽管实用性胜过纯粹。

错误不应该悄悄地通过。

除非明确要求保持沉默。

面对模棱两可,拒绝猜测的诱惑。

应该有一种——最好只有一种——明显的方法来做到这一点。

尽管这种方式一开始可能不明显,除非你是荷兰人。

现在总比永远好。

尽管永远往往比“现在”更好。

如果实现很难解释,那是个坏主意。

如果实现很容易解释,那可能是个好主意。

命名空间是一个非常好的主意——让我们做更多这样的事情!

第四章:在 JavaScript 中实现函数式编程技术

紧紧抓住你的帽子,因为我们现在真的要进入函数式思维模式了。

在本章中,我们将做以下事情:

  • 将所有核心概念整合成一个连贯的范式

  • 全面致力于函数式风格时,探索函数式编程所提供的美

  • 逐步推进函数式模式的逻辑进展

  • 同时,我们将构建一个简单的应用程序,做一些非常酷的事情

在上一章中,当处理 JavaScript 的函数式库时,您可能已经注意到了一些概念,但在《第二章》《函数式编程基础》中没有提到。好吧,这是有原因的!组合、柯里化、部分应用等。让我们探讨为什么以及这些库是如何实现这些概念的。

函数式编程可以采用多种风格和模式。本章将涵盖许多不同的函数式编程风格:

  • 数据泛型编程

  • 大部分是函数式编程

  • 函数响应式编程等

然而,本章将尽可能不偏向任何一种函数式编程风格。不过度倚重某种函数式编程风格,总体目标是展示有比通常被接受的正确和唯一的编码方式更好的方式。一旦你摆脱了对编写代码的先入为主的观念,你就可以随心所欲。当你只是出于喜欢而写代码,而不担心符合传统的做事方式时,那么可能性就是无限的。

部分函数应用和柯里化

许多语言支持可选参数,但 JavaScript 不支持。JavaScript 使用一种完全不同的模式,允许将任意数量的参数传递给函数。这为一些非常有趣和不寻常的设计模式留下了空间。函数可以部分或全部应用。

JavaScript 中的部分应用是将值绑定到一个或多个函数参数的过程,返回另一个接受剩余未绑定参数的函数。类似地,柯里化是将具有多个参数的函数转换为接受所需参数的另一个函数的过程。

现在两者之间的区别可能不太明显,但最终会显而易见。

函数操作

实际上,在我们进一步解释如何实现部分应用和柯里化之前,我们需要进行复习。如果我们要揭开 JavaScript 厚重的类 C 语法的外表,暴露它的函数式本质,那么我们需要了解 JavaScript 中原始值、函数和原型是如何工作的;如果我们只是想设置一些 cookie 或验证一些表单字段,我们就不需要考虑这些。

应用、调用和 this 关键字

在纯函数式语言中,函数不是被调用,而是被应用。JavaScript 也是如此,甚至提供了手动调用和应用函数的工具。而这一切都与 this 关键字有关,当然,它是函数的成员所属的对象。

call() 函数允许您将 this 关键字定义为第一个参数。它的工作方式如下:

console.log(['Hello', 'world'].join(' ')) // normal way
console.log(Array.prototype.join.call(['Hello', 'world'], ' ')); // using call

call() 函数可以用来调用匿名函数,例如:

console.log((function(){console.log(this.length)}).call([1,2,3]));

apply() 函数与 call() 函数非常相似,但更有用:

console.log(Math.max(1,2,3)); // returns 3
console.log(Math.max([1,2,3])); // won't work for arrays though
console.log(Math.max.apply(null, [1,2,3])); // but this will work

根本区别在于,call() 函数接受参数列表,而 apply() 函数接受参数数组。

call()apply()函数允许您编写一次函数,然后在其他对象中继承它,而无需重新编写函数。它们本身也是Function参数的成员。

注意

这是额外材料,但当您在自身上使用call()函数时,一些非常酷的事情可能会发生:

// these two lines are equivalent
func.call(thisValue);
Function.prototype.call.call(func, thisValue);

绑定参数

bind()函数允许您将一个方法应用于一个对象,并将this关键字分配给另一个对象。在内部,它与call()函数相同,但它链接到方法并返回一个新的绑定函数。

它在回调函数中特别有用,如下面的代码片段所示:

function Drum(){
  this.noise = 'boom';
  this.duration = 1000;
  this.goBoom = function(){console.log(this.noise)};
}
var drum = new Drum();
setInterval(drum.goBoom.bind(drum), drum.duration);

这解决了面向对象框架中的许多问题,比如 Dojo,特别是在使用定义自己的处理程序函数的类时维护状态的问题。但我们也可以将bind()函数用于函数式编程。

提示

bind()函数实际上可以自行进行部分应用,尽管方式非常有限。

函数工厂

还记得我们在第二章中关于闭包的部分吗,函数式编程基础?闭包是使得可能创建一种称为函数工厂的有用的 JavaScript 编程模式的构造。它们允许我们手动绑定参数到函数。

首先,我们需要一个将参数绑定到另一个函数的函数:

function bindFirstArg(func, a) {
  return function(b) {
    return func(a, b);
  };
}

然后我们可以使用这个函数创建更通用的函数:

var powersOfTwo = bindFirstArg(Math.pow, 2);
console.log(powersOfTwo(3)); // 8
console.log(powersOfTwo(5)); // 32

它也可以用于另一个参数:

function bindSecondArg(func, b) {
  return function(a) {
    return func(a, b);
  };
}
var squareOf = bindSecondArg(Math.pow, 2);
var cubeOf = bindSecondArg(Math.pow, 3);
console.log(squareOf(3)); // 9
console.log(squareOf(4)); // 16
console.log(cubeOf(3));   // 27
console.log(cubeOf(4));   // 64

在函数式编程中,创建通用函数的能力非常重要。但是有一个聪明的技巧可以使这个过程更加通用化。bindFirstArg()函数本身接受两个参数,第一个是函数。如果我们将bindFirstArg函数作为函数传递给它自己,我们就可以创建可绑定函数。以下示例最能描述这一点:

var makePowersOf = bindFirstArg(bindFirstArg, Math.pow);
var powersOfThree = makePowersOf(3);
console.log(powersOfThree(2)); // 9
console.log(powersOfThree(3)); // 27

这就是为什么它们被称为函数工厂。

部分应用

请注意,我们的函数工厂示例中的bindFirstArg()bindSecondArg()函数只适用于具有确切两个参数的函数。我们可以编写新的函数,使其适用于不同数量的参数,但这将偏离我们的通用化模型。

我们需要的是部分应用。

注意

部分应用是将值绑定到一个或多个函数参数的过程,返回一个接受剩余未绑定参数的部分应用函数。

bind()函数和Function对象的其他内置方法不同,我们必须为部分应用和柯里化创建自己的函数。有两种不同的方法可以做到这一点。

  • 作为一个独立的函数,也就是,var partial = function(func){...

  • 作为polyfill,也就是,Function.prototype.partial = function(){...

Polyfills 用于用新函数增加原型,并且允许我们将新函数作为我们想要部分应用的函数的方法来调用。就像这样:myfunction.partial(arg1, arg2, …);

从左侧进行部分应用

这就是 JavaScript 的apply()call()实用程序对我们有用的地方。让我们看一下 Function 对象的可能的 polyfill:

Function.prototype.partialApply = function(){
  var func = this; 
  args = Array.prototype.slice.call(arguments);
  return function(){
    return func.apply(this, args.concat(
      Array.prototype.slice.call(arguments)
    ));
  };
};

正如您所看到的,它通过切割arguments特殊变量来工作。

注意

每个函数都有一个特殊的本地变量称为arguments,它是传递给它的参数的类似数组的对象。它在技术上不是一个数组。因此它没有任何数组方法,比如sliceforEach。这就是为什么我们需要使用 Array 的slice.call方法来切割参数。

现在让我们看看当我们在一个例子中使用它时会发生什么。这一次,让我们远离数学,转而做一些更有用的事情。我们将创建一个小应用程序,将数字转换为十六进制值。

function nums2hex() {
  function componentToHex(component) {
    var hex = component.toString(16);
    // make sure the return value is 2 digits, i.e. 0c or 12
    if (hex.length == 1) {
      return "0" + hex;
    }
    else {
      return hex;
    }
  }
  return Array.prototype.map.call(arguments, componentToHex).join('');
}

// the function works on any number of inputs
console.log(nums2hex()); // ''
console.log(nums2hex(100,200)); // '64c8'
console.log(nums2hex(100, 200, 255, 0, 123)); // '64c8ff007b'

// but we can use the partial function to partially apply
// arguments, such as the OUI of a mac address
var myOUI = 123;
var getMacAddress = nums2hex.partialApply(myOUI);
console.log(getMacAddress()); // '7b'
console.log(getMacAddress(100, 200, 2, 123, 66, 0, 1)); // '7b64c8027b420001'

// or we can convert rgb values of red only to hexadecimal
var shadesOfRed = nums2hex.partialApply(255);
console.log(shadesOfRed(123, 0));   // 'ff7b00'
console.log(shadesOfRed(100, 200)); // 'ff64c8'

这个例子表明我们可以部分应用参数到一个通用函数,并得到一个新的函数作为返回。这个第一个例子是从左到右,这意味着我们只能部分应用第一个、最左边的参数。

从右侧进行部分应用

为了从右侧应用参数,我们可以定义另一个 polyfill。

Function.prototype.partialApplyRight = function(){
  var func = this; 
  args = Array.prototype.slice.call(arguments);
  return function(){
    return func.apply(
      this,
      [].slice.call(arguments, 0)
      .concat(args));
  };
};

var shadesOfBlue = nums2hex.partialApplyRight(255);
console.log(shadesOfBlue(123, 0));   // '7b00ff'
console.log(shadesOfBlue(100, 200)); // '64c8ff'

var someShadesOfGreen = nums2hex.partialApplyRight(255, 0);
console.log(shadesOfGreen(123));   // '7bff00'
console.log(shadesOfGreen(100));   // '64ff00'

部分应用使我们能够从一个非常通用的函数中提取更具体的函数。但这种方法最大的缺陷是参数传递的方式,即数量和顺序可能是模糊的。模糊从来不是编程中的好事。有更好的方法来做到这一点:柯里化。

柯里化

柯里化是将具有多个参数的函数转换为具有一个参数的函数的过程,该函数返回另一个根据需要接受更多参数的函数。形式上,具有 N 个参数的函数可以转换为 N 个函数的,每个函数只有一个参数。

一个常见的问题是:部分应用和柯里化之间有什么区别?虽然部分应用立即返回一个值,而柯里化只返回另一个接受下一个参数的柯里化函数,但根本区别在于柯里化允许更好地控制参数如何传递给函数。我们将看到这是真的,但首先我们需要创建执行柯里化的函数。

这是我们为 Function 原型添加柯里化的 polyfill:

Function.prototype.curry = function (numArgs) {
  var func = this;
  numArgs = numArgs || func.length;

  // recursively acquire the arguments
  function subCurry(prev) {
    return function (arg) {
      var args = prev.concat(arg);
      if (args.length < numArgs) {
        // recursive case: we still need more args
        return subCurry(args);
      }
      else {
        // base case: apply the function
        return func.apply(this, args);
      }
    };
  }
  return subCurry([]);
};

numArgs参数让我们可以选择指定柯里化函数需要的参数数量,如果没有明确定义的话。

让我们看看如何在我们的十六进制应用程序中使用它。我们将编写一个将 RGB 值转换为适用于 HTML 的十六进制字符串的函数:

function rgb2hex(r, g, b) {
  // nums2hex is previously defined in this chapter
  return '#' + nums2hex(r) + nums2hex(g) + nums2hex(b);
}
var hexColors = rgb2hex.curry();
console.log(hexColors(11)) // returns a curried function
console.log(hexColors(11,12,123)) // returns a curried function
console.log(hexColors(11)(12)(123)) // returns #0b0c7b
console.log(hexColors(210)(12)(0))  // returns #d20c00

它将返回柯里化函数,直到传入所有需要的参数。它们按照被柯里化函数定义的左到右的顺序传入。

但是我们可以再进一步,定义我们需要的更具体的函数如下:

var reds = function(g,b){return hexColors(255)(g)(b)};
var greens = function(r,b){return hexColors(r)(255)(b)};
var blues  = function(r,g){return hexColors(r)(g)(255)};
console.log(reds(11, 12))   // returns #ff0b0c
console.log(greens(11, 12)) // returns #0bff0c
console.log(blues(11, 12))  // returns #0b0cff

这是使用柯里化的一个好方法。但是,如果我们只想直接对nums2hex()进行柯里化,我们会遇到一些麻烦。那是因为该函数没有定义任何参数,它只允许您传入任意数量的参数。因此,我们必须定义参数的数量。我们可以使用 curry 函数的可选参数来设置被柯里化函数的参数数量。

var hexs = nums2hex.curry(2);
console.log(hexs(11)(12));     // returns 0b0c
console.log(hexs(11));         // returns function
console.log(hexs(110)(12)(0)); // incorrect

因此,柯里化不适用于接受可变数量参数的函数。对于这样的情况,部分应用更可取。

所有这些不仅仅是为了函数工厂和代码重用的好处。柯里化和部分应用都融入了一个更大的模式,称为组合。

函数组合

最后,我们已经到达了函数组合。

在函数式编程中,我们希望一切都是函数。如果可能的话,我们尤其希望是一元函数。如果我们可以将所有函数转换为一元函数,那么就会发生神奇的事情。

注意

一元函数是只接受一个输入的函数。具有多个输入的函数是多元的,但对于接受两个输入的函数,我们通常说是二元,对于三个输入的函数,我们说是三元。有些函数不接受特定数量的输入;我们称这些为可变元

操纵函数及其可接受的输入数量可以非常具有表现力。在本节中,我们将探讨如何从较小的函数组合新函数:将逻辑的小单元组合成整个程序,这些程序比单独的函数的总和更大。

组合

组合函数允许我们从许多简单的通用函数构建复杂的函数。通过将函数视为其他函数的构建块,我们可以构建具有出色可读性和可维护性的模块化应用程序。

在我们定义 compose() 的 polyfill 之前,您可以通过以下示例看到它是如何工作的:

var roundedSqrt = Math.round.compose(Math.sqrt)
console.log( roundedSqrt(5) ); // Returns: 2

var squaredDate =  roundedSqrt.compose(Date.parse)
console.log( squaredDate("January 1, 2014") ); // Returns: 1178370 

在数学中,fg 变量的组合被定义为 f(g(x))。在 JavaScript 中,这可以写成:

var compose = function(f, g) {
  return function(x) {
    return f(g(x));
  };
};

但如果我们就此结束,我们将失去 this 关键字的跟踪,还有其他问题。解决方案是使用 apply()call() 工具。与柯里化相比,compose() 的 polyfill 相当简单。

Function.prototype.compose = function(prevFunc) {
  var nextFunc = this;
  return function() {
    return nextFunc.call(this,prevFunc.apply(this,arguments));
  }
}

为了展示它的使用,让我们构建一个完全牵强的例子,如下所示:

function function1(a){return a + ' 1';}
function function2(b){return b + ' 2';}
function function3(c){return c + ' 3';}
var composition = function3.compose(function2).compose(function1);
console.log( composition('count') ); // returns 'count 1 2 3'

您是否注意到 function3 参数被首先应用了?这非常重要。函数是从右到左应用的。

序列 - 反向组合

因为许多人喜欢从左到右阅读,所以按照这个顺序应用函数可能是有意义的。我们将这称为序列而不是组合。

要颠倒顺序,我们只需要交换 nextFuncprevFunc 参数。

Function.prototype.sequence  = function(prevFunc) {
  var nextFunc = this;
  return function() {
    return prevFunc.call(this,nextFunc.apply(this,arguments));
  }
}

这使我们现在可以以更自然的顺序调用函数。

var sequences = function1.sequence(function2).sequence(function3);
console.log( sequences('count') ); // returns 'count 1 2 3'

组合与链

这里有五种不同的 floorSqrt() 函数组合实现。它们看起来是相同的,但值得仔细检查。

function floorSqrt1(num) {
  var sqrtNum = Math.sqrt(num);
  var floorSqrt = Math.floor(sqrtNum);
  var stringNum = String(floorSqrt);
  return stringNum;
}

function floorSqrt2(num) {
  return String(Math.floor(Math.sqrt(num)));
}

function floorSqrt3(num) {
  return [num].map(Math.sqrt).map(Math.floor).toString();
}
var floorSqrt4 = String.compose(Math.floor).compose(Math.sqrt);
var floorSqrt5 = Math.sqrt.sequence(Math.floor).sequence(String);

// all functions can be called like this:
floorSqrt<N>(17); // Returns: 4

但是有一些关键的区别我们应该了解:

  • 显然,第一种方法冗长且低效。

  • 第二种方法是一个很好的一行代码,但在应用了几个函数之后,这种方法变得非常难以阅读。

注意

说少量代码更好是错的。当有效指令更简洁时,代码更易维护。如果您减少屏幕上的字符数而不改变执行的有效指令,这将产生完全相反的效果——代码变得更难理解,维护性明显降低;例如,当我们使用嵌套的三元运算符,或者在一行上链接多个命令。这些方法减少了屏幕上的 '代码量',但并没有减少代码实际指定的步骤数。因此,这种简洁性使得代码更易维护的方式是有效地减少指定的指令(例如,通过使用更简单的算法来实现相同结果,或者仅仅用消息替换代码,例如,使用具有良好文档化 API 的第三方库)。

  • 第三种方法是一系列数组函数的链,特别是 map 函数。这很有效,但在数学上不正确。

  • 这是我们的 compose() 函数的实际应用。所有方法都被强制成一元的、纯函数,鼓励使用更好、更简单、更小的函数,只做一件事并且做得很好。

  • 最后一种方法使用了 compose() 函数的反向顺序,这同样有效。

使用 compose 进行编程

组合最重要的方面是,除了应用的第一个函数之外,它最适合使用纯 一元 函数:只接受一个参数的函数。

应用的第一个函数的输出被发送到下一个函数。这意味着函数必须接受前一个函数传递给它的内容。这是 类型签名 的主要影响。

注意

类型签名用于明确声明函数接受的输入类型和输出类型。它们最初由 Haskell 使用,在函数定义中由编译器使用。但在 JavaScript 中,我们只是将它们放在代码注释中。它们看起来像这样:foo :: arg1 -> argN -> output

示例:

// getStringLength :: String -> Intfunction getStringLength(s){return s.length};
// concatDates :: Date -> Date -> [Date]function concatDates(d1,d2){return [d1, d2]};
// pureFunc :: (int -> Bool) -> [int] -> [int]pureFunc(func, arr){return arr.filter(func)} 

为了真正享受组合的好处,任何应用都需要大量的一元、纯函数。这些是组合成更大函数的构建块,反过来又用于制作非常模块化、可靠和易维护的应用程序。

让我们通过一个例子来了解。首先我们需要许多构建块函数。其中一些函数是基于其他函数构建的,如下所示:

// stringToArray :: String -> [Char]
function stringToArray(s) { return s.split(''); }

// arrayToString :: [Char] -> String
function arrayToString(a) { return a.join(''); }

// nextChar :: Char -> Char
function nextChar(c) { 
  return String.fromCharCode(c.charCodeAt(0) + 1); }

// previousChar :: Char -> Char
function previousChar(c) {
  return String.fromCharCode(c.charCodeAt(0)-1); }

// higherColorHex :: Char -> Char
function higherColorHex(c) {return c >= 'f' ? 'f' :
                                   c == '9' ? 'a' :
                                   nextChar(c)}

// lowerColorHex :: Char -> Char
function lowerColorHex(c) { return c <= '0' ? '0' : 
                                   c == 'a' ? '9' : 
                                   previousChar(c); }

// raiseColorHexes :: String -> String
function raiseColorHexes(arr) { return arr.map(higherColorHex); }

// lowerColorHexes :: String -> String
function lowerColorHexes(arr) { return arr.map(lowerColorHex); }

现在让我们将其中一些组合在一起。

var lighterColor = arrayToString
  .compose(raiseColorHexes)
  .compose(stringToArray)
  var darkerColor = arrayToString
  .compose(lowerColorHexes)
  .compose(stringToArray)

console.log( lighterColor('af0189') ); // Returns: 'bf129a'
console.log( darkerColor('af0189')  );  // Returns: '9e0078'

我们甚至可以将compose()curry()函数一起使用。事实上,它们在一起工作得非常好。让我们将柯里化示例与我们的组合示例结合起来。首先我们需要之前的辅助函数。

// component2hex :: Ints -> Int
function componentToHex(c) {
  var hex = c.toString(16);
  return hex.length == 1 ? "0" + hex : hex;
}

// nums2hex :: Ints* -> Int
function nums2hex() {
  return Array.prototype.map.call(arguments, componentToHex).join('');
}

首先我们需要制作柯里化和部分应用的函数,然后我们可以将它们组合到我们的其他组合函数中。

var lighterColors = lighterColor
  .compose(nums2hex.curry());
var darkerRed = darkerColor
  .compose(nums2hex.partialApply(255));
Var lighterRgb2hex = lighterColor
  .compose(nums2hex.partialApply());

console.log( lighterColors(123, 0, 22) ); // Returns: 8cff11 
console.log( darkerRed(123, 0) ); // Returns: ee6a00 
console.log( lighterRgb2hex(123,200,100) ); // Returns: 8cd975

这就是我们的内容!这些函数读起来非常流畅,而且意义深远。我们被迫从只做一件事的小函数开始。然后我们能够组合具有更多实用性的函数。

让我们来看最后一个例子。这是一个通过可变量来减轻 RBG 值的函数。然后我们可以使用组合来从中创建新的函数。

// lighterColorNumSteps :: string -> num -> string
function lighterColorNumSteps(color, n) {
  for (var i = 0; i < n; i++) {
    color = lighterColor(color);
  }
  return color;
}

// now we can create functions like this:
var lighterRedNumSteps = lighterColorNumSteps.curry().compose(reds)(0,0);

// and use them like this:
console.log( lighterRedNumSteps(5) ); // Return: 'ff5555'
console.log( lighterRedNumSteps(2) ); // Return: 'ff2222'

同样,我们可以轻松地创建更多用于创建更浅或更深的蓝色、绿色、灰色、紫色等的函数。这是构建 API 的一个非常好的方法

我们只是刚刚触及了函数组合的表面。组合的作用是夺走 JavaScript 的控制权。通常 JavaScript 会从左到右进行评估,但现在解释器在说“好的,其他东西会处理这个,我只会继续下一个。”现在compose()函数控制着评估顺序!

这就是Lazy.jsBacon.js等库能够实现诸如惰性评估和无限序列等功能的方式。接下来,我们将看看这些库是如何使用的。

大部分是函数式编程

没有副作用的程序算不上是程序。

用不可避免产生副作用的函数式代码来补充我们的代码可以称为“大部分是函数式编程”。在同一个代码库中使用多种范式,并在最优的地方应用它们,是最佳的方法。大部分是函数式编程是即使是纯粹的、传统的函数式程序也是如何建模的:将大部分逻辑放在纯函数中,并与命令式代码进行接口。

这就是我们将要编写自己的一个小应用程序的方式。

在这个例子中,我们有一个老板告诉我们,我们的公司需要一个网页应用来跟踪员工的可用性状态。这家虚构公司的所有员工只有一个工作:使用我们的网站。员工到达工作地点时会签到,离开时会签退。但这还不够,它还需要在内容发生变化时自动更新,这样我们的老板就不必一直刷新页面了。

我们将使用 Lazy.js 作为我们的函数库。而且我们也会变得懒惰:不用担心处理所有用户的登录和退出、WebSockets、数据库等等,我们只需假装有一个通用的应用对象来为我们做这些,并且恰好具有完美的 API。

所以现在,让我们先把丑陋的部分搞定,也就是那些接口和产生副作用的部分。

function Receptor(name, available){
  this.name = name;
  this.available = available; // mutable state
  this.render = function(){
    output = '<li>';
    output += this.available ? 
      this.name + ' is available' : 
      this.name + ' is not available';
    output += '</li>';
    return output;
  }
}
var me = new Receptor;
var receptors = app.getReceptors().push(me);
app.container.innerHTML = receptors.map(function(r){
  return r.render();
}).join('');

这对于只显示可用性列表来说已经足够了,但我们希望它是响应式的,这就带来了我们的第一个障碍。

通过使用Lazy.js库将对象存储在一个序列中,直到调用toArray()方法才会实际计算任何内容,我们可以利用其惰性来提供一种函数式响应式编程。

var lazyReceptors = Lazy(receptors).map(function(r){
  return r.render();
});
app.container.innerHTML = lazyReceptors.toArray().join('');

因为Receptor.render()方法返回新的 HTML 而不是修改当前的 HTML,我们只需要将innerHTML参数设置为它的输出。

我们还需要相信,我们用于用户管理的通用应用程序将为我们提供回调方法供我们使用。

app.onUserLogin = function(){
  this.available = true;
  app.container.innerHTML = lazyReceptors.toArray().join('');
};
app.onUserLogout = function(){
  this.available = false;
  app.container.innerHTML = lazyReceptors.toArray().join('');
};

这样,每当用户登录或退出时,lazyReceptors参数将被重新计算,并且可用性列表将以最新的值打印出来。

处理事件

但是,如果应用程序没有提供用户登录和注销时的回调怎么办?回调很混乱,很快就会使程序变得混乱。相反,我们可以通过直接观察用户来确定。如果用户关注网页,那么他/她必须是活跃和可用的。我们可以使用 JavaScript 的focusblur事件来实现这一点。

window.addEventListener('focus', function(event) {
  me.available = true;
  app.setReceptor(me.name, me.available); // just go with it
  container.innerHTML = lazyReceptors.toArray().join('');
});
window.addEventListener('blur', function(event) {
  me.available = false;
  app.setReceptor(me.name, me.available);
  container.innerHTML = lazyReceptors.toArray().join('');
});

等一下,事件也是响应式的吗?它们也可以懒计算吗?在Lazy.js库中可以,甚至还有一个方便的方法。

var focusedReceptors = Lazy.events(window, "focus").each(function(e){
  me.available = true;
  app.setReceptor(me.name, me.available);
  container.innerHTML = lazyReceptors.toArray().join('');
});
var blurredReceptors = Lazy.events(window, "blur").each(function(e){
  me.available = false;
  app.setReceptor(me.name, me.available);
  container.innerHTML = lazyReceptors.toArray().join('');
});

简单得很。

注意

通过使用Lazy.js库来处理事件,我们可以创建一个无限序列的事件。每次事件触发时,Lazy.each()函数都能再次迭代。

我们的老板到目前为止很喜欢这个应用,但她指出,如果员工在离开前从未注销并关闭页面,那么应用会显示员工仍然可用。

要确定员工在网站上是否活跃,我们可以监视键盘和鼠标事件。假设在 30 分钟没有活动后,他们被视为不可用。

var timeout = null;
var inputs = Lazy.events(window, "mousemove").each(function(e){
  me.available = true;
  container.innerHTML = lazyReceptors.toArray().join('');
  clearTimeout(timeout);
  timeout = setTimeout(function(){
    me.available = false;
    container.innerHTML = lazyReceptors.toArray().join('');
  }, 1800000); // 30 minutes
});

Lazy.js库让我们很容易地处理事件,将其作为一个可以映射的无限流。这是可能的,因为它使用函数组合来控制执行顺序。

但这里有一个小问题。如果没有用户输入事件可以依附呢?相反,如果有一个属性值一直在变化呢?在下一节中,我们将详细调查这个问题。

函数式响应式编程

让我们构建另一种工作方式基本相同的应用程序;一个使用函数式编程来对状态变化做出反应的应用程序。但是,这次应用程序不能依赖事件监听器。

想象一下,你在一家新闻媒体公司工作,你的老板告诉你要创建一个网络应用,用于跟踪选举日政府选举结果。数据不断地流入,因为当地选区提交他们的结果时,页面上要显示的结果是非常反应灵敏的。但我们还需要按地区跟踪结果,因此会有多个对象要跟踪。

与其创建一个大的面向对象的层次结构来建模界面,我们可以将其声明性地描述为不可变数据。我们可以使用纯函数和半纯函数的链式转换,其最终副作用仅是更新绝对必须保留的状态位(理想情况下,不多)。

我们将使用Bacon.js库,它将允许我们快速开发函数式响应式编程FRP)应用程序。该应用程序一年只会在一天(选举日)使用一次,我们的老板认为它应该花费相应的时间。通过函数式编程和Bacon.js这样的库,我们将在一半的时间内完成。

但首先,我们需要一些对象来表示投票区域,比如州、省、地区等。

function Region(name, percent, parties){
  // mutable properties:
  this.name = name;
  this.percent = percent; // % of precincts reported
  this.parties = parties; // political parties

  // return an HTML representation
  this.render = function(){
    var lis = this.parties.map(function(p){
      return '<li>' + p.name + ': ' + p.votes + '</li>';
    });
    var output = '<h2>' + this.name + '</h2>';
    output += '<ul>' + lis.join('') + '</ul>'; 
    output += 'Percent reported: ' + this.percent; 
    return output;
  }
}
function getRegions(data) {
  return JSON.parse(data).map(function(obj){
    return new Region(obj.name, obj.percent, obj.parties);
  });
}
var url = 'http://api.server.com/election-data?format=json';
var data = jQuery.ajax(url);
var regions = getRegions(data);
app.container.innerHTML = regions.map(function(r){
  return r.render();
}).join('');

虽然以上内容对于仅显示静态选举结果列表已经足够了,但我们需要一种动态更新区域的方法。是时候煮一些 Bacon 和 FRP 了。

响应性

Bacon 有一个函数Bacon.fromPoll(),它让我们创建一个事件流,其中事件只是在给定间隔上调用的函数。而stream.subscribe()函数让我们订阅一个处理函数到流中。因为它是懒惰的,流没有订阅者时实际上不会执行任何操作。

var eventStream = Bacon.fromPoll(10000, function(){
  return Bacon.Next;
});
var subscriber = eventStream.subscribe(function(){
  var url = 'http://api.server.com/election-data?format=json';
  var data = jQuery.ajax(url);
  var newRegions = getRegions(data);	
  container.innerHTML = newRegions.map(function(r){
    return r.render();
  }).join('');
});

通过将其放入每 10 秒运行一次的循环中,我们可以完成任务。但这种方法会频繁地 ping 网络,效率非常低下。这并不是很实用。相反,让我们深入了解一下Bacon.js库。

在 Bacon 中,有 EventStreams 和 Properties 参数。属性可以被认为是随时间变化的“魔术”变量,以响应事件。它们并不真的是魔术,因为它们仍然依赖于事件流。属性随时间变化,与其 EventStream 相关。

Bacon.js库还有另一个技巧。Bacon.fromPromise()函数是一种通过promises向流发出事件的方法。而且自 jQuery 版本 1.5.0 起,jQuery AJAX 实现了 promises 接口。所以我们只需要编写一个在异步调用完成时发出事件的 AJAX 搜索函数。每当承诺被解决时,它都会调用 EventStream 的订阅者。

var url = 'http://api.server.com/election-data?format=json';
var eventStream = Bacon.fromPromise(jQuery.ajax(url));
var subscriber = eventStream.onValue(function(data){
  newRegions = getRegions(data);
  container.innerHTML = newRegions.map(function(r){
    return r.render();
  }).join('');
}

承诺可以被认为是最终值;使用Bacon.js库,我们可以懒惰地等待最终值。

将所有内容整合在一起

现在我们已经涵盖了响应性,我们终于可以玩一些代码了。

我们可以使用纯函数的链式修改订阅者,做一些诸如累加总和和过滤不需要的结果的操作,而且我们都是在我们创建的按钮的onclick()处理函数中完成的。

// create the eventStream out side of the functions
var eventStream = Bacon.onPromise(jQuery.ajax(url));
var subscribe = null;
var url = 'http://api.server.com/election-data?format=json';

// our un-modified subscriber
$('button#showAll').click(function() {
  var subscriber = eventStream.onValue(function(data) {
    var newRegions = getRegions(data).map(function(r) {
      return new Region(r.name, r.percent, r.parties);
    });
    container.innerHTML = newRegions.map(function(r) {
      return r.render();
    }).join('');
  });
});

// a button for showing the total votes
$('button#showTotal').click(function() {
  var subscriber = eventStream.onValue(function(data) {
    var emptyRegion = new Region('empty', 0, [{
      name: 'Republican', votes: 0
    }, {
      name: 'Democrat', votes: 0
    }]);
    var totalRegions = getRegions(data).reduce(function(r1, r2) {
      newParties = r1.parties.map(function(x, i) {
      return {
        name: r1.parties[i].name,
        votes: r1.parties[i].votes + r2.parties[i].votes
      };
    });
    newRegion = new Region('Total', (r1.percent + r2.percent) / 2, newParties);
    return newRegion;
    }, emptyRegion);
    container.innerHTML = totalRegions.render();
  });
});

// a button for only displaying regions that are reporting > 50%
$('button#showMostlyReported').click(function() {
  var subscriber = eventStream.onValue(function(data) {
    var newRegions = getRegions(data).map(function(r) {
      if (r.percent > 50) return r;
      else return null;
    }).filter(function(r) {return r != null;});
    container.innerHTML = newRegions.map(function(r) {
      return r.render();
    }).join('');
  });
});

美妙之处在于,当用户在按钮之间点击时,事件流不会改变,但订阅者会改变,这使得一切都能顺利运行。

总结

JavaScript 是一种美丽的语言。

它的内在美真正闪耀在函数式编程中。这正是赋予它出色可扩展性的力量。它允许可以做很多事情的头等函数,这正是打开函数式大门的原因。概念在彼此之上构建,不断堆叠。

在本章中,我们首先深入了解了 JavaScript 中的函数式范式。我们涵盖了函数工厂、柯里化、函数组合以及使其工作所需的一切。我们构建了一个极其模块化的应用程序,使用了这些概念。然后我们展示了如何使用一些使用这些概念的函数式库,即函数组合,来操纵执行顺序。

在本章中,我们涵盖了几种函数式编程风格:数据通用编程、大部分函数式编程和函数式响应式编程。它们彼此并没有太大的不同,它们只是在不同情况下应用函数式编程的不同模式。

在上一章中,简要提到了范畴论。在下一章中,我们将学习更多关于它是什么以及如何使用它。

第五章:范畴论

托马斯·沃森曾经著名地说过:“我认为世界市场上可能只需要五台计算机。”那是在 1948 年。当时,每个人都知道计算机只会用于两件事:数学和工程。甚至科技界最伟大的头脑也无法预测,有一天,计算机将能够将西班牙语翻译成英语,或者模拟整个天气系统。当时,最快的机器是 IBM 的 SSEC,每秒进行 50 次乘法运算,显示终端要等 15 年才会出现,多处理意味着多个用户终端共享一个处理器。晶体管改变了一切,但科技的远见者仍然未能抓住要点。肯·奥尔森在 1977 年又做了一个著名的愚蠢预测,他说:“没有理由让任何人在家里放一台计算机。”

现在对我们来说很明显,计算机不仅仅是为科学家和工程师准备的,但这是事后诸葛亮。70 年前,机器不仅仅能做数学这个想法一点都不直观。沃森不仅没有意识到计算机如何改变社会,他也没有意识到数学的变革和发展力量。

但是,计算机和数学的潜力并没有被所有人忽视。约翰·麦卡锡在 1958 年发明了Lisp,这是一种革命性的基于算法的语言,开启了计算机发展的新时代。自诞生以来,Lisp 在使用抽象层(编译器、解释器、虚拟化)推动计算机从严格的数学机器发展到今天的样子方面发挥了重要作用。

从 Lisp 出现了Scheme,它是 JavaScript 的直接祖先。现在我们又回到了原点。如果计算机在本质上只是做数学,那么基于数学的编程范式就会表现出色是理所当然的。

这里使用的“数学”一词并不是用来描述计算机显然可以做的“数字计算”,而是用来描述离散数学:研究离散数学结构的学科,比如逻辑陈述或计算机语言的指令。通过将代码视为离散数学结构,我们可以将数学中的概念和思想应用到其中。这就是为什么函数式编程在人工智能、图搜索、模式识别和计算机科学中的其他重大挑战中如此重要。

在本章中,我们将尝试一些这些概念及其在日常编程挑战中的应用。它们将包括:

  • 范畴论

  • 态射

  • 函子

  • 可能性

  • 承诺

  • 镜头

  • 函数组合

有了这些概念,我们将能够非常轻松和安全地编写整个库和 API。我们将从解释范畴论到在 JavaScript 中正式实现它。

范畴论

范畴论是赋予函数组合力量的理论概念。范畴论和函数组合就像发动机排量和马力,像 NASA 和航天飞机,像好啤酒和杯子一样紧密相连。基本上,一个离不开另一个。

范畴论简介

范畴论实际上并不是一个太难的概念。它在数学中的地位足以填满整个研究生课程,但它在计算机编程中的地位可以很容易地总结起来。

爱因斯坦曾说过:“如果你不能向一个 6 岁的孩子解释清楚,那么你自己也不懂。”因此,在向一个 6 岁的孩子解释的精神下,范畴论就是连接点。虽然这可能严重简化了范畴论,但它确实很好地以直接的方式解释了我们需要知道的内容。

首先,您需要了解一些术语。范畴只是具有相同类型的集合。在 JavaScript 中,它们是包含明确定义为数字、字符串、布尔值、日期、节点等的变量的数组或对象。态射是纯函数,当给定特定的输入集时,总是返回相同的输出。同态操作限于单个范畴,而多态操作可以在多个范畴上操作。例如,同态函数乘法只对数字起作用,但多态函数加法也可以对字符串起作用。

范畴论简介

下图显示了三个范畴——A、B 和 C——和两个态射——ƒɡ

范畴论告诉我们,当我们有两个态射,其中第一个的范畴是另一个的预期输入时,它们可以组合成以下内容:

范畴论简介

ƒ o g符号是态射ƒg的组合。现在我们可以连接点了。

范畴论简介

这就是它的全部内容,只是连接点。

类型安全

让我们连接一些点。范畴包含两个东西:

  1. 对象(在 JavaScript 中,类型)。

  2. 态射(在 JavaScript 中,只对类型起作用的纯函数)。

这些是数学家给范畴论的术语,所以我们的 JavaScript 术语中存在一些不幸的命名重载。范畴论中的对象更像是具有显式数据类型的变量,而不是 JavaScript 对象定义中的属性和值的集合。态射只是使用这些类型的纯函数。

因此,将范畴论的思想应用到 JavaScript 中非常容易。在 JavaScript 中使用范畴论意味着每个范畴都使用一种特定的数据类型。数据类型包括数字、字符串、数组、日期、对象、布尔值等。但是,在 JavaScript 中没有严格的类型系统,事情可能会出错。因此,我们将不得不实现自己的方法来确保数据是正确的。

JavaScript 中有四种原始数据类型:数字、字符串、布尔值和函数。我们可以创建类型安全函数,它们要么返回变量,要么抛出错误。这满足了范畴的对象公理

var str = function(s) {
  if (typeof s === "string") {
    return s;
  }
  else {
    throw new TypeError("Error: String expected, " + typeof s + " given.");   
  }
}
var num = function(n) {
  if (typeof n === "number") {
    return n;
  }
  else {
    throw new TypeError("Error: Number expected, " + typeof n + " given.");   
  }
}
var bool = function(b) {
  if (typeof b === "boolean") {
    return b;
  }
  else {
    throw new TypeError("Error: Boolean expected, " + typeof b + " given.");   
  }
}
var func = function(f) {
  if (typeof f === "function") {
    return f;
  }
  else {
    throw new TypeError("Error: Function expected, " + typeof f + " given.");   
  }
}

然而,这里有很多重复的代码,这并不是很实用。相反,我们可以创建一个返回另一个函数的函数,这个函数是类型安全函数。

var typeOf = function(type) {
  return function(x) {
    if (typeof x === type) {
      return x;
    }
    else {
      throw new TypeError("Error: "+type+" expected, "+typeof x+" given.");
    }
  }
}
var str = typeOf('string'),
  num = typeOf('number'),
  func = typeOf('function'),
  bool = typeOf('boolean');

现在,我们可以使用它们来确保我们的函数表现如预期。

// unprotected method:
var x = '24';
x + 1; // will return '241', not 25

// protected method
// plusplus :: Int -> Int
function plusplus(n) {
  return num(n) + 1;
}
plusplus(x); // throws error, preferred over unexpected output

让我们看一个更有意思的例子。如果我们想要检查由 JavaScript 函数Date.parse()返回的 Unix 时间戳的长度,而不是作为字符串而是作为数字,那么我们将不得不使用我们的str()函数。

// timestampLength :: String -> Int
function timestampLength(t) { return num(**str(t)**.length); }
timestampLength(Date.parse('12/31/1999')); // throws error
timestampLength(Date.parse('12/31/1999')
  .toString()); // returns 12

像这样明确地将一种类型转换为另一种类型(或相同类型)的函数被称为态射这满足了范畴论的态射公理。通过类型安全函数和使用它们的态射强制类型声明,这些都是我们在 JavaScript 中表示范畴概念所需要的一切。

对象标识

还有一个重要的数据类型:对象。

var obj = typeOf('object');
obj(123); // throws error
obj({x:'a'}); // returns {x:'a'}

然而,对象是不同的。它们可以被继承。除了原始的数字、字符串、布尔值和函数之外,一切都是对象,包括数组、日期、元素等。

没有办法知道某个对象是什么类型,比如从typeof关键字知道 JavaScript 的一个子类型是什么,所以我们将不得不 improvisation。对象有一个toString()函数,我们可以利用它来实现这个目的。

var obj = function(o) {
  if (Object.prototype.toString.call(o)==="[object Object]") {
    return o;
  }
  else {
    throw new TypeError("Error: Object expected, something else given."); 
  }
}

再次,有了所有这些对象,我们应该实现一些代码重用。

var objectTypeOf = function(name) {
  return function(o) {
    if (Object.prototype.toString.call(o) === "[object "+name+"]") {
      return o;
    }
    else {
      throw new TypeError("Error: '+name+' expected, something else given.");
    }
  }
}
var obj = objectTypeOf('Object');
var arr = objectTypeOf('Array');
var date = objectTypeOf('Date');
var div = objectTypeOf('HTMLDivElement');

这些将对我们接下来的主题非常有用:函子。

函子

虽然态射是类型之间的映射,函数器是范畴之间的映射。它们可以被看作是将值从容器中提取出来,对其进行态射,然后将其放入新的容器中的函数。第一个输入是类型的态射,第二个输入是容器。

注意

函数器的类型签名如下:

// myFunctor :: (a -> b) -> f a -> f b

这意味着,“给我一个接受a并返回b的函数和一个包含a的盒子,我会返回一个包含b的盒子”。

创建函数器

事实证明,我们已经有一个函数器:map()。它从容器中(数组)获取值,并对其应用函数。

[1, 4, 9].map(Math.sqrt); // Returns: [1, 2, 3]

但是,我们需要将其编写为全局函数,而不是数组对象的方法。这将使我们能够以后编写更清洁、更安全的代码。

// map :: (a -> b) -> [a] -> [b]
var map = function(f, a) {
  return arr(a).map(func(f));
}

这个例子看起来像一个人为的包装,因为我们只是依赖map()函数。但它有一个目的。它为其他类型的映射提供了一个模板。

// strmap :: (str -> str) -> str -> str
var strmap = function(f, s) {
  return str(s).split('').map(func(f)).join('');
}

// MyObject#map :: (myValue -> a) -> a
MyObject.prototype.map(f{
  return func(f)(this.myValue);
}

数组和函数器

在函数式 JavaScript 中,数组是处理数据的首选方式。

有没有一种更简单的方法来创建已经分配给态射的函数器?是的,它被称为arrayOf。当你传入一个期望整数并返回一个数组的态射时,你会得到一个期望整数数组并返回一个数组的态射。

它本身不是一个函数器,但它允许我们从态射创建函数器。

// arrayOf :: (a -> b) -> ([a] -> [b])
var arrayOf = function(f) {
  return function(a) {
    return map(func(f), arr(a));
  }
}

以下是如何使用态射创建函数器:

var plusplusall = arrayOf(plusplus); // plusplus is our morphism
console.log( plusplusall([1,2,3]) ); // returns [2,3,4]
console.log( plusplusall([1,'2',3]) ); // error is thrown

arrayOf函数器的有趣属性是它也适用于类型安全。当你传入字符串的类型安全函数时,你会得到一个字符串数组的类型安全函数。类型安全被视为恒等函数态射。这对于确保数组包含所有正确的类型非常有用。

var strs = arrayOf(str);
console.log( strs(['a','b','c']) ); // returns ['a','b','c']
console.log( strs(['a',2,'c']) ); // throws error

重新审视函数组合

函数是我们可以为其创建一个函数器的另一种原始类型。这个函数器被称为fcompose。我们将函数器定义为从容器中取出一个值并对其应用函数的东西。当容器是一个函数时,我们只需调用它以获取其内部值。

我们已经知道函数组合是什么,但让我们看看它们在范畴论驱动的环境中能做什么。

函数组合是可结合的。如果你的高中代数老师像我的一样,她教你这个性质什么,但没有教你它能什么。在实践中,组合是可结合性能做的事情。

重新审视函数组合重新审视函数组合

我们可以进行任何内部组合,不管它是如何分组的。这与交换律属性不同。ƒ o g并不总是等于g o ƒ。换句话说,字符串的第一个单词的反向不同于字符串反向的第一个单词。

这一切意味着,不管应用了哪些函数以及顺序如何,只要每个函数的输入来自前一个函数的输出,就没有关系。但是,等等,如果右边的函数依赖于左边的函数,那么难道只能有一种评估顺序吗?从左到右?是的,但如果它被封装起来,那么我们可以根据自己的意愿来控制它。这就是 JavaScript 中懒惰评估的强大之处。

重新审视函数组合

让我们重新编写函数组合,不是作为函数原型的扩展,而是作为一个独立的函数,这将允许我们更充分地利用它。基本形式如下:

var fcompose = function(f, g) {
  return function() {
    return f.call(this, g.apply(this, arguments));
  };
};

但是我们需要它能够处理任意数量的输入。

var fcompose = function() {
  // first make sure all arguments are functions
  var funcs = arrayOf(func)(arguments);

  // return a function that applies all the functions
  return function() {
    var argsOfFuncs = arguments;
    for (var i = funcs.length; i > 0; i -= 1) {
      argsOfFuncs  = [funcs[i].apply(this, args)];
    }
    return args[0];
  };
};

// example:
var f = fcompose(negate, square, mult2, add1);
f(2); // Returns: -36

现在我们已经封装了这些函数,我们对它们有了控制。我们可以重写 compose 函数,使得每个函数都接受另一个函数作为输入,存储它,并返回一个做同样事情的对象。我们可以接受一个单一数组作为输入,对源中的每个元素执行所有操作(每个map()filter()等等,组合在一起),最后将结果存储在一个新数组中。这是通过函数组合实现的惰性评估。这里没有理由重新发明轮子。许多库都有这个概念的很好的实现,包括Lazy.jsBacon.jswu.js库。

这种不同的模式使我们能够做更多的事情:异步迭代,异步事件处理,惰性评估,甚至自动并行化。

注意

自动并行化?在计算机科学行业中有一个词:不可能。但它真的不可能吗?摩尔定律的下一个进化飞跃可能是一个为我们的代码并行化的编译器,而函数组合可能就是这样的编译器?

不,事情并不完全是这样的。JavaScript 引擎才是真正进行并行化的,不是自动的,而是通过深思熟虑的代码。Compose 只是给引擎一个机会将其拆分成并行进程。但这本身就很酷。

单子

单子是帮助您组合函数的工具。

像原始类型一样,单子是可以用作函子“触及”的容器的结构。函子抓取数据,对其进行处理,将其放入一个新的单子中,并返回它。

我们将专注于三个单子:

  • Maybes

  • 承诺

  • Lenses

所以除了数组(map)和函数(compose)之外,我们还有五个函子(map、compose、maybe、promise 和 lens)。这些只是许多其他函子和单子中的一些。

Maybes

Maybes 允许我们优雅地处理可能为空的数据,并设置默认值。也就是说,maybe 是一个变量,它要么有一些值,要么没有。而对调用者来说这并不重要。

单独看起来,这似乎并不是什么大不了的事。每个人都知道,使用if-else语句很容易实现空检查:

if (getUsername() == null ) {
  username = 'Anonymous') {
else {
  username = getUsername();
}

但是通过函数式编程,我们正在摆脱逐行进行程序的程序化方式,而是使用函数和数据的管道。如果我们必须在中间打断链条来检查值是否存在,我们将不得不创建临时变量并编写更多的代码。Maybes 只是帮助我们保持逻辑在管道中流动的工具。

为了实现 maybes,我们首先需要创建一些构造函数。

// the Maybe monad constructor, empty for now
var Maybe = function(){}; 

// the None instance, a wrapper for an object with no value
var None = function(){}; 
None.prototype = Object.create(Maybe.prototype);
None.prototype.toString = function(){return 'None';};

// now we can write the `none` function
// saves us from having to write `new None()` all the time
var none = function(){return new None()};

// and the Just instance, a wrapper for an object with a value
var Just = function(x){return this.x = x;};
Just.prototype = Object.create(Maybe.prototype);
Just.prototype.toString = function(){return "Just "+this.x;};
var just = function(x) {return new Just(x)};

最后,我们可以编写maybe函数。它返回一个新的函数,要么返回空,要么返回一个 maybe。它是一个函子

var maybe = function(m){
  if (m instanceof None) {
    return m;
  }
  else if (m instanceof Just) {
    return just(m.x);   
  }
  else {
    throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); 
  }
}

我们也可以创建一个与数组类似的函子生成器。

var maybeOf = function(f){
  return function(m) {
    if (m instanceof None) {
      return m;
    }
    else if (m instanceof Just) {
      return just(f(m.x));
    }
    else {
      throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); 
    }
  }
}

所以Maybe是一个单子,maybe是一个函子,maybeOf返回一个已经分配给一个态射的函子。

在我们继续之前,我们还需要一件事。我们需要为Maybe单子对象添加一个帮助我们更直观地使用它的方法。

Maybe.prototype.orElse = function(y) {
  if (this instanceof Just) {
    return this.x;
  }
  else {
    return y;
  }
}

在其原始形式中,maybes 可以直接使用。

maybe(just(123)).x; // Returns 123
maybeOf(plusplus)(just(123)).x; // Returns 124
maybe(plusplus)(none()).orElse('none'); // returns 'none'

任何返回一个然后执行的方法都足够复杂,以至于容易出问题。所以我们可以通过调用我们的curry()函数来使它更加简洁。

maybePlusPlus = maybeOf.curry()(plusplus);
maybePlusPlus(just(123)).x; // returns 123
maybePlusPlus(none()).orElse('none'); // returns none

但是当直接调用none()just()函数的肮脏业务被抽象化时,maybes 的真正力量将变得清晰。我们将通过一个使用 maybes 的示例对象User来做到这一点。

var User = function(){
  this.username = none(); // initially set to `none`
};
User.prototype.setUsername = function(name) {
  this.username = just(str(name)); // it's now a `just
};
User.prototype.getUsernameMaybe = function() {
  var usernameMaybe = maybeOf.curry()(str);
  return usernameMaybe(this.username).orElse('anonymous');
};

var user = new User();
user.getUsernameMaybe(); // Returns 'anonymous'

user.setUsername('Laura');
user.getUsernameMaybe(); // Returns 'Laura'

现在我们有了一个强大而安全的方法来定义默认值。记住这个User对象,因为我们将在本章后面使用它。

承诺

承诺的本质是它们对变化的情况保持免疫。

- 弗兰克·安德伍德,《纸牌屋》

在函数式编程中,我们经常使用管道和数据流:一系列函数,其中每个函数产生一个数据类型,由下一个函数消耗。然而,许多这些函数是异步的:readFile、事件、AJAX 等。我们如何修改这些函数的返回类型来指示结果,而不是使用延续传递风格和深度嵌套的回调?通过将它们包装在promises中。

Promises 就像回调的函数式等价物。显然,回调并不都是函数式的,因为如果有多个函数对同一数据进行变异,就会出现竞争条件和错误。Promises 解决了这个问题。

你应该使用 promises 来完成这个:

fs.readFile("file.json", function(err, val) {
  if( err ) {
    console.error("unable to read file");
  }
  else {
    try {
      val = JSON.parse(val);
      console.log(val.success);
    }
    catch( e ) {
      console.error("invalid json in file");
    }
  }
});

进入以下代码片段:

fs.readFileAsync("file.json").then(JSON.parse)
  .then(function(val) {
    console.log(val.success);
  })
  .catch(SyntaxError, function(e) {
    console.error("invalid json in file");
  })
  .catch(function(e){
    console.error("unable to read file")
  });

上述代码来自bluebird的 README:一个功能齐全的Promises/A+实现,性能异常出色。Promises/A+是 JavaScript 中实现 promises 的规范。鉴于它在 JavaScript 社区内的当前辩论,我们将把实现留给Promises/A+团队,因为它比可能更复杂得多。

但这是一个部分实现:

// the Promise monad
var Promise = require('bluebird');

// the promise functor
var promise = function(fn, receiver) {
  return function() {
    var slice = Array.prototype.slice,
    args = slice.call(arguments, 0, fn.length - 1),
    promise = new Promise();
    args.push(function() {
      var results = slice.call(arguments),
      error = results.shift();
      if (error) promise.reject(error);
      else promise.resolve.apply(promise, results);
    });
    fn.apply(receiver, args);
    return promise;
  };
};

现在我们可以使用promise()函子将接受回调的函数转换为返回 promises 的函数。

var files = ['a.json', 'b.json', 'c.json'];
readFileAsync = promise(fs.readFile);
var data = files
  .map(function(f){
    readFileAsync(f).then(JSON.parse)
  })
  .reduce(function(a,b){
    return $.extend({}, a, b)
  });

镜头

程序员真正喜欢单子的另一个原因是,它们使编写库变得非常容易。为了探索这一点,让我们扩展我们的User对象,增加更多用于获取和设置值的函数,但是,我们将使用lenses而不是使用 getter 和 setter。

镜头是一流的获取器和设置器。它们不仅允许我们获取和设置变量,还允许我们在其上运行函数。但是,它们不是对数据进行变异,而是克隆并返回由函数修改的新数据。它们强制数据是不可变的,这对于安全性和一致性以及库来说非常好。无论应用程序如何,它们都非常适合优雅的代码,只要引入额外的数组副本不会对性能造成重大影响。

在编写lens()函数之前,让我们看看它是如何工作的。

var first = lens(
  function (a) { return arr(a)[0]; }, // get
  function (a, b) { return [b].concat(arr(a).slice(1)); } // set
);
first([1, 2, 3]); // outputs 1
first.set([1, 2, 3], 5); // outputs [5, 2, 3]
function tenTimes(x) { return x * 10 }
first.modify(tenTimes, [1,2,3]); // outputs [10,2,3]

这就是lens()函数的工作原理。它返回一个具有 get、set 和 mod 定义的函数。lens()函数本身是一个函子。

var lens = fuction(get, set) {
  var f = function (a) {return get(a)};
  f.get = function (a) {return get(a)}; 
  f.set = set;
  f.mod = function (f, a) {return set(a, f(get(a)))};
  return f;
};

让我们来试一个例子。我们将扩展我们之前例子中的User对象。

// userName :: User -> str
var userName = lens(
  function (u) {return u.getUsernameMaybe()}, // get
  function (u, v) { // set
    u.setUsername(v);  
    return u.getUsernameMaybe(); 
  }
);

var bob = new User();
bob.setUsername('Bob');
userName.get(bob); // returns 'Bob'
userName.set(bob, 'Bobby'); //return 'Bobby'
userName.get(bob); // returns 'Bobby'
userName.mod(strToUpper, bob); // returns 'BOBBY'
strToUpper.compose(userName.set)(bob, 'robert'); // returns 'ROBERT'
userName.get(bob); // returns 'robert'

jQuery 是一个单子

如果你认为所有这些关于范畴、函子和单子的抽象胡言没有真正的现实应用,那就再想想吧。流行的 JavaScript 库 jQuery 提供了一个增强的接口,用于处理 HTML,实际上是一个单子库。

jQuery对象是一个单子,它的方法是函子。实际上,它们是一种特殊类型的函子,称为endofunctorsEndofunctors是返回与输入相同类别的函子,即F :: X -> X。每个jQuery方法都接受一个jQuery对象并返回一个jQuery对象,这允许方法被链接,并且它们将具有类型签名jFunc :: jquery-obj -> jquery-obj

$('li').add('p.me-too').css('color', 'red').attr({id:'foo'});

这也是 jQuery 的插件框架的强大之处。如果插件以jQuery对象作为输入并返回一个作为输出,则可以将其插入到链中。

让我们看看 jQuery 是如何实现这一点的。

单子是函子“触及”以获取数据的容器。通过这种方式,数据可以受到库的保护和控制。jQuery 通过其许多方法提供对底层数据的访问,这些数据是一组包装的 HTML 元素。

jQuery对象本身是作为匿名函数调用的结果编写的。

var jQuery = (function () {
  var j = function (selector, context) {
    var jq-obj = new j.fn.init(selector, context);
    return jq-obj;
  };

  j.fn = j.prototype = {
    init: function (selector, context) {
      if (!selector) {
        return this;
      }
    }
  };
  j.fn.init.prototype = j.fn;
  return j;
})();

在这个高度简化的 jQuery 版本中,它返回一个定义了j对象的函数,实际上只是一个增强的init构造函数。

var $ = jQuery(); // the function is returned and assigned to `$`
var x = $('#select-me'); // jQuery object is returned

与函子将值提取出容器的方式相同,jQuery 包装了 HTML 元素并提供对它们的访问,而不是直接修改 HTML 元素。

jQuery 并不经常宣传,但它有自己的map()方法,用于将 HTML 元素对象从包装器中提取出来。就像fmap()方法一样,元素被提取出来,对它们进行处理,然后放回容器中。这就是 jQuery 的许多命令在后端工作的方式。

$('li').map(function(index, element) {
  // do something to the element
  return element
});

另一个用于处理 HTML 元素的库 Prototype 不是这样工作的。Prototype 通过助手直接改变 HTML 元素。因此,它在 JavaScript 社区中的表现并不好。

实施类别

是时候我们正式将范畴论定义为 JavaScript 对象了。范畴是对象(类型)和态射(仅在这些类型上工作的函数)。这是一种非常高级的、完全声明式的编程方式,但它确保代码非常安全可靠——非常适合担心并发和类型安全的 API 和库。

首先,我们需要一个帮助我们创建同态的函数。我们将其称为homoMorph(),因为它们将是同态。它将返回一个函数,该函数期望传入一个函数,并根据输入生成其组合。输入是态射接受的输入和输出的类型。就像我们的类型签名一样,即// morph :: num -> num -> [num],只有最后一个是输出。

var homoMorph = function( /* input1, input2,..., inputN, output */ ) {
  var before = checkTypes(arrayOf(func)(Array.prototype.slice.call(arguments, 0, arguments.length-1)));
  var after = func(arguments[arguments.length-1])
  return function(middle) {
    return function(args) {
      return after(middle.apply(this, before([].slice.apply(arguments))));   
    }
  }
}

// now we don't need to add type signature comments
// because now they're built right into the function declaration
add = homoMorph(num, num, num)(function(a,b){return a+b})
add(12,24); // returns 36
add('a', 'b'); // throws error
homoMorph(num, num, num)(function(a,b){
  return a+b;
})(18, 24); // returns 42

homoMorph()函数相当复杂。它使用闭包(参见第二章,“函数式编程基础”)返回一个接受函数并检查其输入和输出值的类型安全性的函数。为此,它依赖于一个辅助函数:checkTypes,其定义如下:

var checkTypes = function( typeSafeties ) {
  arrayOf(func)(arr(typeSafeties));
  var argLength = typeSafeties.length;
  return function(args) {
    arr(args);
    if (args.length != argLength) {
      throw new TypeError('Expected '+ argLength + ' arguments');
    }
    var results = [];
    for (var i=0; i<argLength; i++) {
      results[i] = typeSafetiesi;   
    }
    return results;
  }
}

现在让我们正式定义一些同态。

var lensHM = homoMorph(func, func, func)(lens);
var userNameHM = lensHM(
  function (u) {return u.getUsernameMaybe()}, // get
  function (u, v) { // set
    u.setUsername(v);
    return u.getUsernameMaybe(); 
  }
)
var strToUpperCase = homoMorph(str, str)(function(s) {
  return s.toUpperCase();
});
var morphFirstLetter = homoMorph(func, str, str)(function(f, s) {
  return f(s[0]).concat(s.slice(1));
});
var capFirstLetter = homoMorph(str, str)(function(s) {
  return morphFirstLetter(strToUpperCase, s)
});

最后,我们可以把它带回家。以下示例包括函数组合、镜头、同态和其他内容。

// homomorphic lenses
var bill = new User();
userNameHM.set(bill, 'William'); // Returns: 'William'
userNameHM.get(bill); // Returns: 'William'

// compose
var capatolizedUsername = fcompose(capFirstLetter,userNameHM.get);
capatolizedUsername(bill, 'bill'); // Returns: 'Bill'

// it's a good idea to use homoMorph on .set and .get too
var getUserName = homoMorph(obj, str)(userNameHM.get);
var setUserName = homoMorph(obj, str, str)(userNameHM.set);
getUserName(bill); // Returns: 'Bill'
setUserName(bill, 'Billy'); // Returns: 'Billy'

// now we can rewrite capatolizeUsername with the new setter
capatolizedUsername = fcompose(capFirstLetter, setUserName);
capatolizedUsername(bill, 'will'); // Returns: 'Will'
getUserName(bill); // Returns: 'will'

前面的代码非常声明式,安全,可靠和可信赖。

代码声明式是什么意思?在命令式编程中,我们编写一系列指令,告诉机器如何做我们想要的事情。在函数式编程中,我们描述值之间的关系,告诉机器我们想要它计算什么,机器会找出指令序列来实现它。函数式编程是声明式的。

整个库和 API 可以通过这种方式构建,允许程序员自由编写代码,而不必担心并发和类型安全,因为这些问题在后端处理。

总结

大约每 2000 人中就有一人患有一种称为共感觉的病症,这是一种神经现象,其中一种感官输入渗入另一种感官。最常见的形式涉及将颜色与字母相匹配。然而,还有一种更罕见的形式,即将句子和段落与味道和感觉联系起来。

对于这些人来说,他们不是逐字逐句地阅读。他们看整个页面/文档/程序,感受它的“味道”——不是口中的味道,而是“心灵”中的味道。然后他们像拼图一样把文本的部分放在一起。

这就是编写完全声明式代码的样子:描述值之间的关系,告诉机器我们想要它计算什么。程序的部分不是按照逐行指令。共感者可能自然而然地做到这一点,但只要稍加练习,任何人都可以学会如何将关系拼图一起放在一起。

在本章中,我们看了几个数学概念,这些概念适用于函数式编程,以及它们如何允许我们在数据之间建立关系。接下来,我们将探讨递归和 JavaScript 中的其他高级主题。

第六章:JavaScript 中的高级主题和陷阱

JavaScript 被称为 Web 的"汇编语言"。这个类比(它并不完美,但哪个类比是完美的?)源自于 JavaScipt 经常是编译的目标,主要来自ClojureCoffeeScript,但也来自许多其他来源,比如pyjamas(python 到 JS)和 Google Web Kit(Java 到 JS)。

但这个类比也提到了一个愚蠢的想法,即 JavaScript 和 x86 汇编一样具有表现力和低级。也许这个想法源于 JavaScript 自从 1995 年首次与网景一起发布以来就一直因其设计缺陷和疏忽而受到抨击。它是在匆忙开发和发布的,还没有完全开发就发布了。正因为如此,一些有问题的设计选择进入了 JavaScript,这种语言很快成为了 Web 的事实脚本语言。分号是一个大错误。定义函数的模糊方法也是错误的。是var foo = function();还是function foo();

函数式编程是规避一些这些错误的绝佳方式。通过专注于 JavaScript 实际上是一种函数式语言这一事实,可以清楚地看到,在前面关于不同声明函数的方式的示例中,最好将函数声明为变量。分号大多只是为了使 JavaScript 看起来更像 C 而已。

但是,始终记住你正在使用的语言。JavaScript,像任何其他语言一样,都有其缺陷。而且,在编写通常会绕过可能的边缘的风格时,这些小失误可能会变成不可恢复的陷阱。其中一些陷阱包括:

  • 递归

  • 变量作用域和闭包

  • 函数声明与函数表达式

然而,这些问题可以通过一点注意来克服。

递归

在任何语言中,递归对于函数式编程非常重要。许多函数式语言甚至要求通过不提供forwhile循环语句来进行迭代,这只有在语言保证尾调用消除时才可能,而 JavaScript 并非如此。在第二章函数式编程基础中简要介绍了递归。但在本节中,我们将深入探讨递归在 JavaScript 中的工作原理。

尾递归

JavaScript 处理递归的例程被称为尾递归,这是一种基于堆栈的递归实现。这意味着,对于每次递归调用,堆栈中都会有一个新的帧。

为了说明这种方法可能出现的问题,让我们使用经典的递归算法来计算阶乘。

var factorial = function(n) {
  if (n == 0) {
    // base case
    return 1;
  }
  else {
    // recursive case
    return n * factorial(n-1);
  }
}

该算法将自己调用n次以获得答案。它实际上计算了(1 x 1 x 2 x 3 x … x N)。这意味着时间复杂度是O(n)

注意

O(n),读作"大 O 到 n",意味着算法的复杂度将随着输入规模的增长而增长,这是更精简的增长。O(n2)是指数增长,O(log(n))是对数增长,等等。这种表示法既可以用于时间复杂度,也可以用于空间复杂度。

但是,由于每次迭代都会为内存堆栈分配一个新的帧,因此空间复杂度也是O(n)。这是一个问题。这意味着内存将以这样的速度被消耗,以至于很容易超出内存限制。在我的笔记本电脑上,factorial(23456)返回Uncaught Error: RangeError: Maximum call stack size exceeded

虽然计算 23456 的阶乘是一种不必要的努力,但可以肯定的是,许多使用递归解决的问题将很容易增长到这样的规模。考虑数据树的情况。树可以是任何东西:搜索应用程序、文件系统、路由表等。下面是树遍历函数的一个非常简单的实现:

var traverse = function(node) {
  node.doSomething(); // whatever work needs to be done
  node.childern.forEach(traverse); // many recursive calls
}

每个节点只有两个子节点时,时间复杂度和空间复杂度(在最坏的情况下,整个树必须被遍历以找到答案)都将是O(n2),因为每次都会有两个递归调用。如果每个节点有许多子节点,复杂度将是O(nm),其中m是子节点的数量。递归是树遍历的首选算法;while循环会更加复杂,并且需要维护一个堆栈。

指数增长意味着不需要一个非常大的树就能抛出RangeError异常。必须有更好的方法。

尾调用消除

我们需要一种方法来消除每次递归调用都分配新的堆栈帧。这就是所谓的尾调用消除

通过尾调用消除,当一个函数返回调用自身的结果时,语言实际上不执行另一个函数调用。它为您将整个过程转换为循环。

好的,我们该怎么做呢?使用惰性求值。如果我们可以将其重写为对惰性序列进行折叠,使得函数返回一个值或者返回调用另一个函数的结果而不对该结果进行任何操作,那么就不需要分配新的堆栈帧。

为了将其转换为“尾递归形式”,阶乘函数必须被重写,使得内部过程fact在控制流中最后调用自身,如下面的代码片段所示:

var factorial = function(n) {
  var _fact = function(x, n) {
    if (n == 0) {
      // base case
      return x;
    }
    else {
      // recursive case
      return _fact(n*x, n-1);
    }
  }
  return fact(1, n);
}

注意

与其让递归尾部产生结果(比如n * factorial(n-1)),不如让结果在递归尾部进行计算(通过调用_fact(r*n, n-1)),并由该尾部中的最后一个函数产生结果(通过return r;)。计算只朝一个方向进行,而不是向上。对解释器来说,将其处理为迭代相对容易。

然而,尾调用消除在 JavaScript 中不起作用。将上述代码放入您喜欢的 JavaScript 引擎中,factorial(24567)仍然会返回Uncaught Error: RangeError: Maximum call stack size exceeded异常。尾调用消除被列为要包含在下一个 ECMAScript 版本中的新功能,但在所有浏览器实现它之前还需要一些时间。

JavaScript 无法优化转换为尾递归形式的函数。这是语言规范和运行时解释器的特性,简单明了。这与解释器如何获取堆栈帧的资源有关。有些语言在不需要记住任何新信息时会重用相同的堆栈帧,就像在前面的函数中一样。这就是尾调用消除如何减少时间和空间复杂度。

不幸的是,JavaScript 不会这样做。但如果它这样做了,它将重新组织堆栈帧,从这样:

call factorial (3)
  call fact (3 1)
    call fact (2 3)
      call fact (1 6)
        call fact (0 6)
        return 6
      return 6
    return 6
  return 6
return 6

转换为以下形式:

call factorial (3)
  call fact (3 1)
  call fact (2 3)
  call fact (1 6)
  call fact (0 6)
  return 6
return 6

trampolining

解决方案?一种称为trampolining的过程。这是一种通过使用thunks来“黑客”尾调用消除概念的方法。

注意

为此,thunks 是带有参数的表达式,用于包装没有自己参数的匿名函数。例如:function(str){return function(){console.log(str)}}。这可以防止表达式在接收函数调用匿名函数之前被评估。

trampoline 是一个接受函数作为输入并重复执行其返回值直到返回的不再是函数的函数。以下是一个简单的实现代码片段:

var trampoline = function(f) {
  while (f && f instanceof Function) {
    f = f.apply(f.context, f.args);
  }
  return f;
}

要实际实现尾调用消除,我们需要使用 thunks。为此,我们可以使用bind()函数,它允许我们将一个方法应用于具有分配给另一个对象的this关键字的对象。在内部,它与call关键字相同,但它链接到方法并返回一个新的绑定函数。bind()函数实际上进行了部分应用,尽管方式非常有限。

var factorial = function(n) {
  var _fact = function(x, n) {
    if (n == 0) {
      // base case
      return x;
    }
    else {
      // recursive case
      return _fact.bind(null, n*x, n-1);
    }
  }
  return trampoline(_fact.bind(null, 1, n));
}

但是编写 fact.bind(null, ...) 方法很麻烦,会让任何阅读代码的人感到困惑。相反,让我们编写自己的函数来创建 thunks。thunk() 函数必须做一些事情:

  • thunk() 函数必须模拟 _fact.bind(null, n*x, n-1) 方法,返回一个未评估的函数

  • thunk() 函数应该包含另外两个函数:

  • 用于处理给定函数,以及

  • 用于处理函数参数,这些参数将在调用给定函数时使用

有了这些,我们就可以开始编写函数了。我们只需要几行代码就可以写出来。

var thunk = function (fn) {
  return function() {
    var args = Array.prototype.slice.apply(arguments);
    return function() { return fn.apply(this, args); };
  };
};

现在我们可以在阶乘算法中使用 thunk() 函数,就像这样:

var factorial = function(n) {
  var fact = function(x, n) {
    if (n == 0) {
      return x;
    }
    else {
      return thunk(fact)(n * x, n - 1);
    }
  }
  return trampoline(thunk(fact)(1, n));
}

但是,我们可以通过将 _fact() 函数定义为 thunk() 函数来进一步简化。通过将内部函数定义为 thunk() 函数,我们无需在内部函数定义中和返回语句中都使用 thunk() 函数。

var factorial = function(n) {
  var _fact = thunk(function(x, n) {
    if (n == 0) {
      // base case
      return x;
    }
    else {
      // recursive case
      return _fact(n * x, n - 1);
    }
  });
  return trampoline(_fact(1, n));
}

结果是美丽的。看起来像 _fact() 函数被递归调用以实现无尾递归,实际上几乎透明地被处理为迭代!

最后,让我们看看 trampoline()thunk() 函数如何与我们更有意义的树遍历示例一起工作。以下是使用 trampolining 和 thunks 遍历数据树的一个简单示例:

var treeTraverse = function(trunk) {
  var _traverse = thunk(function(node) {
    node.doSomething();
    node.children.forEach(_traverse);
  }
  trampoline(_traverse(trunk));
}

我们已经解决了尾递归的问题。但是有没有更好的方法?如果我们能够简单地将递归函数转换为非递归函数呢?接下来,我们将看看如何做到这一点。

Y 组合子

Y 组合子是计算机科学中令人惊叹的事物之一,即使是最熟练的编程大师也会感到惊讶。它自动将递归函数转换为非递归函数的能力是为什么 Douglas Crockford 称其为 "计算机科学中最奇怪和奇妙的产物",而 Sussman 和 Steele 曾经说过,"这个方法能够工作真是了不起"。

因此,一个真正令人惊叹的、奇妙的计算机科学产物,能够让递归函数屈服,一定是庞大而复杂的,对吗?不完全是这样。它在 JavaScript 中的实现只有九行非常奇怪的代码。它们如下:

var Y = function(F) {
  return (function (f) {
    return f(f);
  } (function (f) {
    return F(function (x) {
      return f(f)(x);
    });
  }));
}

它的工作原理是:找到作为参数传入的函数的 "不动点"。不动点提供了另一种思考函数的方式,而不是在计算机编程理论中的递归和迭代。它只使用匿名函数表达式、函数应用和变量引用来实现。请注意,Y 并没有引用自身。事实上,所有这些函数都是匿名的。

正如你可能已经猜到的,Y 组合子源自 λ 演算。它实际上是借助另一个称为 U 组合子的组合子推导出来的。组合子是特殊的高阶函数,它们只使用函数应用和早期定义的组合子来从输入中定义结果。

为了演示 Y 组合子,我们将再次转向阶乘问题,但我们需要以稍微不同的方式定义阶乘函数。我们不再写一个递归函数,而是写一个返回数学定义阶乘的函数。然后我们可以将这个函数传递给 Y 组合子。

var FactorialGen = function(factorial) {
  return (function(n) {
    if (n == 0) {
      // base case
      return 1;
    }
    else {
      // recursive case
      return n * factorial(n – 1);
    }
  });
};
Factorial = Y(FactorialGen);
Factorial(10); // 3628800

然而,当我们给它一个非常大的数字时,堆栈会溢出,就像使用尾递归而没有 trampolining 一样。

Factorial(23456); // RangeError: Maximum call stack size exceeded

但是我们可以像下面这样在 Y 组合子中使用 trampolining:

var FactorialGen2 = function (factorial) {
  return function(n) {
    var factorial = thunk(function (x, n) {
      if (n == 0) {
        return x;
      }
      else {
        return factorial(n * x, n - 1);
      }
    });
    return trampoline(factorial(1, n));
  }
};

var Factorial2 = Y(FactorialGen2)
Factorial2(10); // 3628800
Factorial2(23456); // Infinity

我们还可以重新排列 Y 组合子以执行称为 memoization 的操作。

Memoization

Memoization 是一种存储昂贵函数调用结果的技术。当以相同的参数再次调用函数时,将返回存储的结果,而不是重新计算结果。

尽管 Y 组合子比递归快得多,但它仍然相对较慢。为了加快速度,我们可以创建一个记忆化的不动点组合子:一个类似 Y 的组合子,它缓存中间函数调用的结果。

var Ymem = function(F, cache) {
  if (!cache) {
    cache = {} ; // Create a new cache.
  }
  return function(arg) {
    if (cache[arg]) {
      // Answer in cache
      return cache[arg] ; 
    }
    // else compute the answer
    var answer = (F(function(n){
      return (Ymem(F,cache))(n);
    }))(arg); // Compute the answer.
    cache[arg] = answer; // Cache the answer.
    return answer;
  };
}

那么它有多快呢?通过使用jsperf.com/,我们可以比较性能。

以下结果是使用 1 到 100 之间的随机数。我们可以看到,记忆化的 Y 组合子要快得多。而且加上 trampolining 并不会使它变慢太多。您可以在此 URL 查看结果并运行测试:jsperf.com/memoizing-y-combinator-vs-tail-call-optimization/7

记忆化

最重要的是:在 JavaScript 中执行递归的最有效和最安全的方法是使用记忆化的 Y 组合子,通过 trampolining 和 thunks 进行尾调用消除。

变量作用域

JavaScript 中变量的作用域并不是自然的。事实上,有时它甚至是违反直觉的。他们说 JavaScript 程序员可以通过他们对作用域的理解程度来判断。

作用域解析

首先,让我们来看一下 JavaScript 中不同的作用域解析。

JavaScript 使用作用域链来确定变量的作用域。在解析变量时,它从最内部的作用域开始,向外搜索。

全局作用域

在这个级别定义的变量、函数和对象对整个程序中的任何代码都是可用的。这是最外层的作用域。

var x = 'hi';
function a() {
  console.log(x);
}
a(); // 'hi'

局部作用域

每个描述的函数都有自己的局部作用域。在另一个函数内定义的任何函数都有一个与外部函数相关联的嵌套局部作用域。几乎总是源代码中的位置定义了作用域。

var x = 'hi';
function a() {
  console.log(x);
}
function b() {
  var x = 'hello';
  console.log(x);
}
b(); // hello
a(); // hi

局部作用域仅适用于函数,而不适用于任何表达式语句(ifforwhile等),这与大多数语言处理作用域的方式不同。

function c() {
  var y = 'greetings';
  if (true) {
    var y = 'guten tag';
  }
  console.log(y);
}

function d() {
  var y = 'greetings';
  function e() {
    var y = 'guten tag';
  }
  console.log(y)
}
c(); // 'guten tag'
d(); // 'greetings'

在函数式编程中,这不是太大的问题,因为函数更常用,表达式语句不太常用。例如:

function e(){
  var z = 'namaste';
  [1,2,3].foreach(function(n) {
    var z = 'aloha';
  }
  isTrue(function(){
    var z = 'good morning';
  });
  console.log(z);
}
e(); // 'namaste'

对象属性

对象属性也有它们自己的作用域链。

var x = 'hi';
var obj = function(){
  this.x = 'hola';
};
var foo = new obj();
console.log(foo.x); // 'hola'
foo.x = 'bonjour';
console.log(foo.x); // 'bonjour'

对象的原型在作用域链中更靠下。

obj.prototype.x = 'greetings';
obj.prototype.y = 'konnichi ha';
var bar = new obj();
console.log(bar.x); // still prints 'hola'
console.log(bar.y); // 'konnichi ha'

这甚至不能算是全面的,但这三种作用域类型足以让我们开始。

闭包

这种作用域结构的一个问题是它不留下私有变量的空间。考虑以下代码片段:

var name = 'Ford Focus';
var year = '2006';
var millage = 123456;
function getMillage(){
  return millage;
}
function updateMillage(n) {
  millage = n;
}

这些变量和函数是全局的,这意味着程序后面的代码很容易意外地覆盖它们。一个解决方法是将它们封装到一个函数中,并在定义后立即调用该函数。

var car = function(){
  var name = 'Ford Focus';
  var year = '2006';
  var millage = 123456;
  function getMillage(){
    return Millage;
  }
  function updateMillage(n) {
    millage = n;
  }
}();

在函数外部没有发生任何事情,所以我们应该通过使其匿名来丢弃函数名。

(function(){
  var name = 'Ford Focus';
  var year = '2006';
  var millage = 123456;
  function getMillage(){
    return millage;
  }
  function updateMillage(n) {
    millage = n;
  }
})();

为了使函数getValue()updateMillage()在匿名函数外部可用,我们需要在对象字面量中返回它们,如下面的代码片段所示:

var car = function(){
  var name = 'Ford Focus';
  var year = '2006';
  var millage = 123456;
  return {
    getMillage: function(){
      return millage;
    },
    updateMillage: function(n) {
      millage = n;
    }
  }
}();
console.log( car.getMillage() ); // works
console.log( car.updateMillage(n) ); // also works
console.log( car.millage ); // undefined

这给我们伪私有变量,但问题并不止于此。下一节将探讨 JavaScript 中变量作用域的更多问题。

陷阱

在 JavaScript 中可以找到许多变量作用域的微妙之处。以下绝不是一个全面的列表,但它涵盖了最常见的情况:

  • 以下将输出 4,而不是人们所期望的'undefined':
for (var n = 4; false; ) { } console.log(n);

这是因为在 JavaScript 中,变量的定义发生在相应作用域的开头,而不仅仅是在声明时。

  • 如果你在外部作用域中定义一个变量,然后在函数内部用相同的名称定义一个变量,即使那个if分支没有被执行,它也会被重新定义。例如:
var x = 1;
function foo() {
  if (false) {
    var x = 2;
  }
  return x;
}
foo(); // Return value: 'undefined', expected return value:
2

同样,这是由于将变量定义移动到作用域的开头,使用undefined值引起的。

  • 在浏览器中,全局变量实际上是存储在window对象中的。
window.a = 19;
console.log(a); // Output: 19

全局作用域中的a表示当前上下文的属性,因此a===this.a,在浏览器中的window对象充当全局作用域中this关键字的等价物。

前两个示例是 JavaScript 的一个特性导致的,这个特性被称为提升,在下一节关于编写函数的内容中将是一个关键概念。

函数声明与函数表达式与函数构造函数

这三种声明之间有什么区别?

function foo(n){ return n; }
var foo = function(n){ return n; };
var foo = new Function('n', 'return n');

乍一看,它们只是编写相同函数的不同方式。但这里还有更多的事情。如果我们要充分利用 JavaScript 中的函数以便将它们操纵成函数式编程风格,那么我们最好能够搞清楚这一点。如果在计算机编程中有更好的方法,那么那一种方法应该是唯一的方法。

函数声明

函数声明,有时称为函数语句,使用function关键字定义函数。

function foo(n) {
  return n;
}

使用这种语法声明的函数会被提升到当前作用域的顶部。这实际上意味着,即使函数在几行下面定义,JavaScript 也知道它并且可以在作用域中较早地使用它。例如,以下内容将正确地将 6 打印到控制台:

foo(2,3);
function foo(n, m) {
  console.log(n*m);
}

函数表达式

命名函数也可以通过定义匿名函数并将其赋值给变量来定义为表达式。

var bar = function(n, m) {
  console.log(n*m);
};

它们不像函数声明那样被提升。这是因为,虽然函数声明被提升,但变量声明却没有。例如,这将无法工作并抛出错误:

bar(2,3);
var bar = function(n, m) {
  console.log(n*m);
};

在函数式编程中,我们希望使用函数表达式,这样我们可以将函数视为变量,使它们可以用作回调和高阶函数的参数,例如map()函数。将函数定义为表达式使得它们更像是分配给函数的变量。此外,如果我们要以一种风格编写函数,那么为了一致性和清晰度,我们应该以该风格编写所有函数。

函数构造函数

JavaScript 实际上有第三种创建函数的方式:使用Function()构造函数。与函数表达式一样,使用Function()构造函数定义的函数也不会被提升。

var func = new Function('n','m','return n+m');
func(2,3); // returns 5

Function()构造函数不仅令人困惑,而且非常危险。无法进行语法纠正,也无法进行优化。以以下方式编写相同函数要容易得多、更安全、更清晰:

var func = function(n,m){return n+m};
func(2,3); // returns 5

不可预测的行为

所以区别在于函数声明会被提升,而函数表达式不会。这可能会导致意想不到的事情发生。考虑以下情况:

function foo() {
  return 'hi';
}
console.log(foo());
function foo() {
  return 'hello';
}

实际打印到控制台的是hello。这是因为foo()函数的第二个定义被提升到顶部,成为 JavaScript 解释器实际使用的定义。

虽然乍一看这可能不是一个关键的区别,在函数式编程中这可能会引起混乱。考虑以下代码片段:

if (true) {
  function foo(){console.log('one')};
}
else {
  function foo(){console.log('two')};
}
foo();

当调用foo()函数时,控制台会打印two,而不是one

最后,有一种方法可以结合函数表达式和声明。它的工作方式如下:

var foo = function bar(){ console.log('hi'); };
foo(); // 'hi'
bar(); // Error: bar is not defined

使用这种方法几乎没有意义,因为在声明中使用的名称(在前面的示例中的bar()函数)在函数外部不可用,会引起混乱。只有在递归的情况下才适用,例如:

var foo = function factorial(n) {
  if (n == 0) {
    return 1;
  }
else {
    return n * factorial(n-1);
  }
};
foo(5);


总结

JavaScript 被称为“Web 的汇编语言”,因为它像 x86 汇编语言一样无处不在且不可避免。它是唯一在所有浏览器上运行的语言。它也有缺陷,但将其称为低级语言却不准确。

相反,把 JavaScript 看作是网络的生咖啡豆。当然,有些豆子是受损的,有些是腐烂的。但是如果选择好豆子,由熟练的咖啡师烘焙和冲泡,这些豆子就可以变成一杯绝妙的摩卡咖啡,一次就无法忘怀。它的消费变成了日常习惯,没有它的生活会变得单调,更难以进行,也不那么令人兴奋。一些人甚至喜欢用插件和附加组件来增强这种咖啡,比如奶油、糖和可可,这些都很好地补充了它。

JavaScript 最大的批评者之一道格拉斯·克劳福德曾说过:“肯定有很多人拒绝考虑 JavaScript 可能做对了什么。我曾经也是那些人之一。但现在我对其中的才华仍然感到惊讶。”

JavaScript 最终变得非常棒。

第七章:JavaScript 中的函数式和面向对象编程

你经常会听到 JavaScript 是一种空白语言,其中空白可以是面向对象的、函数式的或通用的。本书将 JavaScript 作为一种函数式语言进行了重点研究,并且已经付出了很大的努力来证明它是这样的。但事实上,JavaScript 是一种通用语言,意味着它完全能够支持多种编程风格。与 Python 和 F#不同,JavaScript 是多范式的。但与这些语言不同,JavaScript 的 OOP 方面是基于原型的,而大多数其他通用语言是基于类的。

在本章中,我们将把函数式和面向对象编程与 JavaScript 联系起来,看看这两种范式如何相辅相成,共存。本章将涵盖以下主题:

  • JavaScript 如何既是函数式的又是面向对象的?

  • JavaScript 的 OOP - 使用原型

  • 如何在 JavaScript 中混合函数式和面向对象编程

  • 函数继承

  • 函数式混入

更好的代码是目标。函数式和面向对象编程只是实现这一目标的手段。

JavaScript - 多范式语言

如果面向对象编程意味着将所有变量视为对象,而函数式编程意味着将所有函数视为变量,那么函数不能被视为对象吗?在 JavaScript 中,它们可以。

但说函数式编程意味着将函数视为变量有些不准确。更好的说法是:函数式编程意味着将一切都视为值,尤其是函数。

描述函数式编程的更好方式可能是将其称为声明式。与命令式编程风格无关,声明式编程表达了解决问题所需的计算逻辑。计算机被告知问题是什么,而不是如何解决它的过程。

与此同时,面向对象编程源自命令式编程风格:计算机会得到解决问题的逐步说明。OOP 要求计算的说明(方法)和它们操作的数据(成员变量)被组织成称为对象的单元。访问数据的唯一方式是通过对象的方法。

那么这两种风格如何集成在一起呢?

  • 对象方法中的代码通常以命令式风格编写。但如果以函数式风格呢?毕竟,OOP 并不排斥不可变数据和高阶函数。

  • 也许更纯粹的混合方式是同时将对象视为函数和传统的基于类的对象。

  • 也许我们可以简单地在面向对象的应用程序中包含一些函数式编程的思想,比如承诺和递归。

  • OOP 涵盖了封装、多态和抽象等主题。函数式编程也涵盖了这些主题,只是它采用了不同的方式。也许我们可以在面向函数的应用程序中包含一些面向对象编程的思想。

重点是:OOP 和 FP 可以混合在一起,有几种方法可以做到这一点。它们并不互斥。

JavaScript 的面向对象实现 - 使用原型

JavaScript 是一种无类语言。这并不意味着它比其他计算机语言更时尚或更蓝领;无类意味着它没有类结构,就像面向对象的语言那样。相反,它使用原型进行继承。

尽管这可能让有 C++和 Java 背景的程序员感到困惑,基于原型的继承比传统继承更具表现力。以下是 C++和 JavaScript 之间差异的简要比较:

C++ JavaScript
强类型 弱类型
静态 动态
基于类 基于原型
函数
构造函数 函数
方法 函数

继承

在我们进一步讨论之前,让我们确保我们充分理解面向对象编程中的继承概念。基于类的继承在以下伪代码中得到了展示:

class Polygon {
  int numSides;
  function init(n) {
    numSides = n;
  }
}
class Rectangle inherits Polygon {
  int width;
  int length;
  function init(w, l) {
    numSides = 4;
    width = w;
    length = l;
  }
  function getArea() {
    return w * l;
  }
}
class Square inherits Rectangle {
  function init(s) {
    numSides = 4;
    width = s;
    length = s;
  }
}

Polygon类是其他类继承的父类。它只定义了一个成员变量,即边数,该变量在init()函数中设置。Rectangle子类继承自Polygon类,并添加了两个成员变量lengthwidth,以及一个方法getArea()。它不需要定义numSides变量,因为它已经被继承的类定义了,并且它还覆盖了init()函数。Square类通过从Rectangle类继承其getArea()方法进一步延续了这种继承链。通过简单地再次覆盖init()函数,使长度和宽度相同,getArea()函数可以保持不变,从而需要编写的代码更少。

在传统的面向对象编程语言中,这就是继承的全部含义。如果我们想要向所有对象添加一个颜色属性,我们只需将其添加到Polygon对象中,而无需修改任何继承自它的对象。

JavaScript 的原型链

JavaScript 中的继承归结为原型。每个对象都有一个名为其原型的内部属性,它是指向另一个对象的链接。该对象本身也有自己的原型。这种模式可以重复,直到达到一个具有undefined作为其原型的对象。这就是原型链,这就是 JavaScript 中继承的工作原理。以下图解释了 JavaScript 中的继承:

JavaScript 的原型链

在搜索对象的函数定义时,JavaScript 会“遍历”原型链,直到找到具有正确名称的函数的第一个定义。因此,覆盖它就像在子类的原型上提供一个新定义一样简单。

JavaScript 中的继承和Object.create()方法

就像有许多方法可以在 JavaScript 中创建对象一样,也有许多方法可以复制基于类的经典继承。但做这件事的首选方法是使用Object.create()方法。

var Polygon = function(n) {
  this.numSides = n;
}

var Rectangle = function(w, l) {
  this.width = w;
  this.length = l;
}

// the Rectangle's prototype is redefined with Object.create
Rectangle.prototype = Object.create(Polygon.prototype);

// it's important to now restore the constructor attribute
// otherwise it stays linked to the Polygon
Rectangle.prototype.constructor = Rectangle;

// now we can continue to define the Rectangle class
Rectangle.prototype.numSides = 4;
Rectangle.prototype.getArea = function() {
  return this.width * this.length;
}

var Square = function(w) {
  this.width = w;
  this.length = w;
}
Square.prototype = Object.create(Rectangle.prototype);
Square.prototype.constructor = Square;

var s = new Square(5);
console.log( s.getArea() ); // 25

这种语法对许多人来说可能看起来不寻常,但经过一点练习,它将变得熟悉。必须使用prototype关键字来访问所有对象都具有的内部属性[[Prototype]]Object.create()方法声明一个新对象,该对象继承自指定的对象原型。通过这种方式,可以在 JavaScript 中实现经典继承。

注意

Object.create()方法在 2011 年的 ECMAScript 5.1 中引入,并被宣传为创建对象的新方法。这只是 JavaScript 整合继承的众多尝试之一。幸运的是,这种方法运行得相当好。

在构建第五章范畴论中的Maybe类时,我们看到了这种继承结构,MaybeNoneJust类,它们彼此之间也是继承关系。

var Maybe = function(){}; 

var None = function(){}; 
None.prototype = Object.create(Maybe.prototype);
None.prototype.constructor = None;
None.prototype.toString = function(){return 'None';};

var Just = function(x){this.x = x;};
Just.prototype = Object.create(Maybe.prototype);
Just.prototype.constructor = Just;
Just.prototype.toString = function(){return "Just "+this.x;};

这表明 JavaScript 中的类继承可以成为函数式编程的一种实现方式。

一个常见的错误是将构造函数传递给Object.create()而不是prototype对象。这个问题的复杂性在于,直到子类尝试使用继承的成员函数时才会抛出错误。

Foo.prototype = Object.create(Parent.prototype); // correct
Bar.prototype = Object.create(Parent); // incorrect
Bar.inheritedMethod(); // Error: function is undefined

如果inheritedMethod()方法已经附加到Foo.prototype类,则无法找到该函数。如果inheritedMethod()方法直接附加到实例上,即在Bar构造函数中使用this.inheritedMethod = function(){...},那么Object.create()中使用Parent作为参数可能是正确的。

在 JavaScript 中混合函数式和面向对象编程

面向对象编程已经是主导的编程范式数十年了。它在世界各地的计算机科学 101 课程中被教授,而函数式编程则没有。软件架构师用它来设计应用程序,而函数式编程则没有。这也是有道理的:面向对象编程使得抽象思想更容易理解。它使编写代码更容易。

所以,除非你能说服你的老板应用程序需要全部是函数式的,否则我们将在面向对象的世界中使用函数式编程。本节将探讨如何做到这一点。

函数式继承

将函数式编程应用于 JavaScript 应用程序的最直接方式可能是在面向对象编程原则内使用大部分函数式风格,比如继承。

为了探索这可能如何工作,让我们构建一个简单的应用程序来计算产品的价格。首先,我们需要一些产品类:

var Shirt = function(size) {
  this.size = size;
};

var TShirt = function(size) {
  this.size = size;
};
TShirt.prototype = Object.create(Shirt.prototype);
TShirt.prototype.constructor = TShirt;
TShirt.prototype.getPrice = function(){
  if (this.size == 'small') {
    return 5;
  }
  else {
    return 10;
  }
}

var ExpensiveShirt = function(size) {
  this.size = size;
}
ExpensiveShirt.prototype = Object.create(Shirt.prototype);
ExpensiveShirt.prototype.constructor = ExpensiveShirt;
ExpensiveShirt.prototype.getPrice = function() {
  if (this.size == 'small') {
    return 20;
  }
  else {
    return 30;
  }
}

然后我们可以在Store类中组织它们如下:

var Store = function(products) {
  this.products = products;
}
Store.prototype.calculateTotal = function(){
  return this.products.reduce(function(sum,product) {
    return sum + product.getPrice();
  }, 10) * TAX; // start with $10 markup, times global TAX var
};

var TAX = 1.08;
var p1 = new TShirt('small');
var p2 = new ExpensiveShirt('large');
var s = new Store([p1,p2]);
console.log(s.calculateTotal()); // Output: 35

calculateTotal()方法使用数组的reduce()函数来干净地将产品的价格相加。

这样做完全没问题,但如果我们需要一种动态计算标记值的方法呢?为此,我们可以转向一个称为策略模式的概念。

策略模式

策略模式是一种定义一组可互换算法的方法。它被面向对象编程程序员用于在运行时操纵行为,但它基于一些函数式编程原则。

  • 逻辑和数据的分离

  • 函数的组合

  • 函数作为一等对象

还有一些面向对象编程的原则:

  • 封装

  • 继承

在我们之前解释的用于计算产品成本的示例应用中,假设我们想要给予某些客户优惠待遇,并且需要调整标记来反映这一点。

所以让我们创建一些客户类:

var Customer = function(){};
Customer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 10) * TAX;
};

var RepeatCustomer = function(){};
RepeatCustomer.prototype = Object.create(Customer.prototype);
RepeatCustomer.prototype.constructor = RepeatCustomer;
RepeatCustomer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 5) * TAX;
};

var TaxExemptCustomer = function(){};
TaxExemptCustomer.prototype = Object.create(Customer.prototype);
TaxExemptCustomer.prototype.constructor = TaxExemptCustomer;
TaxExemptCustomer.prototype.calculateTotal = function(products) {
  return products.reduce(function(total, product) {
    return total + product.getPrice();
  }, 10);
};

每个Customer类封装了算法。现在我们只需要Store类调用Customer类的calculateTotal()方法。

var Store = function(products) {
  this.products = products;
  this.customer = new Customer();
  // bonus exercise: use Maybes from Chapter 5 instead of a default customer instance
}
Store.prototype.setCustomer = function(customer) {
  this.customer = customer;
}
Store.prototype.getTotal = function(){
  return this.customer.calculateTotal(this.products);
};

var p1 = new TShirt('small');
var p2 = new ExpensiveShirt('large');
var s = new Store([p1,p2]);
var c = new TaxExemptCustomer();
s.setCustomer(c);
s.getTotal(); // Output: 45

Customer类进行计算,Product类保存数据(价格),Store类维护上下文。这实现了非常高的内聚性和面向对象编程与函数式编程的很好的混合。JavaScript 的高表现力使这成为可能,而且相当容易。

Mixins

简而言之,mixins 是允许其他类使用它们的方法的类。这些方法仅供其他类使用,而mixin类本身永远不会被实例化。这有助于避免继承的模糊性。它们是将函数式编程与面向对象编程混合的绝佳手段。

每种语言中的 mixin 实现方式都不同。由于 JavaScript 的灵活性和表现力,mixins 被实现为只有方法的对象。虽然它们可以被定义为函数对象(即var mixin = function(){...};),但最好将它们定义为对象字面量(即var mixin = {...};)以保持代码的结构纪律。这将帮助我们区分类和 mixins。毕竟,mixins 应该被视为过程,而不是对象。

让我们从声明一些 mixins 开始。我们将扩展我们之前部分的Store应用程序,使用 mixins 来扩展类。

var small = {
  getPrice: function() {
    return this.basePrice + 6;   
  },
  getDimensions: function() {
    return [44,63]
  }
}
var large = {
  getPrice: function() {
    return this.basePrice + 10;   
  },
  getDimensions: function() {
    return [64,83]
  }
};

我们不仅仅局限于这些。还可以添加许多其他的 mixins,比如颜色或面料材质。我们需要稍微修改我们的Shirt类,如下面的代码片段所示:

var Shirt = function() {
  this.basePrice = 1;
};
Shirt.getPrice = function(){
  return this.basePrice;
}
var TShirt = function() {
  this.basePrice = 5;
};
TShirt.prototype = Object.create(Shirt.prototype);
TShirt..prototype.constructor = TShirt;

现在我们准备使用 mixins 了。

经典 mixin

你可能想知道这些 mixin 是如何与类混合在一起的。这样做的经典方式是将 mixin 的函数复制到接收对象中。可以通过以下方式扩展Shirt原型来实现:

Shirt.prototype.addMixin = function (mixin) {
  for (var prop in mixin) {
    if (mixin.hasOwnProperty(prop)) {
      this.prototype[prop] = mixin[prop];
    }
  }
};

现在可以添加 mixins 如下:

TShirt.addMixin(small);
var p1 = new TShirt();
console.log( p1.getPrice() ); // Output: 11

TShirt.addMixin(large);
var p2 = new TShirt();
console.log( p2.getPrice() ); // Output: 15

然而,存在一个主要问题。当再次计算p1的价格时,结果是15,即大件物品的价格。它应该是小件物品的价格!

console.log( p1.getPrice() ); // Output: 15

问题在于Shirt对象的prototype.getPrice()方法每次添加混入时都会被重写;这根本不是我们想要的函数式编程。

函数式混入

还有另一种使用混入的方法,更符合函数式编程。

我们不是将混入的方法复制到目标对象,而是需要创建一个新对象,该对象是目标对象的克隆,并添加了混入的方法。首先必须克隆对象,这可以通过创建一个继承自它的新对象来实现。我们将这个变体称为plusMixin

Shirt.prototype.plusMixin = function(mixin) {    
  // create a new object that inherits from the old
  var newObj = this;
  newObj.prototype = Object.create(this.prototype);
  for (var prop in mixin) {
    if (mixin.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixin[prop];
    }
  }
  return newObj;
};

var SmallTShirt = Tshirt.plusMixin(small); // creates a new class
var smallT = new SmallTShirt();
console.log( smallT.getPrice() );  // Output: 11

var LargeTShirt = Tshirt.plusMixin(large);
var largeT = new LargeTShirt();
console.log( largeT.getPrice() ); // Output: 15
console.log( smallT.getPrice() ); // Output: 11 (not effected by 2nd mixin call)

现在就来玩乐趣部分!现在我们可以真正地使用混入进行函数式编程。我们可以创建产品和混入的每种可能的组合。

// in the real world there would be way more products and mixins!
var productClasses = [ExpensiveShirt, Tshirt]; 
var mixins = [small, medium, large];

// mix them all together 
products = productClasses.reduce(function(previous, current) {
  var newProduct = mixins.map(function(mxn) {
    var mixedClass = current.plusMixin(mxn);
    var temp = new mixedClass();
    return temp;
  });
  return previous.concat(newProduct);
},[]);
products.forEach(function(o){console.log(o.getPrice())});

为了使其更加面向对象,我们可以重写Store对象以具有此功能。我们还将在Store对象而不是产品中添加一个显示函数,以保持接口逻辑和数据的分离。

// the store
var Store = function() {
  productClasses = [ExpensiveShirt, TShirt];
  productMixins = [small, medium, large];
  this.products = productClasses.reduce(function(previous, current) {
    var newObjs = productMixins.map(function(mxn) {
      var mixedClass = current.plusMixin(mxn);
      var temp = new mixedClass();
      return temp;
    });
    return previous.concat(newObjs);
  },[]);
}
Store.prototype.displayProducts = function(){
  this.products.forEach(function(p) {
    $('ul#products').append('<li>'+p.getTitle()+': $'+p.getPrice()+'</li>');
  });
}

我们所要做的就是创建一个Store对象并调用它的displayProducts()方法来生成产品和价格的列表!

<ul id="products">
  <li>small premium shirt: $16</li>
  <li>medium premium shirt: $18</li>
  <li>large premium shirt: $20</li>
  <li>small t-shirt: $11</li>
  <li>medium t-shirt: $13</li>
  <li>large t-shirt: $15</li>
</ul>

这些行需要添加到product类和混入中,以使前面的输出正常工作:

Shirt.prototype.title = 'shirt';
TShirt.prototype.title = 't-shirt';
ExpensiveShirt.prototype.title = 'premium shirt';

// then the mixins got the extra 'getTitle' function:
var small = {
  ...
  getTitle: function() {
    return 'small ' + this.title; // small or medium or large
  }
}

就这样,我们拥有了一个高度模块化和可扩展的电子商务应用。新的衬衫款式可以非常容易地添加——只需定义一个新的Shirt子类,并将其添加到Store类的数组product类中。混入的添加方式也是一样的。所以现在当我们的老板说:“嘿,我们有一种新类型的衬衫和外套,每种都有标准颜色,我们需要在你今天下班前将它们添加到网站上”,我们可以放心地说我们不会加班了!

总结

JavaScript 具有很高的表现力。这使得将函数式编程和面向对象编程混合使用成为可能。现代 JavaScript 不仅仅是面向对象编程或函数式编程,它是两者的混合体。诸如策略模式和混入之类的概念非常适合 JavaScript 的原型结构,并且它们有助于证明当今 JavaScript 最佳实践中函数式编程和面向对象编程的使用量是相等的。

如果你从这本书中只学到了一件事,我希望是如何将函数式编程技术应用到现实应用中。本章向你展示了如何做到这一点。

附录 A. JavaScript 中函数式编程的常见函数

这个附录涵盖了 JavaScript 中函数式编程的常见函数:

  • 数组函数:
var flatten = function(arrays) {
  return arrays.reduce( function(p,n){
    return p.concat(n);
  });
};

var invert = function(arr) {
  return arr.map(function(x, i, a) {
    return a[a.length - (i+1)];
  });
};
  • 绑定函数:
var bind = Function.prototype.call.bind(Function.prototype.bind);
var call = bind(Function.prototype.call, Function.prototype.call);
var apply = bind(Function.prototype.call, Function.prototype.apply);
  • 范畴论:
var checkTypes = function( typeSafeties ) {
  arrayOf(func)(arr(typeSafeties));
  var argLength = typeSafeties.length;
  return function(args) {
    arr(args);
    if (args.length != argLength) {
      throw new TypeError('Expected '+ argLength + ' arguments');
    }
    var results = [];
    for (var i=0; i<argLength; i++) {
      results[i] = typeSafetiesi;
    }
    return results;
  };
};

var homoMorph = function( /* arg1, arg2, ..., argN, output */ ) {
  var before = checkTypes(arrayOf(func)(Array.prototype.slice.call(arguments, 0, arguments.length-1)));
  var after = func(arguments[arguments.length-1])
  return function(middle) {
    return function(args) {
      return after(middle.apply(this, before([].slice.apply(arguments))));
    };
  };
};
  • 组合:
Function.prototype.compose = function(prevFunc) {
  var nextFunc = this;
  return function() {
    return nextFunc.call(this,prevFunc.apply(this,arguments));
  };
};

Function.prototype.sequence  = function(prevFunc) {
  var nextFunc = this;
  return function() {
    return prevFunc.call(this,nextFunc.apply(this,arguments));
  };
};
  • 柯里化:
Function.prototype.curry = function (numArgs) {
  var func = this;
  numArgs = numArgs || func.length;
  // recursively acquire the arguments
  function subCurry(prev) {
    return function (arg) {
      var args = prev.concat(arg);
      if (args.length < numArgs) {
        // recursive case: we still need more args
        return subCurry(args);
      }
      else {
        // base case: apply the function
        return func.apply(this, args);
      }
    };
  };
  return subCurry([]);
};
  • 函子:
// map :: (a -> b) -> [a] -> [b]
var map = function(f, a) {
  return arr(a).map(func(f));
}

// strmap :: (str -> str) -> str -> str
var strmap = function(f, s) {
  return str(s).split('').map(func(f)).join('');
}

// fcompose :: (a -> b)* -> (a -> b)
var fcompose = function() {
  var funcs = arrayOf(func)(arguments);
  return function() {
    var argsOfFuncs = arguments;
    for (var i = funcs.length; i > 0; i -= 1) {
      argsOfFuncs  = [funcs[i].apply(this, args)];
    }
    return args[0];
  };
};
  • 镜头:
var lens = function(get, set) {
  var f = function (a) {return get(a)};
  f.get = function (a) {return get(a)}; 
  f.set = set;
  f.mod = function (f, a) {return set(a, f(get(a)))};
  return f;
};

// usage:
var first = lens(
  function (a) { return arr(a)[0]; }, // get
  function (a, b) { return [b].concat(arr(a).slice(1)); } // set
);
  • Maybes:
var Maybe = function(){}; 
Maybe.prototype.orElse = function(y) {
  if (this instanceof Just) {
    return this.x;
  }
  else {
    return y;
  }
};

var None = function(){}; 
None.prototype = Object.create(Maybe.prototype);
None.prototype.toString = function(){return 'None';};
var none = function(){return new None()};
// and the Just instance, a wrapper for an object with a value
var Just = function(x){return this.x = x;};
Just.prototype = Object.create(Maybe.prototype);
Just.prototype.toString = function(){return "Just "+this.x;};
var just = function(x) {return new Just(x)};
var maybe = function(m){
  if (m instanceof None) {
    return m;
  }
  else if (m instanceof Just) {
    return just(m.x);
  }
  else {
    throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); 
  }
};

var maybeOf = function(f){
  return function(m) {
    if (m instanceof None) {
      return m;
    }
    else if (m instanceof Just) {
      return just(f(m.x));
    }
    else {
      throw new TypeError("Error: Just or None expected, " + m.toString() + " given."); 
    }
  };
};
  • 混入:
Object.prototype.plusMixin = function(mixin) {
  var newObj = this;
  newObj.prototype = Object.create(this.prototype);
  newObj.prototype.constructor = newObj;
  for (var prop in mixin) {
    if (mixin.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixin[prop];
    }
  }
  return newObj;
};
  • 部分应用:
function bindFirstArg(func, a) {
  return function(b) {
    return func(a, b);
  };
};

Function.prototype.partialApply = function(){
  var func = this; 
  args = Array.prototype.slice.call(arguments);
  return function(){
    return func.apply(this, args.concat(
      Array.prototype.slice.call(arguments)
    ));
  };
};

Function.prototype.partialApplyRight = function(){
  var func = this; 
  args = Array.prototype.slice.call(arguments);
  return function(){
    return func.apply(
      this,
      Array.protype.slice.call(arguments, 0)
    .concat(args));
  };
};
  • Trampolining:
var trampoline = function(f) {
  while (f && f instanceof Function) {
    f = f.apply(f.context, f.args);
  }
  return f;
};

var thunk = function (fn) {
  return function() {
    var args = Array.prototype.slice.apply(arguments);
    return function() { return fn.apply(this, args); };
  };
};
  • 类型安全:
var typeOf = function(type) {
  return function(x) {
    if (typeof x === type) {
      return x;
    }
    else {
      throw new TypeError("Error: "+type+" expected, "+typeof x+" given.");
    }
  };
};

var str = typeOf('string'),
  num = typeOf('number'),
  func = typeOf('function'),
  bool = typeOf('boolean');

var objectTypeOf = function(name) {
  return function(o) {
    if (Object.prototype.toString.call(o) === "[object "+name+"]") {
      return o;
    }
    else {
      throw new TypeError("Error: '+name+' expected, something else given."); 
    }
  };
};
var obj = objectTypeOf('Object');
var arr = objectTypeOf('Array');
var date = objectTypeOf('Date');
var div = objectTypeOf('HTMLDivElement');

// arrayOf :: (a -> b) -> ([a] -> [b])
var arrayOf = function(f) {
  return function(a) {
    return map(func(f), arr(a));
  }
};
  • Y 组合子:
var Y = function(F) {
  return (function (f) {
    return f(f);
  }(function (f) {
    return F(function (x) {
      return f(f)(x);
    });
  }));
};

// Memoizing Y-Combinator:
var Ymem = function(F, cache) {
  if (!cache) {
    cache = {} ; // Create a new cache.
  }
  return function(arg) {
    if (cache[arg]) {
      // Answer in cache
      return cache[arg] ;
    }
    // else compute the answer
    var answer = (F(function(n){
      return (Ymem(F,cache))(n);
    }))(arg); // Compute the answer.
    cache[arg] = answer; // Cache the answer.
    return answer;
  };
};

附录 B. 术语表

这个附录涵盖了本书中使用的一些重要术语:

  • 匿名函数:没有名称且未绑定到任何变量的函数。也称为 Lambda 表达式。

  • 回调:可以传递给另一个函数以在以后的事件中使用的函数。

  • 范畴:在范畴论中,范畴是相同类型的对象集合。在 JavaScript 中,范畴可以是包含明确定义为数字、字符串、布尔值、日期、对象等的对象的数组或对象。

  • 范畴论:一种将数学结构组织成对象集合和对这些对象的操作的概念。本书中使用的计算机程序中的数据类型和函数形成了这些范畴。

  • 闭包:一种环境,使得其中定义的函数可以访问外部不可用的局部变量。

  • 耦合:每个程序模块依赖于其他模块的程度。函数式编程减少了程序内部的耦合程度。

  • Currying:将具有多个参数的函数转换为一个参数的函数的过程,返回另一个可以根据需要接受更多参数的函数。形式上,具有N个参数的函数可以转换为N个函数的函数链,每个函数只有一个参数。

  • Declarative programming:一种表达解决问题所需的计算逻辑的编程风格。计算机被告知问题是什么,而不是解决问题所需的过程。

  • Endofunctor:将一个范畴映射到自身的函子。

  • Function composition:将许多函数组合成一个函数的过程。每个函数的结果作为下一个参数传递,最后一个函数的结果是整个组合的结果。

  • Functional language:促进函数式编程的计算机语言。

  • Functional programming:一种声明式编程范式,侧重于将函数视为数学表达式,并避免可变数据和状态变化。

  • Functional reactive programming:一种侧重于响应式元素和随时间变化的变量的函数式编程风格。

  • Functor:范畴之间的映射。

  • Higher-order function:以一个或多个函数作为输入,并返回一个函数作为输出的函数。

  • Inheritance:一种面向对象编程的能力,允许一个类从另一个类继承成员变量和方法。

  • Lambda expressions:参见匿名函数。

  • Lazy evaluation:一种计算机语言的评估策略,延迟对表达式的评估,直到需要其值。这种策略的相反称为急切评估或贪婪评估。惰性评估也被称为按需调用。

  • Library:一组具有明确定义接口的对象和函数,允许第三方程序调用它们的行为。

  • Memoization:存储昂贵函数调用的结果的技术。当以相同参数再次调用函数时,返回存储的结果,而不是重新计算结果。

  • Method chain:一种模式,其中许多方法并排调用,直接将一个方法的输出传递给下一个方法的输入。这避免了将中间值分配给临时变量的需要。

  • Mixin:一个对象,可以让其他对象使用它的方法。这些方法只能被其他对象使用,而 mixin 对象本身永远不会被实例化。

  • Modularity:程序可以被分解为独立的代码模块的程度。函数式编程增加了程序的模块化。

  • Monad:提供函子所需的封装的结构。

  • Morphism:仅在特定范畴上工作并在给定特定输入集时始终返回相同输出的纯函数。同态操作受限于单一范畴,而多态操作可以在多个范畴上操作。

  • Partial application:将一个或多个参数的值绑定到函数的过程。它返回一个部分应用的函数,该函数反过来接受剩余的未绑定参数。

  • Polyfill:用于用新函数增强原型的函数。它允许我们将新函数作为先前函数的方法来调用。

  • Pure function:其输出值仅取决于作为函数输入的参数的函数。因此,用相同值的参数x两次调用函数f,每次都会产生相同的结果f(x)

  • 递归函数:调用自身的函数。这样的函数依赖于解决同一问题的较小实例来计算较大问题的解决方案。与迭代类似,递归是重复调用相同代码块的另一种方式。但是,与迭代不同,递归要求代码块定义重复调用应该终止的情况,即基本情况。

  • 可重用性:通常是指代码块(通常是 JavaScript 中的函数)可以在同一程序的其他部分或其他程序中被重复使用的程度。

  • 自执行函数:在定义后立即被调用的匿名函数。在 JavaScript 中,通过在函数表达式后放置一对括号来实现这一点。

  • 策略模式:用于定义一组可互换算法的方法。

  • 尾递归:基于堆栈的递归实现。对于每个递归调用,堆栈中都有一个新的帧。

  • 工具包:一个小型软件库,提供一组函数供程序员使用。与库相比,工具包更简单,需要与调用它的程序耦合更少。

  • 蹦床编程:一种递归策略,可以在不提供尾调用消除功能的编程语言中实现,比如 JavaScript。

  • Y 组合子:Lambda 演算中的固定点组合子,消除了显式递归。当它作为返回递归函数的输入时,Y 组合子返回该函数的不动点,即将递归函数转换为非递归函数的转换。

第八章:索引

A

  • 匿名函数/匿名函数

  • apply()函数/应用,调用和 this 关键字

  • arrayOf 函子/数组和函子

  • 数组

  • 关于/数组和函子

B

  • backbone.js

  • 关于/介绍

  • Bacon.js/Bacon.js

  • Bilby.js/Bilby.js

  • bind()函数/绑定参数

  • bluebird/Promises

C

  • C++

  • 与 JavaScript/JavaScript 的面向对象实现-使用原型

  • call()函数/应用,调用和 this 关键字

  • 类别

  • 关于/范畴论简介

  • 实现/实现类别

  • 范畴论

  • 关于/范畴论, 范畴论简介

  • 类型安全函数,创建/类型安全

  • 对象/对象标识

  • 经典混入

  • 关于/经典混入

  • Clojure

  • 关于/介绍

  • 闭包

  • 关于/自调用函数和闭包

  • 使用/自调用函数和闭包

  • 命令行界面(CLI)/CLI

  • 组合

  • 编程/使用 compose 进行编程

  • 柯里化

  • 关于/部分函数应用和柯里化, 柯里化

D

  • 开发环境

  • 关于/开发和生产环境

  • Dojo

  • 关于/介绍

E

  • 电子商务网站应用程序

  • 关于/应用程序-电子商务网站

  • 命令式方法/命令式方法

  • ease.js

  • 关于/JavaScript 是一种函数式编程语言吗?

  • ECMAScript

  • 关于/JavaScript 是一种函数式编程语言吗?

  • 终态函子

  • 关于/jQuery 是一个单子

  • 引擎

  • 关于/JavaScript 是一种函数式编程语言吗?

  • 环境,JavaScript 应用程序

  • 浏览器/浏览器

  • 服务器端 JavaScript/服务器端 JavaScript

  • 命令行界面(CLI)/CLI

F

  • 幻想乡/幻想乡

  • filter()函数

  • 参数/Array.prototype.filter()")

  • forEach()函数

  • 参数/Array.prototype.forEach

  • 函数继承

  • 关于/ 函数继承

  • 策略模式/ 函数继承, 策略模式

  • 函数式语言

  • 编译成 JavaScript/ 编译成 JavaScript 的函数式语言

  • 函数式库

  • 使用/ 使用函数式库与其他 JavaScript 模块

  • JavaScript 的函数式库

  • 关于/ JavaScript 的函数式库

  • Underscore.js/ Underscore.js

  • Fantasy Land/ Fantasy Land

  • Bilby.js/ Bilby.js

  • Lazy.js/ Lazy.js

  • Bacon.js/ Bacon.js

  • FFunctional/ 荣誉提及

  • wwu.js/ 荣誉提及

  • sloth.js/ 荣誉提及

  • stream.js/ 荣誉提及

  • Lo-Dash.js/ 荣誉提及

  • Ssugar/ 荣誉提及

  • from.js/ 荣誉提及

  • JSLINQ/ 荣誉提及

  • Boiler.js/ 荣誉提及

  • FFolktale/ 荣誉提及

  • jjQuery/ 荣誉提及

  • 函数混入

  • 关于/ 函数混入

  • 函数式编程

  • 关于/ 介绍, 函数式编程, 大部分函数式编程

  • 在非函数式编程中使用/ 非函数式世界中的函数式编程

  • 事件处理/ 处理事件

  • 和面向对象编程,混合/ 在 JavaScript 中混合函数式和面向对象编程

  • 使用面向对象编程的函数式编程

  • 函数继承/ 函数继承

  • 混入/ 混入

  • 函数式编程语言

  • JavaScript/ JavaScript 是函数式编程语言吗?

  • 函数式编程语言

  • 关于/ 函数式编程语言

  • 执行/ 什么使语言成为函数式?

  • 特点/ 什么使语言成为函数式?

  • 优势/ 优势, 模块化, 数学正确

  • 函数式响应式编程

  • 关于/ 函数式响应式编程

  • 反应性 / 反应性

  • 订阅者,修改 / 总结一切

  • 函数式响应式编程(FRP) / 函数式响应式编程

  • 函数组合

  • 关于 / 函数组合, 重新审视函数组合

  • compose() / 组合

  • 使用序列 / 序列 - 反向组合

  • 组合,与链 / 组合与链

  • 重写 / 重新审视函数组合

  • 函数构造函数

  • 关于 / 函数构造函数

  • 函数声明

  • 关于 / 函数声明

  • 与函数表达式相比 / 函数表达式, 不可预测的行为

  • 函数表达式

  • 关于 / 函数表达式

  • 与函数构造函数相比 / 函数构造函数

  • 函数工厂 / 函数工厂

  • 函数操作

  • 关于 / 函数操作

  • apply() 函数 / Apply, call, and the this keyword

  • this 关键字 / Apply, call, and the this keyword

  • call() 函数 / Apply, call, and the this keyword

  • bind() 函数 / 绑定参数

  • 函数工厂 / 函数工厂

  • 函数

  • 使用 / 使用函数

  • 自调用函数,使用 / 自调用函数和闭包

  • 闭包,使用 / 自调用函数和闭包

  • 高阶函数 / 高阶函数

  • 纯函数 / 纯函数

  • 匿名函数 / 匿名函数

  • 方法,链接 / 方法链

  • 递归函数 / 递归

  • 惰性评估 / 惰性评估

  • 函子

  • 关于 / 函子

  • 创建 / 创建函子

  • 函数组合 / 重新审视函数组合

G

  • 全局范围,变量

  • 关于 / 全局范围

H

  • Haskell

  • 关于 / 什么使一种语言是函数式的?

  • 高阶函数 / 高阶函数

  • 同态操作

  • 关于 / 范畴论简介

  • 恒等函数态射 / 数组和函子

  • 继承

  • 关于 / 继承

  • 使用 Object.create() 方法 / JavaScript 中的继承和 Object.create()方法方法")

J

  • JavaScript

  • 关于 / 介绍, JavaScript 是一种函数式编程语言吗?

  • 递归 / 递归

  • 变量作用域 / 变量作用域

  • 函数声明 / 函数声明与函数表达式与函数构造器的区别

  • 函数表达式 / 函数声明与函数表达式与函数构造器的区别

  • 函数构造器 / 函数声明与函数表达式与函数构造器的区别

  • 多范式语言 / JavaScript-多范式语言

  • 面向对象实现 / JavaScript 的面向对象实现-使用原型

  • 与 C++相比 / JavaScript 的面向对象实现-使用原型

  • jQuery

  • 关于 / 介绍

  • jQuery 对象

  • 关于 / jQuery 是一个单子

  • 实现 / jQuery 是一个单子

  • 朱莉娅

  • 关于 / 介绍

L

  • Lazy.js / Lazy.js

  • 惰性求值

  • 关于 / 惰性求值

  • 好处 / 惰性求值

  • lens() 函数

  • 写作 / 镜头

  • 镜头

  • 关于 / 镜头

  • LINQ(语言集成查询)

  • 关于 / 荣誉提及

  • Lisp

  • 关于 / 什么使一种语言是函数式的?

  • 局部作用域,变量

  • 关于 / 局部作用域

M

  • map() 函数

  • 参数 / Array.prototype.map()")

  • Maybes

  • 关于 / Maybes

  • 写作 / Maybes

  • 记忆化

  • 关于 / 记忆化

  • 引用链接 / 记忆化

  • 混合

  • 关于 / 混合

  • 经典混合 / 经典混合

  • 函数混合 / 函数混合

  • 单子

  • 关于 / 单子

  • Maybes / Maybes

  • 承诺 / 承诺

  • 镜头 / 镜头

  • jQuery 对象 / jQuery 是一个单子

  • 态射

  • 关于/ 范畴论简介, 类型安全

  • MVP(模型-视图-提供者)/ 使用其他 JavaScript 模块的函数库

O

  • 面向对象实现,JavaScript

  • 使用原型/ JavaScript 的面向对象实现-使用原型

  • 继承/ 继承

  • 原型链/ JavaScript 的原型链

  • 继承,使用 Object.create()方法/ JavaScript 中的继承和 Object.create()方法方法")

  • 面向对象编程

  • 和函数式编程,混合/ 在 JavaScript 中混合函数式和面向对象编程

  • Object.create()方法

  • 使用/ JavaScript 中的继承和 Object.create()方法方法")

  • 对象属性,变量

  • 关于/ 对象属性

  • 对象

  • 关于/ 类型安全

  • ƒ o g 符号

  • 关于/ 范畴论简介

P

  • 部分应用

  • 关于/ 部分函数应用和柯里化, 部分应用

  • 左侧参数,应用/ 从左侧进行部分应用

  • 正确的参数,应用/ 从右侧进行部分应用

  • 多元的

  • 关于/ 函数组合

  • 多态操作

  • 关于/ 范畴论简介

  • 生产环境

  • 关于/ 开发和生产环境

  • 承诺

  • 使用/ Promises

  • Promises/A+实现/ Promises

  • 原型链

  • 关于/ JavaScript 的原型链

  • 原型

  • 使用继承/ JavaScript 的面向对象实现-使用原型

  • 纯函数/ 纯函数

  • Pyjs/ 编译为 JavaScript 的函数式语言

  • Python

  • 关于/ 介绍

Q

  • QuickCheck

  • 关于/ Bilby.js

R

  • 递归

  • 关于/ 递归

  • Y-Combinator/ Y 组合子

  • 递归函数

  • 关于/ 递归

  • 分而治之/ 分而治之

  • reduce()函数

  • 参数/ Array.prototype.reduce()")

  • Roy/ 编译为 JavaScript 的函数式语言

  • Ruby

  • 关于/ 介绍

S

  • ScalaCheck

  • 关于 / Bilby.js

  • 方案

  • 关于 / 什么使一种语言是函数式的?

  • 作用域解析

  • 关于 / 作用域解析

  • 全局范围 / 全局范围

  • 本地范围 / 本地范围

  • 对象属性 / 对象属性

  • 自调用函数

  • 使用 / 自调用函数和闭包

  • 服务器端 JavaScript

  • 功能用例 / 服务器端环境中的功能用例

  • 策略模式

  • 关于 / 策略模式

T

  • 尾调用消除

  • 关于 / 尾调用消除

  • 跳板 / 跳板

  • 尾递归

  • 关于 / 尾递归

  • 尾调用消除 / 尾调用消除

  • 三元

  • 关于 / 函数组合

  • 此关键字 / 应用、调用和此关键字

  • 惰性计算

  • 关于 / 跳板

  • 工具包,功能程序员

  • 关于 / 函数式程序员的工具包

  • 回调,使用 / 回调

  • 数组原型映射() / 数组原型映射()")

  • 数组原型过滤() / 数组原型过滤()")

  • 数组原型减少() / 数组原型减少()")

  • 数组原型 forEach / 数组原型 forEach

  • 数组原型连接 / 数组原型连接

  • 数组原型反转 / 数组原型反转

  • 数组原型排序 / 数组原型排序

  • 数组原型每个 / 数组原型每个和数组原型一些

  • 数组原型一些 / 数组原型每个和数组原型一些

  • 跳板

  • 关于 / JavaScript 是一种函数式编程语言吗?

  • 跳板

  • 关于 / 跳板

  • TypeScript / 编译成 JavaScript 的功能语言

U

  • UHC / 编译成 JavaScript 的功能语言

  • 一元函数

  • 关于 / 函数组合

  • 下划线.js

  • 关于 / 介绍

  • Underscore.js / Underscore.js

V

  • 变量作用域

  • 关于 / 变量作用域

  • 作用域解析 / 作用域解析

  • 问题 / 闭包

  • 功能 / 陷阱

  • 可变参数

  • 关于/ 函数组合

Y

  • Y-组合子

  • 关于/ Y 组合子

  • 记忆化/ 记忆化

posted @ 2024-05-22 12:07  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报