30 KiB
五、高阶函数
开发大型程序通常需要耗费大量财力和物力,这绝不仅仅是因为构建程序所花费时间的问题。大型程序的复杂程度总是很高,而这些复杂性也会给开发人员带来不少困扰,而程序错误或缺陷(bug)往往就是这些时候引入的。大型程序为这些缺陷提供了良好的藏身之所,因此我们更加难以在大型程序中找到它们。
让我们简单回顾一下前言当中的两个示例。其中第一个程序包含了6行代码并可以直接运行。
var total = 0, count = 1;
while (count <= 10) {
total += count;
count += 1;
}
console.log(total);
第二个程序则依赖于外部函数才能执行,且只有一行代码。
console.log(sum(range(1, 10)));
哪一个程序更有可能含有缺陷呢?
如果算上sum和range两个函数的代码量,显然第二个程序的代码量更大。不过,我仍然觉得第二个程序包含缺陷的可能性比第一个程序低。
之所以这么说的原因是,第二个程序编写的代码很好地表达了我们期望解决的问题。对于计算一组数字之和这个操作来说,我们关注的是计算范围和求和运算,而不是循环和计数。
sum和range这两个函数定义的操作当然会包含循环、计数和其他一些操作。但相比于将这些代码直接写到一起,这种表述方式更为简单,同时也易于避免错误。
5.1 抽象
在程序设计中,我们把这种编写代码的方式称为抽象。抽象可以隐藏底层的实现细节,从更高(或更加抽象)的层次看待我们要解决的问题。
举个例子,比较一下这两份豌豆汤的食谱:
按照每人一杯的量将脱水豌豆放入容器中。倒水直至浸没豌豆,然后至少将豌豆浸泡12个小时。将豌豆从水中取出沥干,倒入煮锅中,按照每人四杯水的量倒入水。将食材盖满整个锅底,并慢煮2个小时。按照每人半个的量加入洋葱,用刀切片,然后放入豌豆中。按照每人一根的量加入芹菜,用刀切片,然后放入豌豆当中。按照每人一根的量放入胡萝卜,用刀切片,然后放入豌豆中。最后一起煮10分钟以上即可。
第二份食谱:
一个人的量:一杯脱水豌豆、半个切好的洋葱、一根芹菜和一根胡萝卜。
将豌豆浸泡12个小时。按照每人四杯水的量倒入水,然后用文火煨2个小时。加入切片的蔬菜,煮10分钟以上即可。
相比第一份食谱,第二份食谱更简短且更易于理解。但你需要了解一些有关烹调的术语:浸泡、煨、切片,还有蔬菜。
在编程的时候,我们不能期望所有功能都是现成的。因此,你可能就会像第一份食谱那样编写你的程序,逐个编写计算机需要执行的代码和步骤,而忽略了这些步骤之上的抽象概念。
作为一名程序员,我们需要具备在恰当时候将代码抽象出来,形成一个新的函数或概念的能力。
5.2 数组遍历抽象
我们已经了解的普通函数就是一种很好的构建抽象的工具。但有些时候,光有函数也不一定能够解决我们的问题。
在前面的章节中,出现了好几次这样的for循环语句:
var array = [1, 2, 3];
for (var i = 0; i < array.length; i++) {
var current = array[i];
console.log(current);
}
这段代码的含义是,将数组中每个元素都打印到控制台中。但这段代码其实绕了个弯,它使用了一个用于计数的变量i,该变量用于检查数组的长度和提取当前遍历的元素。这样的编写方法不仅不够优雅,而且也为程序缺陷留下了可乘之机。我们有可能不小心复用了变量i、错误地将length拼写成length,或者混淆了变量i和current等。
那么,让我们来试试将这段代码抽象成一个函数。你会怎么做?
嗯,遍历整个数组并调用console.log来打印每个元素,倒是非常简单。
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));
}
}
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;
}
5.3 高阶函数
如果一个函数操作其他函数,即将其他函数作为参数或将函数作为返回值,那么我们可以将其称为高阶函数。如果你已经熟悉了函数就是一个普通的值的话,那么高阶函数也就不是什么稀奇的概念了。高阶这个术语来源于数学,在数学当中,函数和值的概念有着严格的区分。
我们可以使用高阶函数对一系列操作和值进行抽象。高阶函数有多种表现形式。比如你可以使用高阶函数来新建另一些函数。
function greaterThan(n) {
return function(m) { return m > n; };
}
var greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true
你也可以使用高阶函数来修改其他的函数。
function noisy(f) {
return function(arg) {
console.log("calling with", arg);
var val = f(arg);
console.log("called with", arg, "- got", val);
return val;
};
}
noisy(Boolean)(0);
// → calling with 0
// → called with 0 - got false
你甚至可以使用高阶函数来实现新的控制流。
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() {
console.log(n, "is even");
});
});
// → 0 is even
// → 2 is even
高阶函数的用法正是利用了我们在第3章中所讨论的词法作用域规则。在前面的示例当中,变量n是属于外部函数的参数,但由于内部函数可以访问外部的作用域,因此内部的函数也可以使用变量n。内部函数的函数体可以访问其外部环境的变量,这与循环及条件语句中的{}语句块有异曲同工之妙。其中内部函数与语句块之间的主要区别在于,在内部函数中声明的变量不会因为外部函数执行结束而丢失,这个特性十分有用。
5.4 参数传递
我们在前面定义的函数noisy会把参数传递给另一个函数使用,这会导致一个相当严重的问题。
function noisy(f) {
return function(arg) {
console.log("calling with", arg);
var val = f(arg);
console.log("called with", arg, "- got", val);
return val;
};
}
如果函数f接受多个参数,那么该函数只能接受第一个参数。我们可以为内部函数添加多个参数(arg1、arg2等),然后将这些参数传递给f,但问题在于noisy函数并不知道f函数需要多少参数。因为noisy函数只能传递固定数量的参数给f,因此也不能获取函数的arguments.length,函数f没有办法知道调用者传递给noisy的参数个数。
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/)的本章中下载一个文件,该文件中包含了一个ANCESTRY_FILE变量。该变量是一个包含JSON文件内容的字符串。让我们将字符串解码,并统计一下数据中包含的人数。
var ancestry = JSON.parse(ANCESTRY_FILE);
console.log(ancestry.length);
// → 39
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]);
}
return passed;
}
console.log(filter(ancestry, function(person) {
return person.born > 1900 && person.born < 1925;
}));
// → [{name: "Philibert Haverbeke", …}, …]
该函数使用test函数作为参数来实现过滤操作。我们对数组中的每个元素调用test函数,并通过其返回值来确定当前元素是否满足条件。
在该文件中,有三个人满足过滤条件:我的祖父、祖母和叔(或伯)祖母。
需要注意的是,filter函数并没有从当前数组中删除元素,而是新建了一个数组,并将满足条件的元素存入新建的数组中。这个函数是一个“纯函数”,因为该函数并未修改给定的数组。
与forEach一样,filter函数也是数组中提供的一个标准方法。本例中定义的函数只是用于展示内部实现原理。今后我们会使用以下方法来过滤数据:
console.log(ancestry.filter(function(person) {
return person.father == "Carel Haverbeke";
}));
// → [{name: "Carolus Haverbeke", …}]
5.7 使用map函数转换数组
假设我们已经通过某种方式对ancestry数组进行了过滤,生成一个用于表示人的信息的数组。但我们想创建一个包含人名的数组,因为这样更加易于查阅。
map方法可以对数组中的每个元素调用函数,然后利用返回值来构建一个新的数组,实现转换数组的操作。新建数组的长度与输入的数组一致,但其中的内容却通过对每个元素调用的函数“映射”成新的形式。
function map(array, transform) {
var mapped = [];
for (var i = 0; i < array.length; i++)
mapped.push(transform(array[i]));
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"]
有趣的是,活到至少90岁的人就是我们之前过滤出来的那三个人,即20世纪20年代的那几个年轻人,这几个人在我的数据集中恰好属于最年轻的一代。我想这应该得益于医疗水平的长足进步。
与forEach和filter一样,map也是数组中的一个标准方法。
5.8 使用reduce进行数据汇总
另外还有一种常用的数组计算操作,即根据整个数组计算出一个值。我们在前面讲到循环的时候,有个示例是对数字集合执行求和操作,这正是这类任务的一个实例。这里将给出另一个例子,查找数据集中出生最早的人。
我们将这种高阶操作称为reduce操作(或fold操作)。你可以把这个过程看成折叠数组的操作,每次操作一个元素。在计算所有数字之和时,先将结果初始化为0,然后遍历各个元素,并累加到当前的总和之中。
reduce函数包含三个参数:数组、执行合并操作的函数和初始值。该函数没有filter和map那样直观,因此使用时需多加小心。
function reduce(array, combine, start) {
var current = start;
for (var i = 0; i < array.length; i++)
current = combine(current, array[i]);
return current;
}
console.log(reduce([1, 2, 3, 4], function(a, b) {
return a + b;
}, 0));
// → 10
数组中有一个标准的reduce方法,当然和我们上面看到的那个函数一致,可以简化合并操作。如果你的数组中包含多个元素,在调用reduce方法的时候忽略了start参数,那么该方法将会使用数组中的第一个元素作为初始值,并从第二个元素开始执行合并操作。
要使用reduce方法来找出我的家族中最早出生的人,我们可以编写如下代码:
console.log(ancestry.reduce(function(min, cur) {
if (cur.born < min.born) return cur;
else return min;
}));
// → {name: "Pauwels van Haverbeke", born: 1535, …}
5.9 可组合性
考虑一下,我们怎样才可以在不使用高阶函数的情况下,实现以上示例(找出最早出生的人)当中相同的功能?代码看起来也还不错。
var min = ancestry[0];
for (var i = 1; i < ancestry.length; i++) {
var cur = ancestry[i];
if (cur.born < min.born)
min = cur;
}
console.log(min);
// → {name: "Pauwels van Haverbeke", born: 1535, …}
这段代码中多了一些变量,虽然多了两行代码,但代码逻辑还是很容易让人理解的。
当你遇到需要组合函数的情况时,高阶函数的价值就突显出来了。举个例子,我们编写一段代码,找出数据集中男人和女人的平均年龄。
function average(array) {
function plus(a, b) { return a + b; }
return array.reduce(plus) / array.length;
}
function age(p) { return p.died - p.born; }
function male(p) { return p.sex == "m"; }
function female(p) { return p.sex == "f"; }
console.log(average(ancestry.filter(male).map(age)));
// → 61.67
console.log(average(ancestry.filter(female).map(age)));
// → 54.56
(略显麻烦的是,我们必须定义一个plus函数,因为在JavaScript中运算符不能像函数那样作为参数值传递给函数。)
这段代码并没有将逻辑放到整个循环体中,而是将逻辑巧妙地组合成了我们所关注的几个方面:判断性别、计算年龄和计算平均数。我们可以依次使用这些函数来获得所需结果。
我们可以采用这种方式编写出逻辑清晰的代码。不过,编写这样的代码也是有代价的。
5.10 性能开销
在一片饱含优雅代码和漂亮彩虹的乐土上,住着一只名为“低效”的怪兽。
编写处理数组的程序时,最优雅的方式是将程序描述成清晰分离的步骤序列,每一步完成一些任务,并生成一个新数组。但构造这些中间数组却需要很高的性能开销。
同样地,将函数传递给forEach来处理数组迭代任务的确十分方便而且易于阅读。但JavaScript中函数调用却比简单的循环结构代价更高。
许多能够改善代码清晰程度的技术都会对性能产生影响。抽象在计算机操作与我们使用的概念之间增加了中间层,因此计算机在处理高层抽象的时候也要执行更多的代码。但并非所有技术都存在性能损耗的问题,有些编程语言提供了很好的构建抽象的支持,而且可以避免性能损耗的问题。甚至在JavaScript中,有经验的开发人员也可以编写出高性能的抽象代码。不过我们仍然会经常碰到性能问题。
幸运的是,绝大多数计算机的运行速度都非常快。如果你处理的数据集合中的数量适中,或者需要人为触发某项操作(比如每次用户点击一个按钮),那么你编写的是结构层次清晰但执行过程需要消耗0.5毫秒的代码,还是经过优化而且执行过程仅消耗0.1毫秒的代码,其实就不那么重要了。
粗略地记录一下你的程序的执行频率是很有帮助的。如果一个循环中嵌套了一个循环(无论是直接执行内部循环,还是在外部循环中调用函数,最终执行内部循环),最后会执行N×M次内部循环代码,其中N是外部循环次数,M是外部循环每次迭代时的内部循环次数。如果内部循环又嵌套了一层循环,循环次数为P次,那么最内层循环的循环体将会执行M×N×P次,以此类推。这些代码的运行次数可能最终会累计成很大的数字。当程序执行很慢的时候,问题往往只是嵌套最深的循环体中的一小部分代码引起的。
5.11 曾曾曾曾……祖父
我的祖父Philibert Haverbeke的信息也在数据文件中。我可以从他开始追溯我的家族中最早的祖先Pauwels van Haverbeke,他是我的直系祖先。如果他是的话,我想知道从理论上来说我与他有多少DNA是相同的。
为了能够根据祖辈的名字找出表示该人的实际对象,我们先要构建一个对象,将祖辈的姓名与表示人的对象关联起来。
var byName = {};
ancestry.forEach(function(person) {
byName[person.name] = person;
});
console.log(byName["Philibert Haverbeke"]);
// → {name: "Philibert Haverbeke", …}
现在的问题可比依据father属性来追溯Pauwels要复杂了许多。在家谱树中有许多人和他们的兄妹结婚(这在小村庄中非常普遍)。因此家谱树中的分支会在某些地方重新合并到家谱树中,这意味着我从Pauwels继承的基因数量超过了其基因数量的1/2G(其中G表示Pauwels和我相差几代)。该公式假定每一代都会将自己一半的基因遗传给下一代。
我们可以将这个问题与reduce方法进行类比,该方法会从左到右遍历数组中的元素,然后反复对元素进行合并,最终提炼出一个值。在本例中,我们也想从数据结构中提炼出一个值,只不过是根据家系进行遍历的。这里的数据集合更像是一个家族树,而非简单的列表。
我们希望通过合并一个人的祖先的基因比例得到该人的基因比例。我们可以使用递归来实现:如果想获得A这个人的基因组成比例,我们就必须计算出A的父母的基因组成比例,也就需要计算出A的爷爷奶奶的基因组成比例,依此类推。理论上这个计算过程会无限执行下去,不过我们的数据集是一定的,因此计算过程总会执行到某个地方停下来。归纳函数可以接受一个默认参数,如果我们需要计算某人的基因比例时该人不存在于数据中,我们将默认值作为该人的基因比例。我们的例子将默认值设置为0,即假设不在列表中的人不会从祖先遗传任何基因。
下面编写reduceAncestors函数,用于从家谱树中提炼出一个值。该函数的参数分别是需要计算的人、用于合并父母基因组成比例的函数以及默认值。
function reduceAncestors(person, f, defaultValue) {
function valueFor(person) {
if (person == null)
return defaultValue;
else
return f(person, valueFor(byName[person.mother]),
valueFor(byName[person.father]));
}
return valueFor(person);
}
内部函数(即valueFor)用于计算一个人的基因组成比例。借助于递归提供强大功能,该函数只需要调用自身来处理待计算者父亲的数据与母亲的数据即可。递归调用的计算结果连同待计算者对象都会作为参数传递给f,由f函数返回这个人的基因组成比例。
接下来我们就可以使用该函数来计算我的祖父遗传了Pauwels van Haverbeke的DNA比例,然后将计算结果除以4。
function sharedDNA(person, fromMother, fromFather) {
if (person.name == "Pauwels van Haverbeke")
return 1;
else
return (fromMother + fromFather) / 2;
}
var ph = byName["Philibert Haverbeke"];
console.log(reduceAncestors(ph, sharedDNA, 0) / 4);
// → 0.00049
很明显,Pauwels van Haverbeke这个人和Pauwels van Haverbeke共享了100%的基因(数据集中没有重名的人)。因此函数在遍历到他时直接返回了1。而其他所有人的相应基因组成比例是其父母相应数值之和的平均数。
那么从统计学角度来看,我从这个十六世纪的人身上遗传了大约0.05%的DNA比例。需要注意的是,这个计算结果只是统计学上的近似结果,并非确切的数字。这个数字很小,但考虑到人类携带的基因数量(大约30亿个碱基对)如此之庞大,因此在我的身上还是有可能找到一些来自Pauwels的特征。
我们也可以不使用reduceAncestors函数来计算一个人的基因比例。但是把特定的计算(计算遗传的DNA比例)抽象成通用的计算方法(从家谱树中提炼一个值)可以提高代码的清晰程度,我们也可以使用程序的抽象方法来计算其他内容。下面的代码用于找出满足特定条件的祖先比例,比如可以查找年龄超过70岁的人:
function countAncestors(person, test) {
function combine(current, fromMother, fromFather) {
var thisOneCounts = current != person && test(current);
return fromMother + fromFather + (thisOneCounts ? 1 : 0);
}
return reduceAncestors(person, combine, 0);
}
function longLivingPercentage(person) {
var all = countAncestors(person, function(person) {
return true;
});
var longLiving = countAncestors(person, function(person) {
return (person.died - person.born) >= 70;
});
return longLiving / all;
}
console.log(longLivingPercentage(byName["Emile Haverbeke"]));
// → 0.129
由于我们的数据集合当中所包含的人的信息比较随意,因此这些计算结果也就不那么准确了。但我们还是可以从这段代码看出,reduceAncestors函数可以满足各种有关家谱数据结构的计算需求。
5.12 绑定
每个函数都有一个bind方法,该方法可以用来创建新的函数,称为绑定函数。在调用新创建的函数时,就会调用原来的那个函数,只不过其中一些参数是预先就绑定好的。
下面的代码展示了bind的使用方法。代码中定义了isInSet函数,用于判断某个人是否在给定的字符串集合中。为了收集姓名在特定集合中的person对象,我们可以编写一个函数表达式,直接调用isInSet,并将特定集合作为第一个参数传递进去,也可以把isInSet函数的部分参数预先确定下来。
var theSet = ["Carel Haverbeke", "Maria van Brussel",
"Donald Duck"];
function isInSet(set, person) {
return set.indexOf(person.name) > -1;
}
console.log(ancestry.filter(function(person) {
return isInSet(theSet, person);
}));
// → [{name: "Maria van Brussel", …},
// {name: "Carel Haverbeke", …}]
console.log(ancestry.filter(isInSet.bind(null, theSet)));
// → … same result
调用bind会返回一个新的函数,该函数调用isInSet时会将theSet作为第一个参数,并将传递给该函数的剩余参数一起传递给isInSet。
与之前的apply一样,我们传递给bind的第一个参数是null,该参数是给方法调用使用的。我们会在下一章当中对这个问题进行详细的解释。
5.13 本章小结
将函数类型的值传递给其他函数不仅十分有用,而且还是JavaScript中一个重要的功能。我们可以在编写函数的时候把某些特定的操作预留出来,并在真正的函数调用中将具体的操作作为函数传递进来,实现完整的计算过程。
数组中提供了很多实用的高阶函数,其中forEach用于遍历数组元素,实现某些特定的功能。filter用于过滤掉一些元素,构造一个新数组。map会构建一个新数组,并通过一个函数处理每个元素,将处理结果放入新数组中。reduce则将数组元素最终归纳成一个值。
函数对象有一个apply方法,我们可以通过该方法调用函数,并使用数组来指定函数参数。另外还有一个bind方法,它用于创建一个新函数,并预先确定其中一部分参数。
5.14 习题
5.14.1 数组降维
结合使用reduce与concat方法,将输入的二维数组(数组的数组)中的元素提取出来,并存放到一个一维数组当中。
var arrays = [[1, 2, 3], [4, 5], [6]];
// Your code here.
// → [1, 2, 3, 4, 5, 6]
5.14.2 计算母子年龄差
使用本章中提供的数据集,计算母亲与孩子之间的平均年龄差(也就是孩子出生时母亲的年龄)。你可以使用本章前面定义的average函数。
需要注意的是,并非数据中所有的父元素都在数组中存在。这里byName对象可以帮助我们很容易地根据人名找出其对应的对象。
function average(array) {
function plus(a, b) { return a + b; }
return array.reduce(plus) / array.length;
}
var byName = {};
ancestry.forEach(function(person) {
byName[person.name] = person;
});
// Your code here.
// → 31.2
5.14.3 计算平均寿命
当我们查找数据集中寿命90岁以上的人时,结果只有数据中最年轻的一代。让我们深入分析一下这个问题。
计算并输出每个世纪祖先的平均寿命。每个人属于其死亡的那个世纪,即将每个人的去世年份除以100,并向上取整,如Math.ceil(person.died/100)。
作为额外练习,编写一个groupBy函数,对这类分组操作进行抽象。该函数接受两个参数,一个数组和一个处理数组中每个元素的函数,并返回一个对象,建立小组名称与小组编号之间的映射关系。
function average(array) {
function plus(a, b) { return a + b; }
return array.reduce(plus) / array.length;
}
// Your code here.
// → 16: 43.5
// 17: 51.2
// 18: 52.8
// 19: 54.8
// 20: 84.7
// 21: 94
5.14.4 使用every和some方法
数组中还有两个标准方法,分别是every和some。这两个方法均接受一个预测函数,以数组元素作为参数,并返回true或false。every函数很像&&运算符:&&运算符只有在两侧表达式均为true时,才返回true;而every函数只有对所有数组元素调用预测函数,返回值均为true时,才返回true。类似地,只要对任一元素调用预测函数返回true,some函数就返回true。
编写两个函数,every和some,除了需将待处理数组作为第一个参数外,其他行为都与上述方法一致。
// Your code here.
console.log(every([NaN, NaN, NaN], isNaN));
// → true
console.log(every([NaN, NaN, 4], isNaN));
// → false
console.log(some([NaN, 3, 4], isNaN));
// → true
console.log(some([2, 3, 4], isNaN));
// → false