四、使用 JavaScript 实现函数式编程技术

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

在本章中,我们将执行以下操作:

  • 将所有核心概念放在一起,形成一个有凝聚力的范例
  • 探索当我们完全致力于风格时,函数式编程所能提供的美
  • 当功能模式建立在彼此之上时,逐步完成它们的逻辑发展
  • 与此同时,我们将构建一个简单的应用,做一些非常酷的事情

您可能已经注意到了上一章在处理 JavaScript 函数库时提到的一些概念,但在第 2 章函数编程基础中没有提到。嗯,那是有原因的!构图、曲线、局部应用等等。让我们来探索这些库为什么以及如何实现这些概念。

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

  • 数据泛型编程
  • 主要是函数式编程
  • 功能反应编程等等

然而,这一章将尽可能不带风格偏见。不要过于依赖一种类型的函数式编程,总的目标是表明有比通常被认为是正确和唯一的方法更好的方法来编写代码。一旦你解放了对什么是正确的方法和什么不是编写代码的正确方法的先入之见,你就可以做任何你想做的事情。当你只是像孩子一样毫无理由地写代码,而不是因为你喜欢它,当你不在乎遵循传统的做事方式时,那么可能性是无穷无尽的。

部分功能应用及电流

许多语言支持可选参数,但在 JavaScript 中不支持。JavaScript 使用了完全不同的模式,允许将任意数量的参数传递给函数。这为一些非常有趣和不寻常的设计模式打开了大门。功能可以部分或全部应用。

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

两者的区别现在可能还不清楚,但最终会很明显。

功能操作

实际上,在我们进一步解释如何实现部分应用和货币之前,我们需要一个回顾。如果我们要撕掉 JavaScript 厚厚的 C 类语法的外衣,暴露它的功能缺陷,那么我们需要了解 JavaScript 中的原语、函数和原型是如何工作的;如果我们只想设置一些 cookies 或验证一些表单字段,我们永远不需要考虑这些。

应用、调用和这个关键字

在纯函数语言中,不调用函数;他们被申请了。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()函数只适用于正好有两个参数的函数。我们可以写一些新的,适用于不同数量的论点,但这将远离我们的一般化模型。

我们需要的是部分应用。

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

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

  • 作为独立功能,即var partial = function(func){...
  • 作为多线,也就是Function.prototype.partial = function(){...

Polyfills 用于用新函数扩充原型,并允许我们将新函数作为我们希望部分应用的函数的方法来调用。就这样:myfunction.partial(arg1, arg2, …);

从左侧部分应用

这里是,JavaScript 的apply()call()实用程序对我们来说变得有用了。让我们看看函数对象的一个可能的聚合函数:

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'

部分应用允许我们获取一个非常通用的函数,并从中提取更具体的函数。但是这种方法最大的缺陷是传递参数的方式,比如传递的数量和顺序,可能是不明确的。而且模糊性在编程中从来都不是一件好事。有一个更好的方法:拍马屁。

当前

Currying 是将一个有很多参数的函数转换成一个有一个参数的函数,然后返回另一个需要更多参数的函数的过程。形式上,一个有 N 个参数的函数可以转化成 N 个函数的函数链,每个函数只有一个参数。

*一个常见的问题是:部分申请和 currying 有什么区别?虽然部分应用确实会立即返回一个值,而 currying 只返回另一个接受下一个参数的 curry ed 函数,但基本区别在于 currying 允许更好地控制如何将参数传递给函数。我们将看看这是如何实现的,但首先我们需要创建函数来执行货币兑换。

这是我们为函数原型添加电流的聚合线:

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

它将返回 curried 函数,直到所有需要的参数都被传入。它们是按照由函数所定义的从左到右的顺序传递的。

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

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 函数的可选参数来实现,它允许我们设置 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

因此,currying 不适用于接受可变数量参数的函数。对于类似的事情,部分应用是首选。

所有这些不仅仅是为了函数工厂和代码重用。迎合和部分应用进入一个更大的模式,称为合成。

功能组成

最后,我们到达了函数合成。

在函数式编程中,我们希望一切都是函数。如果可能的话,我们特别想要一元函数。如果我们能把所有函数都转换成一元函数,那么神奇的事情就会发生。

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

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

化合物

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

在我们定义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

但是有几个关键的区别我们应该讨论一下:

  • 显然第一种方法冗长且效率低下。
  • The second method is a nice one-liner, but this approach becomes very unreadable after only a few functions are applied.

    说代码越少越好是没有意义的。有效的指令越简洁,代码越容易维护。如果你减少屏幕上的字符数量,而不改变执行的有效指令,这将产生完全相反的效果——代码变得更难理解,并且明显不太容易维护;例如,当我们使用嵌套三元运算符时,或者我们在一行中将几个命令链接在一起时。这些方法减少了“屏幕上的代码”数量,但并没有减少代码实际指定的步骤数量。因此,其效果是混淆和使代码更难理解。使代码更容易维护的那种简洁是有效减少指定指令的简洁(例如,通过使用更简单的算法,用更少和/或更简单的步骤实现相同的结果),或者当我们简单地用消息替换代码时,例如,调用具有良好记录的应用编程接口的第三方库。

  • 第三种方法是数组函数链,特别是map函数。这相当有效,但在数学上并不正确。

  • 这是我们的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 的一个非常棒的方法。

我们只是勉强触及了函数组合能做什么的表面。compose 所做的是将控制权从 JavaScript 中拿走。通常 JavaScript 会从左到右进行评估,但是现在解释器会说“好的,其他东西会处理这个,我会继续下一个。”现在compose()功能已经控制了评估顺序!

这就是Lazy.jsBacon.js等人如何能够实现懒惰评价和无限序列等东西。接下来,我们将研究如何使用这些库。

多为函数式编程

什么是没有副作用的程序?无所作为的程序。

用带有不可避免的副作用的函数代码来补充我们的代码可以称为“大部分是函数式编程。”在同一个代码库中使用多个范例,并将它们应用到最优化的地方是最好的方法。大多数函数式编程都是纯传统函数式程序的建模方式:将大部分逻辑保存在纯函数中,并与命令式代码接口。

这就是我们如何编写自己的应用。

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

我们将使用 Lazy.js 作为我们的函数库。我们也将变得懒惰:不再担心处理所有的用户登录和退出、网络套接字、数据库等等,我们将假装有一个通用的应用对象为我们做这些,并且恰好有完美的 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库,这将允许我们快速开发功能反应编程 ( 玻璃钢)应用。应用一年中只会有一天(选举日)使用,我们的老板认为它应该花费成比例的时间。有了函数式编程和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.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 秒运行一次的循环中,我们可以完成工作。但是这种方法会敲打网络,效率非常低。那就没什么用了。相反,让我们深入挖掘一下Bacon.js库。

在培根中,有事件流和属性参数。属性可以被认为是“神奇”的变量,随着时间的推移而变化,以响应事件。它们不是真正的魔法,因为它们仍然依赖于一连串的事件。该属性相对于其事件流随时间变化。

Bacon.js图书馆还有另一个锦囊妙计。Bacon.fromPromise()功能是通过使用承诺将事件发送到流中的一种方式。从 jQuery 1 . 5 . 0 版本开始,jQuery AJAX 实现了 promises 接口。因此,我们需要做的就是编写一个 AJAX 搜索函数,当异步调用完成时,它会发出事件。每次承诺被解决,它都会调用 EvenStream 的订阅者。

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 中的函数范式。我们涵盖了功能工厂、电流、功能组成以及使其工作所需的一切。我们使用这些概念构建了一个极其模块化的应用。然后我们展示了如何使用一些本身使用这些相同概念的函数库,即函数组合,来操纵执行顺序。

在这一章中,我们讨论了函数式编程的几种风格:数据泛型编程,主要是函数式编程,以及函数式反应式编程。它们彼此之间并没有什么不同,它们只是在不同情况下应用函数式编程的不同模式。

在前一章中,简单提到了一个叫做范畴论的东西。在下一章中,我们将更多地了解它是什么以及如何使用它。*