【玩转 JS 函数式编程_010】3.2 JS 函数式编程筑基之:以函数式编程的方式活用函数(上)

写在前面
按照惯例,过长的篇幅分开介绍,本篇为 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 将位于 1003 之间,因为 10020 之前(排序元素均被视作字符串)而 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,它一种类似于鸵鸟的飞禽。虽然以 ñ 开头的西班牙语单词并不多,而笔者的国家乌拉圭恰好就有这些鸟类——这就是存在特殊单词的客观原因。

哎呀!在西班牙语中,ñ 介于 no 之间,但 ñandú 排到了末尾。此外,mítico(对应英文 mythical,注意重音字母 í)本应出现在 manomusical 之间,波浪号应该被忽略。要解决这个问题,需要向 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 进行比较:根据西班牙语("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_functionawait operator

3.2.3. 连续传递风格 Continuation passing style

前面的代码,在调用一个函数的同时,还传递了另一个在 I/O 操作完成时要执行的函数,可以认为是 连续传递风格CPS,Continuation Passing Style)的一种具体体现。这是一种什么样的编码技术呢?不妨从一个实际问题切入理解:如果禁止使用 return 语句,该怎样编程?

乍一看,这个问题似乎无从下手。然而,通过 将回调传函数递给被调用函数,我们便能寻得解决之道:当该过程准备返回控制权给调用者时,它不会实际返回,而是去调用所传递的回调函数。这么一来,回调函数就为被调用函数提供了延续该操作过程的一种途径,CPS 风格中的“连续”(continuation)就是这么来的。CPS 风格本节不具体展开,留待第九章《函数设计——递归》再进行深入研究。值得一提的是,正如我们将看到的那样,CPS 风格将有助于规避递归中的一个重要限制。

弄清“连续”的具体用法,有时是一件颇具挑战的事,但总归是能够达成的。这种编码方式一个有趣的好处在于,通过指定程序的接续方式,可以打破所有常见的程序控制结构(ifwhilereturn 等等),实现想要的任何控制流程。对于处理过程未必是线性的某些问题而言,这类编码风格将会非常有用。当然,这也可能导致您新发明的某种控制结构,远比想象中使用 GOTO 语句的后果更为糟糕!这种做法的危险如下图所示:

图 3.1 弄乱程序流程,可能会发生什么更糟糕的情况呢?

【图 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 现有的控制结构,但这超出了本书的讨论范围,感兴趣的读者可自行研究。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安冬的码畜日常

您的鼓励是我持续优质内容的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值