三、从函数开始——一个核心概念

在第二章,思维功能——第一个例子,我们走过去函数式编程的一个例子(FP)想,但现在让我们看看基础知识和审核功能。 在第 1 章Becoming Functional - Several Questions中,我们提到了 JavaScript 的两个重要特性:一级对象和闭包。

在本章中,我们将涉及几个重要的主题:

  • JavaScript 中的函数,包括如何定义它们,特别关注箭头函数
  • curry 和函数作为一级对象
  • 以 FP 方式使用函数的几种方法

在所有这些内容之后,您将了解与函数相关的通用和具体概念,毕竟,这是 FP 的核心!

所有关于功能

让我们先简要回顾一下 JavaScript 中的函数及其与 FP 概念的关系。 我们可以开始我们主要提到的函数作为一类对象第一章,成为功能——几个问题【5】,在几个地方在第二章,【显示】思维功能——第一个例子,关于函数作为一流的对象, 然后继续讨论在实际编码中使用它们的几个考虑因素。

特别地,我们将着眼于以下内容:

  • 关于 lambda 演算的一些基本和非常重要的概念,这是 FP 的理论基础
  • 箭头函数,这是 lambda 演算到 JavaScript 的最直接的转换
  • 使用函数作为一级对象,这是 FP 中的一个关键概念

和函数

在 lambda 微积分术语中,函数可以看起来像λ*x*.2**x*。 我们的理解是,λ字符之后的变量是函数的参数,而圆点之后的表达式是您将替换作为参数传递的任何值的地方。 在本章的后面,我们将看到这个特殊的例子可以用 JavaScript 写成x => 2*x,以箭头函数的形式,正如你所看到的,它的形式非常相似。

If you sometimes wonder about the difference between arguments and parameters, a mnemonic with some alliteration may help: Parameters are Potential, Arguments are Actual. Parameters are placeholders for potential values that will be passed, and arguments are the actual values passed to the function. In other words, when you define the function, you list its parameters, and when you call it, you provide arguments.

应用函数意味着向它提供一个实际的参数,该参数以通常的方式编写,使用括号。 例如,(λ*x*.2**x*)(3)将被计算为 6。 在 JavaScript 中,这些 lambda 函数等价于什么? 这是个有趣的问题! 有几种定义函数的方法,它们的含义并不相同。

A good article that shows the many ways of defining functions, methods, and more is The Many Faces of Functions in JavaScript by Leo Balter and Rick Waldron, at https://bocoup.com/blog/the-many-faces-of-functions-in-javascript—give it a look!

在 JavaScript 中有多少种方法可以定义函数? 答案是:的方式可能比你想象的要多! 至少,你可以这样写:**

*** 命名函数声明:function first(...) {...}; * 匿名函数表达式:var second = function(...) {...}; * 命名函数表达式:

  • 立即调用的表达式:var fourth = (function() { ...; return function(...) {...}; })();
  • 函数构造函数:var fifth = new Function(...);
  • 箭头函数:var sixth = (...) => {...};

如果需要,还可以添加对象方法声明,因为它们实际上也意味着函数,但前面的列表应该就足够了。

JavaScript also allows us to define generator functions (as in function*(...) {...}) that actually return a Generator object, and async functions that are really a mix of generators and promises. We won't be using these kinds of functions, but you can read more about them at https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/function* and https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function—they can be useful in other contexts.

这些定义函数的方法有什么不同,我们为什么要关心? 让我们一个一个来回顾一下:

  • 第一个定义是以function关键字开头的独立声明,可能是 JavaScript 中最常用的定义,它定义了一个名为first(即first.name=="first")的函数。 由于吊装,在定义的范围内,此功能可以在任何地方使用。

You can read more about hoisting at https://developer.mozilla.org/en-US/docs/Glossary/Hoisting. Keep in mind that it applies only to declarations and not to initializations.

  • 第二个定义是将一个函数赋值给一个变量,也生成一个函数,但是一个匿名(即未命名)函数; 然而,许多 JavaScript 引擎能够推断出应该是什么名称,然后将设置为second.name === "second"。 (请看下面的代码,它显示了匿名函数没有分配名称的情况。) 由于赋值没有被悬挂,所以只有在执行赋值之后才能访问该函数。 此外,你可能更喜欢用const而不是var来定义变量,因为你不会(不应该)改变函数:
var second = function() {};
console.log(second.name);
// "second"

var myArray = new Array(3);
myArray[1] = function() {};
console.log(myArray[1].name);
// ""
  • 第三个定义与第二个定义相同,除了函数现在有了自己的名称:third.name === "someName"

The name of a function is relevant when you want to call it, and also if you plan to perform recursive calls; we'll come back to this in Chapter 9, Designing Functions – Recursion. If you just want a function for, say, a callback, you can use one without a name; however, note that named functions are more easily recognized in an error traceback, the kind of listing you get to use when you are trying to understand what happened, and which function called what.

var myCounter = (function(initialValue = 0) {
  let count = initialValue;
  return function() {
    count++;
    return count;
  };
})(77);

myCounter(); // 78
myCounter(); // 79
myCounter(); // 80

仔细研究代码:外层函数接收一个参数(在本例中是77),用作count的初始值(如果没有提供初始值,则从0开始)。 内部函数可以访问count(因为闭包的原因),但是变量不能在其他任何地方访问。 在所有方面,返回的函数都是一个通用函数——唯一的区别是它对私有元素的访问。 这也是模块模式的基础。

  • 第五个定义是不安全的,你不应该使用它! 首先传递参数的名称,然后以字符串形式传递实际的函数体,然后使用eval()的等效函数创建函数,这可能会导致许多危险的操作,所以不要这样做! 只是为了满足你的好奇心,让我们来看一个例子重写函数非常简单的sum3()我们看到在传播部分第一章,成为功能——几个问题:
var sum3 = new Function("x", "y", "z", "var t = x+y+z; return t;");
sum3(4, 6, 7); // 17

This sort of definition is not only unsafe, but has some other quirks—they don't create closures with their creation contexts, and so they are always global. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function for more on this, but remember that using this way of creating functions isn't a good idea!

  • 最后一个定义使用了箭头=>定义,这是定义函数最简洁的方法,我们将尽可能使用它。

至此,我们已经看到了定义函数的几种方法,但现在让我们关注箭头函数,这是我们在为本书编写代码时最喜欢的一种风格。

箭头功能-现代方式

即使箭头函数和其他函数的工作方式差不多,但它们和普通函数之间还是有一些重要的区别。 箭头函数可以隐式返回一个值,即使没有return语句,this的值没有绑定,并且没有arguments对象。 让我们回顾一下这三点。

There are some extra differences: arrow functions cannot be used as constructors, they do not have a prototype property, and they cannot be used as generators because they don't allow the yield keyword. For more details on these points, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this.

在本节中,我们将讨论几个 JavaScript 函数相关的主题,包括:

  • 如何返回不同的值
  • 如何处理具有this价值的问题

  • 如何处理不同数量的参数

  • 一个重要的概念,curry,我们会在书的其余部分发现它的许多用法

返回值

在 lambda 编码风格中,函数只包含一个结果。 为了简洁起见,新的箭头函数提供了一种语法。 当你写类似于(x,y,z) =>后面跟着一个表达式时,就隐含了return。 例如,下面两个函数实际上与我们前面展示的sum3()函数具有相同的功能:

const f1 = (x, y, z) => x + y + z;

const f2 = (x, y, z) => {
  return x + y + z;
};

如果你想return一个对象,那么你必须使用括号; 否则,JavaScript 将假定代码如下。

A matter of style: when you define an arrow function with only one parameter, you can omit the parentheses around it. For consistency, I prefer to always include them. However, Prettier, the formatting tool I use (we mentioned it in Chapter 1, Becoming Functional - Several Questions) doesn't approve. Feel free to choose your style! 

最后一点:为了避免你认为这是一个非常不可能的情况,请查看本章后面的问题章节中一个非常常见的场景!

处理这个值

JavaScript 的一个经典问题是对this的处理,它的值并不总是你所期望的那样。 ES2015 用箭头函数解决了这个问题,它继承了正确的this值,从而避免了问题。 下面的代码是可能出现问题的示例:在调用 timeout 函数时,this将指向全局(window)变量,而不是新对象,因此您将在控制台中获得undefined:

function ShowItself1(identity) {
  this.identity = identity;
  setTimeout(function() {
    console.log(this.identity);
  }, 1000);
}

var x = new ShowItself1("Functional");
// *after one second,* undefined *is displayed*

有两种经典的方法可以用老式的 JavaScript 和箭头方式来解决这个问题:

  • 一种解决方案使用闭包并定义一个局部变量(通常命名为thatself),该变量将获得this的原始值,因此它不会是未定义的。
  • 第二种方法使用bind(),因此超时函数将被绑定到正确的this值上。
  • 第三种更现代的方法只是使用一个箭头函数,因此this无需更多的麻烦就可以获得正确的值(指向对象)。

We will also be using bind(). See the Of lambdas and functions section.

让我们看看实际代码中的三个解决方案。 我们为第一个超时使用闭包,为第二个超时使用绑定,为第三个超时使用箭头函数:

function ShowItself2(identity) {
  this.identity = identity;
  let that = this;
  setTimeout(function() {
    console.log(that.identity);
  }, 1000);

  setTimeout(
    function() {
      console.log(this.identity);
    }.bind(this),
    2000
  );

  setTimeout(() => {
    console.log(this.identity);
  }, 3000);
}

var x = new ShowItself2("JavaScript");
// *after one second, "JavaScript"*
// *after another second, the same*
// *after yet another second, once again*

如果您运行此代码,您将在一秒后得到JavaScript,然后在另一秒后再次得到,然后在另一秒后再次得到第三次; 这三种方法都是正确的,所以选择哪一种取决于您更喜欢哪一种。

使用参数

在第一章,成为功能——几个问题【5】,第二章,【显示】思维功能——第一个例子,我们看到一些使用的传播(...)算子。 然而,最实际的用法是处理参数; 我们会在第 6 章中看到一些例子。 让我们回顾一下我们的once()函数:

const once = func => {
  let done = false;
  return (...args) => {
    if (!done) {
      done = true;
      func(...args);
    }
  };
};

为什么我们要写return (...args) =>,然后是func(...args)? 答案与处理可变数量(可能为零)参数的更现代方法有关。 在旧版本的 JavaScript 中,您是如何管理这类代码的? 答案与arguments对象(而不是数组!)有关,该对象允许您访问传递给函数的实际参数。

For more on this, read https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/arguments.

在 JavaScript 5 和更早的时候,如果我们想要一个函数能够处理任意数量的参数,我们必须编写如下代码:

function somethingElse() {
  // *get arguments and do something*
}

function listArguments() {
  console.log(arguments);
  var myArray = Array.prototype.slice.call(arguments);
  console.log(myArray);
  somethingElse.apply(null, myArray);
}

listArguments(22, 9, 60);
// (3) [22, 9, 60, callee: function, Symbol(Symbol.iterator): function]
// (3) [22, 9, 60]

第一个日志显示,arguments实际上是一个对象; 第二个日志对应一个简单数组。 另外,请注意调用somethingElse()所需的复杂方式,这需要使用apply()

在最新的 JavaScript 版本中,等价的代码是什么? 它要短得多,这就是为什么我们会在书中看到几个使用扩展运算符的例子:

function listArguments2(...args) {
  console.log(args);
  somethingElse(...args);
}

listArguments2(12, 4, 56);
// (3) [12, 4, 56]

在查看这段代码时,您应该记住以下几点:

  • 通过编写listArguments2(...args),我们立即明确地表示我们的新函数接受几个(可能是零)参数。
  • 你不需要做任何事情来获取数组。 控制台日志显示args实际上是一个数组。
  • 写作somethingElse(...args)比我们之前使用的替代方法(使用apply())清晰得多。

顺便说一下,在当前的 JavaScript 版本中,arguments对象仍然可用。 如果你想从它创建一个数组,你有两种替代方法来做,而不必诉诸于Array.prototype.slice.call技巧:

  • from()方法写var myArray=Array.from(arguments)
  • let myArray=[...arguments],它显示了扩展运算符的另一种用法。

当我们讨论高阶函数时,编写处理参数未知的其他函数的函数将是司空见惯的事。

JavaScript 提供了一种更短的方式来实现这一点,这就是为什么您必须习惯这种用法的原因。 它是值得的!

一个还是多个论点?

也可以编写返回函数的函数,在第 6 章生成函数——高阶函数中,我们将看到更多这方面的内容。 例如,在 lambda 演算中,不需要编写带有多个参数的函数,而只需要一个参数; 你要用一种叫做curry的技术来做这件事(为什么要这样做?) 有这种想法; 我们稍后再谈)。

Currying gets its name from Haskell Curry, who developed the concept. Note that there is an FP language that is named after him—Haskell; double recognition! 

例如,我们之前看到的对三个数字求和的函数可以写成:

const altSum3 = x => y => z => x + y + z;

为什么我要更改函数的名称? 简单地说,因为这是而不是与我们之前看到的相同的函数。 尽管如此,它可以用于生成与前面的函数完全相同的结果。 话虽如此,但它在一个重要的方面是不同的。 让我们来看看你如何使用它,例如,对数字123求和:

altSum3(1)(2)(3); // 6

Test yourself before reading on, and think on this: what would have been returned if you had written altSum3(1,2,3) instead? Tip: it would not be a number! For the full answer, keep reading.

这是怎么做到的呢? 把它分成多个调用会有所帮助; 这将是 JavaScript 解释器实际计算前一个表达式的方式:

let fn1 = altSum3(1);
let fn2 = fn1(2);
let fn3 = fn2(3);

认为功能! 根据定义,调用altSum3(1)的结果是一个函数,该函数在闭包的作用下等价于:

let fn1 = y => z => 1 + y + z;

我们的altSum3()函数意味着接收一个参数,而不是三个! 这个调用的结果,fn1,也是一个单参数函数。 当你使用fn1(2)时,结果仍然是一个函数,也有一个参数,相当于:

let fn2 = z => 1 + 2 + z;

当你计算fn2(3)时,最终会返回一个值——太棒了! 正如我们所说的,该函数执行与我们前面看到的相同的计算,但以一种本质上不同的方式。

您可能认为 curry 只是一种特殊的技巧:谁会想只使用单参数函数呢? 你会看到原因当我们考虑如何加入功能在一起第八章,连接功能——流水线和作文,第十二章,建筑更好的容器——功能数据类型,它不会可行传递多个参数从一个步骤。

函数作为对象

一级对象的概念意味着函数可以被创建、赋值、更改、作为参数传递,并作为其他函数的结果返回,就像你可以使用数字或字符串一样。 让我们从它的定义开始。 让我们看看你通常是如何定义函数的:

function xyzzy(...) { ... }

这(几乎)相当于写以下内容:

var xyzzy = function(...) { ... }

然而,对于吊装则不成立。 在这种情况下,JavaScript 将所有定义移动到当前范围的顶部,但不移动赋值; 因此,对于第一个定义,您可以从代码中的任何地方调用xyzzy(...),但是对于第二个定义,您不能调用该函数,直到执行了赋值。

See the parallel with the Colossal Cave Adventure game? Invoking xyzzy(...) anywhere won't always work! And, if you have never played that famous interactive fiction game, try it online—for example, at http://www.web-adventures.org/cgi-bin/webfrotz?s=Adventure or http://www.amc.com/shows/halt-and-catch-fire/colossal-cave-adventure/landing.

我们想要说明的一点是,函数可以赋值给变量,如果需要,也可以重新赋值。 类似地,当需要函数时,我们可以当场定义它们。 我们甚至可以不命名它们:就像普通表达式一样,如果它们只被使用一次,那么您就不需要命名它们或将它们存储在变量中。

一个 React-Redux 减速器

我们可以看到另一个涉及函数赋值的例子。 正如我们在本章前面提到的,React-Redux 通过分派由减速器处理的动作来工作。 减速机通常包括带有开关的代码:

function doAction(state = initialState, action) {
  let newState = {};
  switch (action.type) {
    case "CREATE":
      // *update state, generating newState,*
      // *depending on the action data*
      // *to create a new item*
      return newState;

    case "DELETE":
      // *update state, generating newState,*
      // *after deleting an item*
      return newState;

    case "UPDATE":
      // *update an item,*
      // *and generate an updated state*
      return newState;

    default:
      return state;
  }
}

Providing initialState as a default value for state is a simple way of initializing the global state the first time around. Pay no attention to that default; it's not relevant for our example, but I included it just for the sake of completeness.

利用存储函数的可能性,我们可以构建一个分派表,并简化前面的代码。 首先,我们用每个操作类型的函数代码初始化一个对象。

基本上,我们只是使用前面的代码并创建单独的函数:

const dispatchTable = {
  CREATE: (state, action) => {
    // *update state, generating newState,*
    // *depending on the action data*
    // *to create a new item*
    return newState;
  },

  DELETE: (state, action) => {
    // *update state, generating newState,*
    // *after deleting an item*
    return newState;
  },

  UPDATE: (state, action) => {
    // *update an item,*
    // *and generate an updated state*
    return newState;
  }
};

我们将处理每种类型操作的不同函数作为属性存储在一个对象中,该对象将作为分派器表工作。 此对象只创建一次,并在应用执行期间保持不变。 有了它,我们现在可以用一行代码重写动作处理代码:

function doAction2(state = initialState, action) {
  return dispatchTable[action.type]
    ? dispatchTable[action.type](state, action)
    : state;
}

让我们分析一下:给定动作,如果action.type匹配调度对象中的属性,则执行存储它的对象中的相应函数。 如果没有匹配,我们就按照 Redux 的要求返回当前状态。 如果我们不能将函数(存储和调用它们)作为一级对象来处理,那么这种代码就不可能实现。

一个不必要的错误

然而,有一个常见的(尽管事实上是无害的)错误经常被犯。 你经常会看到这样的代码:

fetch("some/remote/url").then(function(data) {
  processResult(data);
});

这段代码是做什么的? 其思想是,获取一个远程 URL,当数据到达时,调用一个函数——这个函数本身使用data作为参数调用processResult。 也就是说,在then()部分,我们需要一个函数,给定data,计算processResult(data)。 但我们不是已经有这样的功能了吗?

A small bit of theory: in lambda calculus terms, we are replacing λx.func x with simply func—this is called an eta conversion, or more specifically, an eta reduction. (If you were to do it the other way round, it would be an eta abstraction.) In our case, it could be considered a (very, very small!) optimization, but its main advantage is shorter, more compact code.

基本上,当你看到以下情况时,你可以应用一条规则:

function someFunction(someData) { 
  return someOtherFunction(someData);
}

该规则规定,您可以用someOtherFunction替换类似于上述代码的代码。 所以,在我们的例子中,我们可以直接这样写:

fetch("some/remote/url").then(processResult);

这段代码与我们前面看到的方法完全相同(尽管它非常快,因为您避免了一个函数调用),但它更容易理解吗?

这种编程风格被称为pointfree风格或默会风格,其主要特点是您从不为每个函数应用指定参数。 这种编码方式的一个优点是,它帮助编写人员(以及代码的未来读者)考虑函数本身及其含义,而不是在较低的层次上工作,传递数据并使用它。 在较短的代码版本中,没有无关或无关的细节:如果您理解了被调用函数的作用,那么您就理解了整个代码段的含义。 在我们的文章中,我们经常(但不一定总是)这样做。

Unix/Linux users may already be accustomed to this style, because they work in a similar way when they use pipes to pass the result of a command as an input to another. When you write something as ls|grep doc|sort, the output of ls is the input to grep, and the latter's output is the input to sort—but input arguments aren't written out anywhere; they are implied. We'll come back to this in the Pointfree style section of Chapter 8, Connecting Functions - Pipelining and Composition.

处理方法

但是,有一种情况你应该知道:如果你调用一个对象的方法会发生什么? 看看下面的代码:

fetch("some/remote/url").then(function(data) {
  myObject.store(data);
});

如果你的原始代码与前面的代码类似,那么看起来很明显的转换后的代码将会失败:

fetch("some/remote/url").then(myObject.store);

为什么? 原因是,在原始代码中,被调用的方法绑定到一个对象(myObject),但在修改后的代码中,它没有绑定,只是一个free函数。 我们可以用bind()来简单地修复它,如下所示:

fetch("some/remote/url").then(myObject.store.bind(myObject));

这是一个通解。 当处理一个方法时,你不能只是分配它; 您必须使用bind(),以使正确的上下文可用。 看看下面的代码:

function doSomeMethod(someData) { 
  return someObject.someMethod(someData);
}

按照这个规则,类似前面代码的代码应该转换成以下代码:

const doSomeMethod = someObject.someMethod.bind(someObject);

Read more on bind() at https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_objects/Function/bind.

这看起来相当笨拙,也不是很优雅,但这是必须的,这样方法才能与正确的对象相关联。 当我们在第 6 章承诺函数时,我们将看到它的一个应用。 即使这个代码不是很好,只要你有处理对象(记住,我们并没有说我们会努力追求完全 FP 代码,并说我们会接受其他构造如果他们使事情更容易),你必须记得绑定方法作为一类对象传递之前 pointfree 风格。

**# 以 FP 方式使用函数

有几个常见的编码模式实际上利用了 FP 样式,即使您没有意识到它。 在本节中,我们将详细介绍它们,并查看代码的功能方面,以便您能够更加习惯这种编码风格。

然后,我们将通过考虑几种 FP 技术来详细了解如何以 FP 方式使用函数,如下所示:

  • 注入,这是需要排序不同的策略,以及其他用途
  • 回调和承诺,引入延续传递风格
  • Polyfilling 和存根
  • 直接调用计划

注射-分门别类

第一个将函数作为参数传递的例子是由Array.prototype.sort()方法提供的。 如果你有一个字符串数组,你想对它排序,你可以使用sort()方法。 例如,要用彩虹的颜色对数组进行字母排序,我们可以这样写:

var colors = [
  "violet",
  "indigo",
  "blue",
  "green",
  "yellow",
  "orange",
  "red"
];
colors.sort();
console.log(colors);
// *["blue", "green", "indigo", "orange", "red", "violet", "yellow"]*

注意,我们不需要为sort()调用提供任何参数,但是数组被很好地排序了。 默认情况下,该方法根据字符串的 ASCII 内部表示形式对字符串进行排序。 所以,如果你使用这个方法的数组进行排序数字,它将失败,因为它将决定,20必须1003,因为10020(作为字符串!),后者之前3,【显示】这需要修复! 下面的代码显示了这个问题:

var someNumbers = [3, 20, 100];
someNumbers.sort();

console.log(someNumbers);
// ***[100, 20, 3]***

但是让我们暂时忘记数字,继续对字符串进行排序。 我们想问问自己,如果我们想对一些西班牙语单词(palabras)进行排序,但遵循适当的区域设置规则,会发生什么情况? 我们将对字符串进行排序,但结果不会是正确的:

var palabras = ["ñandú", "oasis", "mano", "natural", "mítico", "musical"];
palabras.sort();

console.log(palabras);
// *["mano", "musical", "mítico", "natural", "oasis", "ñandú"]* -- ***wrong result***!

For language or biology buffs, "ñandú" in English is rhea, a running bird somewhat similar to ostriches. There aren't many Spanish words beginning with ñ, and we happen to have these birds in my country, Uruguay, so that's the reason for the odd word!

哦! 在西班牙语中,ñ位于no之间,但"ñandú"排在最后。 此外,"mítico"(在英语中,神话; 注意,重音í应该出现在"mano""musical"之间,因为波浪线应该被忽略。 解决这个问题的适当方法是提供一个与sort()比较的函数。 在这种情况下,我们可以采用以下的localeCompare()方法:

palabras.sort((a, b) => a.localeCompare(b, "es"));

console.log(palabras);
// *["mano", "mítico", "musical", "natural", "ñandú", "oasis"]*

a.localeCompare(b,"es")调用比较了ab字符串,并返回一个负数应该a``b之前,一个积极的值应该ab,和 0 如果a和【显示】是相同但据西班牙("es")排序规则。

现在一切都好了! 通过引入一个新函数spanishComparison()来执行所需的字符串比较,可以使代码更加清晰:

const spanishComparison = (a, b) => a.localeCompare(b, "es");

palabras.sort(spanishComparison);
// *sorts the palabras array according to Spanish rules:*
// *["mano", "mítico", "musical", "natural", "ñandú", "oasis"]*

在即将到来的章节中,我们将讨论如何 FP 让你更多的声明式的方式编写代码,生产更容易理解的代码,和这样的小改变可以帮助:当读者的代码sort,他们会立即推断正在做什么,即使评论不是礼物。

This way of changing the way that the sort() function works by injecting different comparison functions is actually a case of the strategy design pattern. We'll be learning more about this in Chapter 11, Implementing Design Patterns – the Functional Way.

提供一个sort函数作为参数(以一种非常 FP 的方式!)也可以帮助解决其他几个问题,例如:

  • sort()只适用于字符串。 如果您想对数字进行排序(就像我们之前尝试做的那样),您必须提供一个函数来进行数字比较。 例如,你可以这样写myNumbers.sort((a,b) => a-b)
  • 如果想按给定的属性对对象进行排序,可以使用一个与之进行比较的函数。 例如,你可以按照年龄对人们进行排序,比如myPeople.sort((a,b) => a.age - b.age)

For more on the localeCompare() possibilities, see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare. You can specify which locale rules to apply, in which order to place upper/lowercase letters, whether to ignore punctuation, and much more. But be careful: not all browsers may support the required extra parameters.

这是一个您以前可能使用过的简单示例,但它毕竟是 FP 模式。 让我们继续讨论在执行 Ajax 调用时函数作为参数的更常见用法。

回调、承诺和延续

作为一级对象传递的函数最常用的例子可能与回调和承诺有关。 在 Node 中,读取文件是异步完成的,代码如下:

const fs = require("fs");

fs.readFile("someFile.txt", (err, data) => {
  if (err) {
    console.error(err); // *or throw an error, or otherwise handle the problem*
  } else {
    console.log(data.toString()); // *do something with the data*
  }
});

readFile()函数需要一个回调函数——在本例中是一个匿名函数——当文件读取操作完成时将被调用。

更好的方法是使用承诺; 详见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise。 有了这个,当使用更现代的fetch()函数执行 Ajax web 服务调用时,您可以沿着以下代码行编写一些代码:

fetch("some/remote/url")
  .then(data => {
    // *Do some work with the returned data*
  })
  .catch(error => {
    // *Process all errors here*
  });

Note that if you had defined appropriate processData(data) and processError(error) functions, the code could have been shortened to fetch("some/remote/url").then(processData).catch(processError) along the lines that we saw previously.

最后,你还应该考虑使用async/await; 阅读更多关于它在 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_functionhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

连续的传递形式

在前面的代码中,您调用一个函数,但也传递另一个函数,该函数将在输入/输出操作完成时执行,可以认为是continuation pass style(CPS)的情况。 什么是编码技术? 一种看待它的方法是通过思考这个问题:如果使用return语句被禁止,你将如何编程? 【显示】

乍一看,这似乎是一种不可能的情况。 但是,我们可以通过向被调用的函数传递回调来解决这个问题,这样当这个过程准备返回给调用者时,它就会调用所传递的回调,而不是实际返回。 通过这样做,回调函数为被调用函数提供了继续进程的方法,因此使用单词continue。 我们现在不会讨论这个,但在第 9 章设计函数——递归中,我们将深入研究它。 特别是,正如我们将看到的,CPS 将帮助我们避免一个重要的递归限制。

弄清楚如何使用延续有时很有挑战性,但总是有可能的。 这种编码方式的一个有趣的优点是,通过自己指定进程将如何继续,您可以超越所有通常的结构(ifwhilereturn等等),实现您想要的任何机制。 这在某些过程不一定是线性的问题中非常有用。 当然,这也可能导致您发明一种控制结构,它比您可能想象的GOTO语句的可能用法更糟糕! 图 3.1显示了这种做法的危险!

Figure 3.1: What's the worse that could happen if you start messing with the program flow? This XKCD comic is available online at https://xkcd.com/292/

您不局限于传递单个延续。 与承诺一样,您可以提供两个或多个可选回调。 顺便说一下,这可以为如何处理异常提供一个解决方案。 如果我们简单地允许函数抛出错误,那么它将是对调用者的隐含返回,而我们不希望这样。 的办法是提供一个替代的回调(即不同的延续)使用时抛出异常(在 12 章,建筑更好的容器——功能数据类型,我们会发现另一个解决方案的使用单体):

function doSomething(a, b, c, normalContinuation, errorContinuation) {
  let r = 0;
  // *... do some calculations involving a, b, and c,*
  // *and store the result in r*

  // *if an error happens, invoke:*
  // *errorContinuation("description of the error")*

  // *otherwise, invoke:*
  // *normalContinuation(r)*
}

使用 CPS 甚至可以让您超越 JavaScript 提供的控制结构,但这将超出本书的目标,所以我将让您自己研究!

Polyfills

能够动态地分配函数(就像你可以给一个变量赋不同的值一样)也允许你在定义腻子填充时更有效地工作。

检测 Ajax

让我们回到 Ajax 开始出现的时候。 考虑到不同的浏览器以不同的方式实现 Ajax 调用,您总是必须围绕这些差异编写代码。 下面的代码展示了如何通过测试几个不同的条件来实现 Ajax 调用:

function getAjax() {
  let ajax = null;
  if (window.XMLHttpRequest) {
    // *modern browser? use XMLHttpRequest*
    ajax = new XMLHttpRequest();

  } else if (window.ActiveXObject) {
    // *otherwise, use ActiveX for IE5 and IE6*
    ajax = new ActiveXObject("Microsoft.XMLHTTP");

  } else {
    throw new Error("No Ajax support!");
  }

  return ajax;
}

这是有效的,但意味着您将为每个调用重做 Ajax 检查,即使测试结果永远不会改变。 有一种更有效的方法,那就是使用函数作为一级对象。 我们可以定义两个不同的函数,只对条件进行一次测试,然后分配正确的函数供以后使用; 研究以下代码以获得这样的替代方案:

(function initializeGetAjax() {
 let myAjax = null;

  if (window.XMLHttpRequest) {
    // *modern browsers? use XMLHttpRequest*
    myAjax = function() {
      return new XMLHttpRequest();
    };

  } else if (window.ActiveXObject) {
    // *it's ActiveX for IE5 and IE6*
    myAjax = function() {
      new ActiveXObject("Microsoft.XMLHTTP");
    };

  } else {
    myAjax = function() {
      throw new Error("No Ajax support!");
    };
  }

  window.getAjax = myAjax;
})();

这段代码显示了两个重要的概念。 首先,我们可以动态地分配一个函数:当这段代码运行时,window.getAjax(即全局getAjax变量)将根据当前浏览器获得三个可能的值之一。 当您稍后在代码中调用getAjax()时,正确的函数将执行,而不需要进行任何进一步的浏览器检测测试。

第二个有趣的想法是,我们定义initializeGetAjax函数,并立即运行它——这种模式称为立即调用函数表达式(IIFE)。 函数会运行,但是会在之后进行清理,因为它的所有变量都是局部的,甚至在函数运行之后都不存在。 稍后我们将了解更多。

添加缺失的功能

这种在运行中定义函数的想法还允许我们编写腻子,这些腻子提供了其他方法所缺少的函数。 例如,假设我们有如下代码:

if (currentName.indexOf("Mr.") !== -1) {
  // *it's a man*
  ...
}

与此相反,你可能更喜欢使用更新、更清晰的 of 方式,只需写以下内容:

if (currentName.includes("Mr.")) {
  // *it's a man*
  ...
}

如果你的浏览器不提供.includes()会发生什么? 同样,我们可以动态地定义适当的函数,但只在需要时才这样做。 如果.includes()可用,则不需要做任何操作,但如果缺少.includes(),则需要定义一个提供相同工作的填充。 下面的代码显示了这种填充的示例:

You can find polyfills for many modern JavaScript features at Mozilla's developer site. For example, the polyfill we used for includes was taken directly from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/includes.

if (!String.prototype.includes) {
  String.prototype.includes = function(search, start) {
    "use strict";
    if (typeof start !== "number") {
      start = 0;
    }

    if (start + search.length > this.length) {
      return false;
    } else {
      return this.indexOf(search, start) !== -1;
    }
  };
}

当这段代码运行时,它会检查String原型是否已经有includes方法。 如果没有,它会向它指定一个完成相同工作的函数,因此从那时起,您就可以放心地使用.includes()了。 顺便说一下,还有其他定义填充的方法:检查问题3.5的答案。

Directly modifying a standard type's prototype object is usually frowned upon, because in essence, it's equivalent to using a global variable, and thus it's prone to errors; however, this case (writing a polyfill for a well established and known function) is quite unlikely to provoke any conflicts.

最后,如果您碰巧认为前面展示的 Ajax 示例已经过时,请考虑以下内容: 如果您希望使用更现代的fetch()的方式调用服务,你也会发现并不是所有的现代浏览器都支持它(检查http://caniuse.com/搜索=获取来验证这一点),所以你必须使用 polyfill,例如在 https://github.com/github/fetch。 研究代码,你会发现它基本上使用了与前面描述的相同的方法来查看是否需要填充并创建它。

存根

在这里,我们将看一个用例,它在某些方面与使用 polyfill 类似:让一个函数根据环境完成不同的工作。 这个想法是执行stub,这个想法来自于测试,它涉及用另一个功能代替一个更简单的工作,而不是做实际的工作。

stub 通常与日志函数一起使用。 您可能希望应用在开发时执行详细的日志记录,但在生产时则不希望看到。 一个常见的解决方案是这样写:

let myLog = someText => {
  if (DEVELOPMENT) {
    console.log(someText); // *or some other way of logging*
  } else {
    // *do nothing*
  }
}

这是可行的,但与 Ajax 检测的例子一样,它做的工作比需要做的更多,因为它每次都检查应用是否处于开发中。

我们可以简化代码(并获得非常非常小的性能提升!),如果我们终止日志功能,使它不会真正记录任何东西; 一个简单的实现如下:

let myLog;
if (DEVELOPMENT) {
  myLog = someText => console.log(someText);
} else {
  myLog = someText => {};
}

我们甚至可以用三元运算符做得更好:

const myLog = DEVELOPMENT
  ? someText => console.log(someText)
  : someText => {};

这有点神秘,但我更喜欢它,因为它使用了一个不能修改的const

Given that JavaScript allows us to call functions with more parameters than arguments, and given that we aren't doing anything in myLog() when we are not in development, we could also have written () => {} and it would have worked fine. However, I do prefer keeping the same signature, and that's why I specified the someText argument, even if it wouldn't be used. It's your call! 

你会注意到我们一次又一次地使用函数作为一级对象的概念; 查看所有代码示例,您将看到!

直接调用

函数还有另一种常见用法,通常出现在流行的库和框架中,它允许您将其他语言的模块化优势引入 JavaScript(甚至是较老的版本!) 通常的写法如下:

(function() {
  // *do something...*
})();

Another equivalent style is (function(){ ... }())—note the different placement of the parentheses for the function call. Both styles have their fans; pick whichever suits you, but just follow it consistently.

你也可以使用相同的样式,但要传递一些参数给函数,作为其参数的初始值:

(function(a, b) {
  // *do something, using the*
  // *received arguments for a and b...*
})(some, values);

最后,你也可以从函数中返回一些东西:

let x = (function(a, b) {
  // *...return an object or function*
})(some, values);

正如我们之前提到的,这种模式本身被称为 IIFE(发音为iffy)。 这个名称很容易理解:您正在定义一个函数并立即调用它,因此它会当场执行。 为什么要这样做,而不是简单地内联编写代码? 原因与作用域有关。

Note the parentheses around the function. These help the parser understand that we are writing an expression. If you were to omit the first set of parentheses, JavaScript would think you were writing a function declaration instead of an invocation. The parentheses also serve as a visual note, so readers of your code will immediately recognize the IIFE. 

如果您在 IIFE 中定义任何变量或函数,那么由于 JavaScript 定义函数范围的方式,这些定义将是内部的,您的代码的其他部分将无法访问它们。 假设您想要编写一些复杂的初始化,如下所示:

function ready() { ... }

function set() { ... }

function go() { ... }

// *initialize things calling ready(),*
// *set() and go() appropriately*

会出什么问题呢? 这个问题取决于这样一个事实:你可能(意外地)有一个与这里三个中的任何一个同名的函数,而提升意味着将调用最后的函数:

function ready() {
  console.log("ready");
}

function set() {
  console.log("set");
}

function go() {
  console.log("go");
}

ready();
set();
go();

function set() {
  console.log("UNEXPECTED...");
}
// *"ready"*
// *"UNEXPECTED"*
// *"go"*

哦! 如果你使用生命周期,问题就不会发生了。 另外,这三个内部函数对其余代码甚至是不可见的,这有助于保持全局名称空间不受污染。 下面的代码显示了一个非常常见的模式:

(function() {
  function ready() {
    console.log("ready");
  }

  function set() {
    console.log("set");
  }

  function go() {
    console.log("go");
  }

  ready();
  set();
  go();
})();

function set() {
  console.log("UNEXPECTED...");
}
// *"ready"*
// *"set"*
// *"go"*

要查看一个涉及返回值的示例,我们可以重新访问第 1 章中的示例,并编写以下代码,创建单个计数器:

const myCounter = (function() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
})();

然后,每次调用myCounter()都会返回一个递增的计数,但是代码的任何其他部分都不可能覆盖内部的count变量,因为它只能在返回的函数中访问。

总结

在本章中,我们讨论了 JavaScript 中定义函数的几种方法,主要集中在箭头函数,它比标准函数有几个优点,包括更简洁。 我们学习了 curry 的概念(稍后将重新讨论),考虑了函数作为一级对象的某些方面,最后,我们考虑了几种恰好在概念上完全是 FP 的技术。 请放心,我们将使用本章中的所有内容作为本书其余部分中更高级技术的构建块; 等着瞧吧!

在第四章,正确的行为——纯函数,我们将更深入地研究函数和学习的概念纯函数,这将让我们更好的编程风格。

问题

3.1未初始化的对象? redux 程序员通常编写动作创建者代码,以简化动作的创建,这些动作稍后将由 reducer 处理。 操作是对象,必须包含一个type属性,该属性用于确定要调度的操作类型。 下面的代码应该可以做到这一点,但您能解释意外的结果吗?

const simpleAction = t => {
 type: t;
};

console.log(simpleAction("INITIALIZE"));
// ***undefined***

3.2。 允许使用箭头吗? 如果您使用箭头函数定义listArguments()listArguments2()工作参数部分,而不是我们使用function关键字的方式,一切是否相同?

3.3。 一行:一些程序员,特别节约代码行,建议将doAction2()重写为一行,即使你不能从格式中看出这一点! 你怎么想:这是对的还是错的?

const doAction3 = (state = initialState, action) =>
  (dispatchTable[action.type] &&
    dispatchTable[action.type](state, action)) ||
  state;

3.4。 发现 bug! 一个程序员,使用一个全局的状态存储(类似于 Redux, Mobx, Vuex 和其他不同的 web 框架使用的概念),想要记录(为了调试目的)对存储的set()方法的所有调用。 在创建新的存储对象之后,他写了以下内容,以便在实际处理之前将store.set()的参数记录下来。 不幸的是,代码没有像预期的那样工作。 是什么问题? 你能发现错误吗?

window.store = new Store();
const oldSet = window.store.set;
window.store.set = (...data) => (console.log(...data), oldSet(...data));

3.5。 indindless binding:设bind()不可用; 你怎么能给它做填充呢?**