mirror of
https://github.com/apachecn/eloquent-js-3e-zh.git
synced 2025-05-23 20:02:20 +00:00
5.
This commit is contained in:
parent
bd03a042ba
commit
b7e3ed3f20
289
5.md
289
5.md
@ -1,11 +1,15 @@
|
||||
## 五、高阶函数
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
开发大型程序通常需要耗费大量财力和物力,这绝不仅仅是因为构建程序所花费时间的问题。大型程序的复杂程度总是很高,而这些复杂性也会给开发人员带来不少困扰,而程序错误或缺陷(bug)往往就是这些时候引入的。大型程序为这些缺陷提供了良好的藏身之所,因此我们更加难以在大型程序中找到它们。
|
||||
|
||||
让我们简单回顾一下前言当中的两个示例。其中第一个程序包含了6行代码并可以直接运行。
|
||||
|
||||
```
|
||||
var total = 0, count = 1;
|
||||
let total = 0, count = 1;
|
||||
while (count <= 10) {
|
||||
total += count;
|
||||
count += 1;
|
||||
@ -45,109 +49,69 @@ sum和range这两个函数定义的操作当然会包含循环、计数和其他
|
||||
|
||||
在编程的时候,我们不能期望所有功能都是现成的。因此,你可能就会像第一份食谱那样编写你的程序,逐个编写计算机需要执行的代码和步骤,而忽略了这些步骤之上的抽象概念。
|
||||
|
||||
作为一名程序员,我们需要具备在恰当时候将代码抽象出来,形成一个新的函数或概念的能力。
|
||||
在编程时,注意您的抽象级别什么时候过低,是一项非常有用的技能。
|
||||
|
||||
### 5.2 数组遍历抽象
|
||||
### 5.2 重复的抽象
|
||||
|
||||
我们已经了解的普通函数就是一种很好的构建抽象的工具。但有些时候,光有函数也不一定能够解决我们的问题。
|
||||
|
||||
在前面的章节中,出现了好几次这样的for循环语句:
|
||||
程序以给定次数执行某些操作很常见。 你可以为此写一个`for`循环,就像这样:
|
||||
|
||||
```
|
||||
var array = [1, 2, 3];
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
var current = array[i];
|
||||
console.log(current);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
console.log(i);
|
||||
}
|
||||
```
|
||||
|
||||
这段代码的含义是,将数组中每个元素都打印到控制台中。但这段代码其实绕了个弯,它使用了一个用于计数的变量i,该变量用于检查数组的长度和提取当前遍历的元素。这样的编写方法不仅不够优雅,而且也为程序缺陷留下了可乘之机。我们有可能不小心复用了变量i、错误地将length拼写成length,或者混淆了变量i和current等。
|
||||
|
||||
那么,让我们来试试将这段代码抽象成一个函数。你会怎么做?
|
||||
|
||||
嗯,遍历整个数组并调用console.log来打印每个元素,倒是非常简单。
|
||||
我们是否能够将“做某件事`N`次”抽象为函数? 编写一个调用`console.log` `N`次的函数是很容易的。
|
||||
|
||||
```
|
||||
function logEach(array) {
|
||||
for (var i = 0; i < array.length; i++)
|
||||
console.log(array[i]);
|
||||
}
|
||||
```
|
||||
|
||||
但如果我们想执行打印元素以外的操作该怎么办呢?我们可以使用函数来定义我们想做的事,而函数也是值,因此我们可以将期望执行的操作封装成函数,然后传递进来。
|
||||
|
||||
```
|
||||
function forEach(array, action) {
|
||||
for (var i = 0; i < array.length; i++)
|
||||
action(array[i]);
|
||||
}
|
||||
|
||||
forEach(["Wampeter", "Foma", "Granfalloon"], console.log);
|
||||
// → Wampeter
|
||||
// → Foma
|
||||
// → Granfalloon
|
||||
```
|
||||
|
||||
通常来说,我们不会给forEach传递一个预定义的函数,而是直接新建一个函数值。
|
||||
|
||||
```
|
||||
var numbers = [1, 2, 3, 4, 5], sum = 0;
|
||||
forEach(numbers, function(number) {
|
||||
sum += number;
|
||||
});
|
||||
console.log(sum);
|
||||
// → 15
|
||||
```
|
||||
|
||||
这部分代码看起来跟一般的for循环差不多,在函数体中包含了一个语句块。但是,现在循环体位于函数值的内部,即包含在forEach函数的括号当中。因此我们要在整个语句之后添加右大括号和右括号。
|
||||
|
||||
我们可以使用这种方式为当前元素(number)指定一个变量名,而不是手动从数组中提取元素。
|
||||
|
||||
实际上,我们不需要自己编写forEach函数,该函数其实是数组的一个标准方法。因为数组提供的方法是对自身进行操作,因此forEach函数实际上只接受一个参数,即每个元素的处理函数。
|
||||
|
||||
为了证明这种代码编写方式的作用,我们来回顾一下上一章中的一个函数。这个函数包含两个循环体,用于遍历数组。
|
||||
|
||||
```
|
||||
function gatherCorrelations(journal) {
|
||||
var phis = {};
|
||||
for (var entry = 0; entry < journal.length; entry++) {
|
||||
var events = journal[entry].events;
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var event = events[i];
|
||||
if (!(event in phis))
|
||||
phis[event] = phi(tableFor(event, journal));
|
||||
}
|
||||
function repeatLog(n) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
console.log(i);
|
||||
}
|
||||
return phis;
|
||||
}
|
||||
```
|
||||
|
||||
使用forEach来改写这段代码,可以让实现变得更加简洁清晰。
|
||||
但如果我们想执行打印数字以外的操作该怎么办呢?我们可以使用函数来定义我们想做的事,而函数也是值,因此我们可以将期望执行的操作封装成函数,然后传递进来。
|
||||
|
||||
```
|
||||
function gatherCorrelations(journal) {
|
||||
var phis = {};
|
||||
journal.forEach(function(entry) {
|
||||
entry.events.forEach(function(event) {
|
||||
if (!(event in phis))
|
||||
phis[event] = phi(tableFor(event, journal));
|
||||
});
|
||||
});
|
||||
return phis;
|
||||
function repeat(n, action) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
action(i);
|
||||
}
|
||||
}
|
||||
|
||||
repeat(3, console.log);
|
||||
// → 0
|
||||
// → 1
|
||||
// → 2
|
||||
```
|
||||
|
||||
您不必将预定义的函数传递给`repeat`。 通常情况下,您希望原地创建一个函数值。
|
||||
|
||||
```
|
||||
let labels = [];
|
||||
repeat(5, i => {
|
||||
labels.push(`Unit ${i + 1}`);
|
||||
});
|
||||
console.log(labels);
|
||||
// → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]
|
||||
```
|
||||
|
||||
这个结构有点像`for`循环 - 它首先描述了这种循环,然后提供了一个主体。 但是,主体现在写为一个函数值,它被包裹在`repeat`调用的括号中。 这就是它必须用右小括号和右大括号闭合的原因。 在这个例子中,主体是单个小表达式,你也可以省略大括号并将循环写成单行。
|
||||
|
||||
### 5.3 高阶函数
|
||||
|
||||
如果一个函数操作其他函数,即将其他函数作为参数或将函数作为返回值,那么我们可以将其称为高阶函数。如果你已经熟悉了函数就是一个普通的值的话,那么高阶函数也就不是什么稀奇的概念了。高阶这个术语来源于数学,在数学当中,函数和值的概念有着严格的区分。
|
||||
如果一个函数操作其他函数,即将其他函数作为参数或将函数作为返回值,那么我们可以将其称为高阶函数。因为我们已经看到函数就是一个普通的值,那么高阶函数也就不是什么稀奇的概念了。高阶这个术语来源于数学,在数学当中,函数和值的概念有着严格的区分。
|
||||
|
||||
我们可以使用高阶函数对一系列操作和值进行抽象。高阶函数有多种表现形式。比如你可以使用高阶函数来新建另一些函数。
|
||||
|
||||
```
|
||||
function greaterThan(n) {
|
||||
return function(m) { return m > n; };
|
||||
return m => m > n;
|
||||
}
|
||||
var greaterThan10 = greaterThan(10);
|
||||
let greaterThan10 = greaterThan(10);
|
||||
console.log(greaterThan10(11));
|
||||
// → true
|
||||
```
|
||||
@ -156,16 +120,16 @@ console.log(greaterThan10(11));
|
||||
|
||||
```
|
||||
function noisy(f) {
|
||||
return function(arg) {
|
||||
console.log("calling with", arg);
|
||||
var val = f(arg);
|
||||
console.log("called with", arg, "- got", val);
|
||||
return val;
|
||||
return (...args) => {
|
||||
console.log("calling with", args);
|
||||
let result = f(...args);
|
||||
console.log("called with", args, ", returned", result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
noisy(Boolean)(0);
|
||||
// → calling with 0
|
||||
// → called with 0 - got false
|
||||
noisy(Math.min)(3, 2, 1);
|
||||
// → calling with [3, 2, 1]
|
||||
// → called with [3, 2, 1] , returned 1
|
||||
```
|
||||
|
||||
你甚至可以使用高阶函数来实现新的控制流。
|
||||
@ -174,12 +138,8 @@ noisy(Boolean)(0);
|
||||
function unless(test, then) {
|
||||
if (!test) then();
|
||||
}
|
||||
function repeat(times, body) {
|
||||
for (var i = 0; i < times; i++) body(i);
|
||||
}
|
||||
|
||||
repeat(3, function(n) {
|
||||
unless(n % 2, function() {
|
||||
repeat(3, n => {
|
||||
unless(n % 2 == 1, () => {
|
||||
console.log(n, "is even");
|
||||
});
|
||||
});
|
||||
@ -187,143 +147,92 @@ repeat(3, function(n) {
|
||||
// → 2 is even
|
||||
```
|
||||
|
||||
高阶函数的用法正是利用了我们在第3章中所讨论的词法作用域规则。在前面的示例当中,变量n是属于外部函数的参数,但由于内部函数可以访问外部的作用域,因此内部的函数也可以使用变量n。内部函数的函数体可以访问其外部环境的变量,这与循环及条件语句中的{}语句块有异曲同工之妙。其中内部函数与语句块之间的主要区别在于,在内部函数中声明的变量不会因为外部函数执行结束而丢失,这个特性十分有用。
|
||||
|
||||
### 5.4 参数传递
|
||||
|
||||
我们在前面定义的函数noisy会把参数传递给另一个函数使用,这会导致一个相当严重的问题。
|
||||
有一个内置的数组方法,`forEach`,它提供了类似`for/of`循环的东西,作为一个高阶函数。
|
||||
|
||||
```
|
||||
function noisy(f) {
|
||||
return function(arg) {
|
||||
console.log("calling with", arg);
|
||||
var val = f(arg);
|
||||
console.log("called with", arg, "- got", val);
|
||||
return val;
|
||||
};
|
||||
["A", "B"].forEach(l => console.log(l));
|
||||
// → A
|
||||
// → B
|
||||
```
|
||||
|
||||
## 脚本数据集
|
||||
|
||||
数据处理是高阶函数表现突出的一个领域。 为了处理数据,我们需要一些真实数据。 本章将使用脚本书写系统的数据集,例如拉丁文,西里尔文或阿拉伯文。
|
||||
|
||||
请记住第 1 章中的 Unicode,该系统为书面语言中的每个字符分配一个数字。 大多数这些字符都与特定的脚本相关联。 该标准包含 140 个不同的脚本 - 81 个今天仍在使用,59 个是历史性的。
|
||||
|
||||
虽然我只能流利地阅读拉丁字符,但我很欣赏这样一个事实,即人们使用其他至少 80 种书写系统来编写文本,其中许多我甚至不认识。 例如,以下是泰米尔语手写体的示例。
|
||||
|
||||
![]()
|
||||
|
||||
示例数据集包含 Unicode 中定义的 140 个脚本的一些信息。 本章的[编码沙箱](https://eloquentjavascript.net/code#5)中提供了`SCRIPTS`绑定。 该绑定包含一组对象,其中每个对象都描述了一个脚本。
|
||||
|
||||
```
|
||||
{
|
||||
name: "Coptic",
|
||||
ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
|
||||
direction: "ltr",
|
||||
year: -200,
|
||||
living: false,
|
||||
link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
|
||||
}
|
||||
```
|
||||
|
||||
如果函数f接受多个参数,那么该函数只能接受第一个参数。我们可以为内部函数添加多个参数(arg1、arg2等),然后将这些参数传递给f,但问题在于noisy函数并不知道f函数需要多少参数。因为noisy函数只能传递固定数量的参数给f,因此也不能获取函数的arguments.length,函数f没有办法知道调用者传递给noisy的参数个数。
|
||||
这样的对象会告诉您脚本的名称,分配给它的 Unicode 范围,书写方向,(近似)起始时间,是否仍在使用以及更多信息的链接。 方向可以是从左到右的`"ltr"`,从右到左的`"rtl"`(阿拉伯语和希伯来语文字的写法),或者从上到下的`"ttb"`(蒙古文的写法)。
|
||||
|
||||
JavaScript函数的apply方法可以解决这个问题。该方法接受一个参数数组(或与数组相似的对象),并使用这些参数调用其所属的函数。
|
||||
|
||||
```
|
||||
function transparentWrapping(f) {
|
||||
return function() {
|
||||
return f.apply(null, arguments);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
这里的trasparentWrapping函数没有什么实际用处,但该函数返回的内部函数可以将用户指定的参数全部传递给f。其原理是内部函数调用f函数的apply方法,并将自身的arguments对象传递给该方法。这里我们将apply方法的第一个参数设置为null,该参数用于模拟方法调用。我们会在下一章中使用这个函数的时候再进行详细介绍。
|
||||
|
||||
### 5.5 JSON
|
||||
|
||||
在JavaScript中,许多高阶函数都会对数组中的元素应用某个函数进行处理。在这些函数当中,最简单的就是forEach方法。数组中提供了许多类似于forEach的方法。为了熟悉这些方法的用法,我们来看一下另一个数据集。
|
||||
|
||||
几年前,有人翻阅了大量资料并把这些资料整理成了一本我家的族谱(姓氏是Haverbeke,即Oatbrook)。我翻阅了这本书期望能够找到骑士、海盗或炼金师,结果我找到的基本都是弗兰德的农民。作为我自己的消遣习惯,我从中提取了我的直系祖先的信息,并将这些数据录入了计算机当中。
|
||||
|
||||
我创建的文件内容是这样的:
|
||||
|
||||
```
|
||||
[
|
||||
{"name": "Emma de Milliano", "sex": "f",
|
||||
"born": 1876, "died": 1956,
|
||||
"father": "Petrus de Milliano",
|
||||
"mother": "Sophia van Damme"},
|
||||
{"name": "Carolus Haverbeke", "sex": "m",
|
||||
"born": 1832, "died": 1905,
|
||||
"father": "Carel Haverbeke",
|
||||
"mother": "Maria van Brussel"},
|
||||
… and so on
|
||||
]
|
||||
```
|
||||
|
||||
这种格式是JSON格式(发音Jason),即JavaScript Object Notation的缩写。该格式广泛用于数据存储和Web通信。
|
||||
|
||||
编写JSON格式代码的方式与编写JavaScript数组和对象的方式十分相似,不过有些限制条件。所有属性名都必须用双引号括起来,而且只能使用简单的数据表达式,不能填写函数调用、变量以及任何含有实际计算过程的代码。JSON当中也不能包含注释。
|
||||
|
||||
JavaScript提供了JSON.stringify函数,用于将数据转换成该格式。还有JSON.parse函数,用于将该格式数据转换成原有的数据类型。JSON.stringify接受一个JavaScript的值,并返回一个JSON编码的字符串。JSON.parse接受一个字符串,并返回一个解码后的值。
|
||||
|
||||
```
|
||||
var string = JSON.stringify({name: "X", born: 1980});
|
||||
console.log(string);
|
||||
// → {"name":"X","born":1980}
|
||||
console.log(JSON.parse(string).born);
|
||||
// → 1980
|
||||
```
|
||||
|
||||
你可以从在线沙盒([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/))的本章中下载一个文件,该文件中包含了一个ANCESTRY_FILE变量。该变量是一个包含JSON文件内容的字符串。让我们将字符串解码,并统计一下数据中包含的人数。
|
||||
|
||||
```
|
||||
var ancestry = JSON.parse(ANCESTRY_FILE);
|
||||
console.log(ancestry.length);
|
||||
// → 39
|
||||
```
|
||||
`ranges`属性包含 Unicode 字符范围数组,每个数组都有两元素,包含下限和上限。 这些范围内的任何字符代码都会分配给脚本。 下限是包括的(代码 994 是一个科普特字符),并且上限排除在外(代码 1008 不是)。
|
||||
|
||||
### 5.6 数组过滤
|
||||
|
||||
我们可以使用以下函数来查询族谱数据中1924年的年轻人。该函数会过滤掉数组中不符合规定的元素。
|
||||
为了找到数据集中仍在使用的脚本,以下函数可能会有所帮助。 它过滤掉数组中未通过测试的元素:
|
||||
|
||||
```
|
||||
function filter(array, test) {
|
||||
var passed = [];
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
if (test(array[i]))
|
||||
passed.push(array[i]);
|
||||
let passed = [];
|
||||
for (let element of array) {
|
||||
if (test(element)) {
|
||||
passed.push(element);
|
||||
}
|
||||
}
|
||||
return passed;
|
||||
}
|
||||
|
||||
console.log(filter(ancestry, function(person) {
|
||||
return person.born > 1900 && person.born < 1925;
|
||||
}));
|
||||
// → [{name: "Philibert Haverbeke", …}, …]
|
||||
console.log(filter(SCRIPTS, script => script.living));
|
||||
// → [{name: "Adlam", …}, …]
|
||||
```
|
||||
|
||||
该函数使用test函数作为参数来实现过滤操作。我们对数组中的每个元素调用test函数,并通过其返回值来确定当前元素是否满足条件。
|
||||
|
||||
在该文件中,有三个人满足过滤条件:我的祖父、祖母和叔(或伯)祖母。
|
||||
该函数使用名为`test`的参数(一个函数值)填充计算中的“间隙” - 决定要收集哪些元素的过程。
|
||||
|
||||
需要注意的是,filter函数并没有从当前数组中删除元素,而是新建了一个数组,并将满足条件的元素存入新建的数组中。这个函数是一个“纯函数”,因为该函数并未修改给定的数组。
|
||||
|
||||
与forEach一样,filter函数也是数组中提供的一个标准方法。本例中定义的函数只是用于展示内部实现原理。今后我们会使用以下方法来过滤数据:
|
||||
与forEach一样,filter函数也是标准的数组方法。本例中定义的函数只是用于展示内部实现原理。今后我们会使用以下方法来过滤数据:
|
||||
|
||||
```
|
||||
console.log(ancestry.filter(function(person) {
|
||||
return person.father == "Carel Haverbeke";
|
||||
}));
|
||||
// → [{name: "Carolus Haverbeke", …}]
|
||||
console.log(SCRIPTS.filter(s => s.direction == "ttb"));
|
||||
// → [{name: "Mongolian", …}, …]
|
||||
```
|
||||
|
||||
### 5.7 使用map函数转换数组
|
||||
|
||||
假设我们已经通过某种方式对ancestry数组进行了过滤,生成一个用于表示人的信息的数组。但我们想创建一个包含人名的数组,因为这样更加易于查阅。
|
||||
假设我们已经通过某种方式过滤了`SCRIPTS`数组,生成一个用于表示脚本的信息数组。但我们想创建一个包含名称的数组,因为这样更加易于检查。
|
||||
|
||||
map方法可以对数组中的每个元素调用函数,然后利用返回值来构建一个新的数组,实现转换数组的操作。新建数组的长度与输入的数组一致,但其中的内容却通过对每个元素调用的函数“映射”成新的形式。
|
||||
|
||||
```
|
||||
function map(array, transform) {
|
||||
var mapped = [];
|
||||
for (var i = 0; i < array.length; i++)
|
||||
mapped.push(transform(array[i]));
|
||||
let mapped = [];
|
||||
for (let element of array) {
|
||||
mapped.push(transform(element));
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
var overNinety = ancestry.filter(function(person) {
|
||||
return person.died - person.born > 90;
|
||||
});
|
||||
console.log(map(overNinety, function(person) {
|
||||
return person.name;
|
||||
}));
|
||||
// → ["Clara Aernoudts", "Emile Haverbeke",
|
||||
// "Maria Haverbeke"]
|
||||
let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl");
|
||||
console.log(map(rtlScripts, s => s.name));
|
||||
// → ["Adlam", "Arabic", "Imperial Aramaic", …]
|
||||
```
|
||||
|
||||
有趣的是,活到至少90岁的人就是我们之前过滤出来的那三个人,即20世纪20年代的那几个年轻人,这几个人在我的数据集中恰好属于最年轻的一代。我想这应该得益于医疗水平的长足进步。
|
||||
|
||||
与forEach和filter一样,map也是数组中的一个标准方法。
|
||||
与forEach和filter一样,map也是标准的数组方法。
|
||||
|
||||
### 5.8 使用reduce进行数据汇总
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user