写在前面
按照惯例,过长的篇幅分开介绍,本篇为 JavaScript 函数式编程核心基础的第二部分——以函数式编程的方式活用函数的上篇,分别介绍了 JS 函数在排序、回调、Promise 期约、以及连续传递等应用场景下的用法演示。和之前章节相比难度又有一定的提升。准备好了吗?
3.2. 以函数式编程的方式使用函数 Using functions in FP ways
有几种常见的编码模式实际上采用了函数式编程的风格,而您甚至都未曾察觉。本节将逐一考察这些模式的具体表现形式,以便您更快地习惯这种编码风格。这些模式包括:
- 注入模式:用于筛选不同策略及其他用途;
- 回调与期约模式:将引入连续传值的相关概念;
- 填充与插入模式;
- 立即调用策略模式。
3.2.1. 注入——整理出来 Injection – sorting it out
Array.prototype.sort()
方法提供了第一个将函数作为参数传递的示例。 给定一个待排序的字符串数组,则可以调用 array.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
将位于 100
和 3
之间,因为 100
在 20
之前(排序元素均被视作字符串)而 20
又在 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!
拓展知识
对于语言或生物学爱好者而言,
ñandú
的英文是rhea
,它一种类似于鸵鸟的飞禽。虽然以ñ
开头的西班牙语单词并不多,而笔者的国家乌拉圭恰好就有这些鸟类——这就是存在特殊单词的客观原因。
哎呀!在西班牙语中,ñ
介于 n
和 o
之间,但 ñandú
排到了末尾。此外,mítico
(对应英文 mythical
,注意重音字母 í
)本应出现在 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")
会对 a
和 b
进行比较:根据西班牙语("es"
)的排序规则,当 a
先于 b
时返回一个负值;a
落后于 b
时返回一个正值;两者相等时返回 0
。
现在排序结果正确了!此时可引入一个新函数 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"]
在接下来的章节中,我们将讨论函数式编程如何让您以更贴近声明式的方式来编写代码,生成更易于理解的代码。这类微小的改变是很有帮助的:当阅读代码的人读到排序这部分时,他们就可以在不借助注释的情况下立即推断出将会执行的逻辑。
小贴士
这种通过注入不同的比较函数来改变
sort()
函数工作方式的模式,实际上是策略设计模式的一种表现形式。第 11 章《实现函数式的设计模式》会具体论述。
提供一个排序函数作为参数(典型的函数式编程风格)还有助于解决其他问题,例如:
sort()
仅适用于字符串。要对数字进行排序,必须提供一个数字排序函数,如:myNumbers.sort((a,b) => a-b)
;- 如要按给定属性对对象排序,则需要传入一个与该属性值进行比较的函数。如:
myPeople.sort((a,b) => a.age - b.age)
可以按年龄升序对人员进行排序。
小贴士
更多
localeCompare()
介绍,请参阅 MDN 官方文档。您可以指定区域规则、大小写字母的排序规则以及是否忽略标点符号等。但请注意:并非所有浏览器都支持所需的额外参数。
这是一个您以前可能用过的简单示例,但它毕竟是一种函数式编程模式。接下来让我们来看看调用 Ajax
时将函数作为参数的更常见用法。
3.2.2. 回调、期约及延续 Callbacks, promises, and continuations
作一等对象传参的函数最常用的示例应该就是回调(callbacks
)和期约(promises
)了。在 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()
需要一个回调函数——本例为一个匿名函数——它将在文件读取操作完成时被调用。
更好的方法是使用 Promise
,详细介绍参考 MDN 文档。有了 Promise
,当使用更现代的 fetch()
函数执行 Ajax
调用 Web
服务时,可以按以下代码执行一些逻辑:
fetch("some/remote/url")
.then(data => {
// Do some work with the returned data
})
.catch(error => {
// Process all errors here
});
提示
请注意,如果定义了适当的
processData(data)
和processError(error)
函数,则代码可以像之前提过的那样,精简为fetch("some/remote/url").then(processData).catch(processError)
。
最后,还应该考虑使用 async / await
,具体用法详见 MDN 文档 async_function 和 await operator。
3.2.3. 连续传递风格 Continuation passing style
前面的代码,在调用一个函数的同时,还传递了另一个在 I/O 操作完成时要执行的函数,可以认为是 连续传递风格(CPS,Continuation Passing Style)的一种具体体现。这是一种什么样的编码技术呢?不妨从一个实际问题切入理解:如果禁止使用 return
语句,该怎样编程?
乍一看,这个问题似乎无从下手。然而,通过 将回调传函数递给被调用函数,我们便能寻得解决之道:当该过程准备返回控制权给调用者时,它不会实际返回,而是去调用所传递的回调函数。这么一来,回调函数就为被调用函数提供了延续该操作过程的一种途径,CPS
风格中的“连续”(continuation
)就是这么来的。CPS
风格本节不具体展开,留待第九章《函数设计——递归》再进行深入研究。值得一提的是,正如我们将看到的那样,CPS
风格将有助于规避递归中的一个重要限制。
弄清“连续”的具体用法,有时是一件颇具挑战的事,但总归是能够达成的。这种编码方式一个有趣的好处在于,通过指定程序的接续方式,可以打破所有常见的程序控制结构(if
、while
、return
等等),实现想要的任何控制流程。对于处理过程未必是线性的某些问题而言,这类编码风格将会非常有用。当然,这也可能导致您新发明的某种控制结构,远比想象中使用 GOTO
语句的后果更为糟糕!这种做法的危险如下图所示:
【图 3.1 弄乱程序流程,可能会发生什么更糟糕的情况呢?】
拓展
这部
XKCD
漫画可以在 这里 在线访问。
此外,可供传递的“连续”体也可以不止一个。就像 Promise
那样,可以提供两个或多个回调逻辑参与传递。顺便说一句,这一特性可用于异常处理领域:如果只是允许一个函数可以抛出一个错误,那么该错误就很可能潜在地返回给调用者,而事实上我们并不希望这样。解决问题的关键在于:提供另一个专门处理报错的回调函数(即不同的连续体),以便在抛出异常时使用(第十二章《构建更好的容器——函数式数据类型》将提出一个基于 monads
的新解决方案):
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
现有的控制结构,但这超出了本书的讨论范围,感兴趣的读者可自行研究。