二、函数式编程基础

到目前为止,您已经看到了函数式编程可以做些什么。但是函数式编程到底是什么呢?是什么让一种语言起作用而另一种语言不起作用?是什么让一种编程风格起作用而不是另一种?

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

  • 将函数和数组用于控制流
  • 编写纯函数、匿名函数、递归函数等等
  • 像传递对象一样传递函数
  • 利用map()filter()reduce()功能

函数式编程语言

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

是什么让语言具有功能性?

函数式编程不能在 c 语言中执行,函数式编程不能在 Java 中执行(没有很多“几乎”函数式编程的繁琐变通办法)。那些以及更多的语言根本不包含支持它的结构。它们纯粹是面向对象的,严格来说是非功能性语言。

在的同时,面向对象编程不能在纯函数语言上执行,比如 SchemeHaskellLisp、等等。

然而,某些语言支持这两种模式。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 演算中的根基,函数式程序可以被数学证明是正确的。对于需要证明程序的增长率、时间复杂度和数学正确性的研究人员来说,这是一个很大的优势。

我们来看看斐波那契数列。尽管除了概念证明之外,它很少用于其他任何事情,但它很好地说明了这个概念。评估斐波那契序列的标准方法是创建一个递归函数,用基本格到return 1 when n < 2来表示fibonnaci(n) = fibonnaci(n-2) + fibonnaci(n–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。这些将在第 3 章设置功能编程环境中介绍。

非功能世界中的功能编程

功能性和非功能性编程可以混合在一起吗?虽然这是第 7 章函数式&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 和 T2 ease . js 这样的库和框架一直在努力尝试抽象它,使它适合面向对象编程。JavaScript 在 20 世纪 90 年代成熟,当时 OOP 风靡一时,我们被告知 JavaScript 是面向对象的,因为我们非常希望它如此。但事实并非如此。

它的真实身份与其祖先更加一致:Scheme 和 Lisp,两种经典的函数式语言。一直以来,JavaScript 都是一种函数式语言。它的函数是一流的,可以嵌套,它有闭包和组合,它允许 currying 和 monads。所有这些都是函数式编程的关键。以下是 JavaScript 成为函数式语言的更多原因:

  • JavaScript 的词法语法包括将函数作为参数传递的能力,具有推断类型系统,并允许匿名函数、高阶函数、闭包等。这些事实对于实现函数式编程的结构和行为至关重要。
  • 它不是纯粹的面向对象语言,大多数面向对象的设计模式都是通过复制 Prototype 对象来实现的,Prototype 对象是面向对象编程的一个弱模型。欧洲计算机制造商协会脚本 ( ECMAScript ),JavaScript 的正式和标准化实施规范,在规范 4.2.1 中陈述如下:

    【ECMAScript】不包含 C++、Smalltalk 或 Java 语言(一种计算机语言,尤用于创建网站)中的适当类,而是支持创建对象的构造函数。在基于类的面向对象语言中,一般来说,状态由实例承载,方法由类承载,继承只是结构和行为的继承。在 ECMAScript 中,状态和方法由对象承载,结构、行为和状态都是继承的

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

话虽如此,JavaScript 确实不是一种纯粹的函数式语言。缺少的是懒惰的评估和内置的不可变数据。这是因为大多数口译员都是叫名字的,而不是按需要的。JavaScript 也不太擅长递归,因为它处理尾部调用的方式。然而,只要稍加注意,所有这些问题都可以得到缓解。无限序列和惰性评估所需的非严格评估可以通过名为Lazy.js的库来实现。不可变数据可以简单地通过编程技术来实现,但这需要更多的程序员纪律,而不是依赖语言来处理它。递归尾调用消除可以通过一个叫做的方法来实现。这些问题将在第 6 章高级主题&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]

这都是关于变量范围的。内部accumulate()函数可以使用values变量,甚至当范围外的代码调用函数时也可以使用。这被称为结束。

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()的代码要求func()x赋给返回值。但是x实际上等同于什么并不重要,直到它被需要。等到需要x的时候再打func()被称为懒评。

这种策略可以大大提高性能,尤其是当与函数式程序员最喜欢的程序流技术方法链和数组一起使用时。

懒惰评价的一个令人兴奋的好处是无穷级数的存在。因为在不能再延迟之前,实际上什么都不会计算,所以可以这样做:

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

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

然而,有一个问题:JavaScript 本身不执行 Lazy 评估。也就是说,有一些 JavaScript 库可以很好地模拟惰性评估。这是第 3 章设置功能编程环境的主题。

功能程序员工具包

如果您已经仔细查看了到目前为止呈现的几个示例,您会注意到一些您可能不熟悉的方法。它们是map()filter()reduce()函数,对任何语言的每个函数程序都至关重要。它们使您能够移除循环和语句,从而产生更干净的代码。

The 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)方法将尝试执行—“未定义”将被打印到控制台,并返回True。当ajax()调用完成时,它将使用“真”作为回调函数的名称,这将抛出一个错误。

这也意味着我们不能指定将什么参数传递给回调函数。如果我们需要不同于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]);

参数:

  • callback():这个函数为新数组生成一个元素,接收这些参数:
    • currentValue:这个参数给出了数组中正在处理的当前元素
    • index:这个参数给出了数组中当前元素的索引
    • array:这个参数给出了正在处理的数组
  • thisArg():该功能可选。执行callback时,该值用作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]);

参数:

  • callback():这个函数用来测试数组中的每个元素。返回True到保持元素,False否则。使用这些参数:
    • currentValue:该参数给出了阵列中正在处理的当前元素
    • index:此参数给出数组中当前元素的索引
  • array:这个参数给出了正在处理的数组。
  • thisArg():该功能可选。执行callback时,值用作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()

有时叫 fold,reduce()函数是用来把数组的所有值累加成一个。回调需要返回要执行的逻辑来组合对象。在数字的情况下,它们通常被加在一起得到一个和,或者相乘得到一个积。在字符串的情况下,字符串经常被附加在一起。

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

参数:

  • callback():此函数将两个对象合二为一,返回。有了这些参数:
    • previousValue:该参数给出了上次回调调用返回的值,或者initialValue,如果提供的话
    • currentValue:此参数给出了数组中正在处理的当前元素
    • index:此参数给出数组中当前元素的索引
    • array:此参数给出正在处理的数组
  • initialValue():该功能可选。对象用作第一个调用callback的的第一个参数。

示例:

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()迭代一个数组,并在每个项目上应用一个callback()函数。然而,它不返回任何东西。这是执行for循环的更干净的方式。

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

参数:

  • callback():该功能针对数组的每个值执行。使用这些参数:
    • currentValue:此参数给出了数组中正在处理的当前元素
    • index:此参数给出数组中当前元素的索引
    • array:此参数给出正在处理的数组
  • thisArg:该功能可选。执行callback时,值用作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]

数组.原型.反转

另一个原生 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 );

数组.原型.排序

很像我们的map()filter()reduce()方法,sort()方法采用了一个callback()函数来定义数组中的对象应该如何排序。但是,像reverse()函数一样,它会在适当的位置变异数组。这可不行。

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

我们可以编写一个不变异数组的纯sort()函数,但是排序算法是很多悲伤的来源。真正需要排序的大型数组应该组织在专门为此设计的数据结构中:快速冲突、合并排序、冒泡排序等等。

Array.prototype.every 和 Array.prototype.some

Array.prototype.every()Array.prototype.some()函数都是纯函数和高阶函数,它们是Array对象的方法,用于根据callback()函数测试数组的元素,该函数必须返回代表相应输入的布尔值。如果callback()函数为数组中的每个元素返回True,则every()函数返回True,如果数组中的某些元素为True,则some()函数返回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 库。