diff --git a/5.md b/5.md index a7e2586..bce6c05 100644 --- a/5.md +++ b/5.md @@ -31,7 +31,7 @@ console.log(sum(range(1, 10))); sum和range这两个函数定义的操作当然会包含循环、计数和其他一些操作。但相比于将这些代码直接写到一起,这种表述方式更为简单,同时也易于避免错误。 -### 5.1 抽象 +## 抽象 在程序设计中,我们把这种编写代码的方式称为抽象。抽象可以隐藏底层的实现细节,从更高(或更加抽象)的层次看待我们要解决的问题。 @@ -51,7 +51,7 @@ sum和range这两个函数定义的操作当然会包含循环、计数和其他 在编程时,注意您的抽象级别什么时候过低,是一项非常有用的技能。 -### 5.2 重复的抽象 +## 重复的抽象 我们已经了解的普通函数就是一种很好的构建抽象的工具。但有些时候,光有函数也不一定能够解决我们的问题。 @@ -101,7 +101,7 @@ console.log(labels); 这个结构有点像`for`循环 - 它首先描述了这种循环,然后提供了一个主体。 但是,主体现在写为一个函数值,它被包裹在`repeat`调用的括号中。 这就是它必须用右小括号和右大括号闭合的原因。 在这个例子中,主体是单个小表达式,你也可以省略大括号并将循环写成单行。 -### 5.3 高阶函数 +## 高阶函数 如果一个函数操作其他函数,即将其他函数作为参数或将函数作为返回值,那么我们可以将其称为高阶函数。因为我们已经看到函数就是一个普通的值,那么高阶函数也就不是什么稀奇的概念了。高阶这个术语来源于数学,在数学当中,函数和值的概念有着严格的区分。 @@ -180,9 +180,9 @@ repeat(3, n => { 这样的对象会告诉您脚本的名称,分配给它的 Unicode 范围,书写方向,(近似)起始时间,是否仍在使用以及更多信息的链接。 方向可以是从左到右的`"ltr"`,从右到左的`"rtl"`(阿拉伯语和希伯来语文字的写法),或者从上到下的`"ttb"`(蒙古文的写法)。 -`ranges`属性包含 Unicode 字符范围数组,每个数组都有两元素,包含下限和上限。 这些范围内的任何字符代码都会分配给脚本。 下限是包括的(代码 994 是一个科普特字符),并且上限排除在外(代码 1008 不是)。 +`ranges`属性包含 Unicode 字符范围数组,每个数组都有两元素,包含下限和上限。 这些范围内的任何字符码都会分配给脚本。 下限是包括的(代码 994 是一个科普特字符),并且上限排除在外(代码 1008 不是)。 -### 5.6 数组过滤 +## 数组过滤 为了找到数据集中仍在使用的脚本,以下函数可能会有所帮助。 它过滤掉数组中未通过测试的元素: @@ -212,7 +212,7 @@ console.log(SCRIPTS.filter(s => s.direction == "ttb")); // → [{name: "Mongolian", …}, …] ``` -### 5.7 使用map函数转换数组 +## 使用map函数转换数组 假设我们已经通过某种方式过滤了`SCRIPTS`数组,生成一个用于表示脚本的信息数组。但我们想创建一个包含名称的数组,因为这样更加易于检查。 @@ -234,283 +234,263 @@ console.log(map(rtlScripts, s => s.name)); 与forEach和filter一样,map也是标准的数组方法。 -### 5.8 使用reduce进行数据汇总 +## 使用reduce进行数据汇总 -另外还有一种常用的数组计算操作,即根据整个数组计算出一个值。我们在前面讲到循环的时候,有个示例是对数字集合执行求和操作,这正是这类任务的一个实例。这里将给出另一个例子,查找数据集中出生最早的人。 +与数组有关的另一个常见事情是从它们中计算单个值。 我们的递归示例,汇总了一系列数字,就是这样一个例子。 另一个例子是找到字符最多的脚本。 -我们将这种高阶操作称为reduce操作(或fold操作)。你可以把这个过程看成折叠数组的操作,每次操作一个元素。在计算所有数字之和时,先将结果初始化为0,然后遍历各个元素,并累加到当前的总和之中。 +表示这种模式的高阶操作称为归约(reduce)(有时也称为折叠(fold))。 它通过反复从数组中获取单个元素,并将其与当前值合并来构建一个值。 在对数字进行求和时,首先从数字零开始,对于每个元素,将其与总和相加。 -reduce函数包含三个参数:数组、执行合并操作的函数和初始值。该函数没有filter和map那样直观,因此使用时需多加小心。 +reduce函数包含三个参数:数组、执行合并操作的函数和初始值。该函数没有filter和map那样直观,所以仔细看看: ``` function reduce(array, combine, start) { - var current = start; - for (var i = 0; i < array.length; i++) - current = combine(current, array[i]); + let current = start; + for (let element of array) { + current = combine(current, element); + } return current; } -console.log(reduce([1, 2, 3, 4], function(a, b) { - return a + b; -}, 0)); +console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0)); // → 10 ``` 数组中有一个标准的reduce方法,当然和我们上面看到的那个函数一致,可以简化合并操作。如果你的数组中包含多个元素,在调用reduce方法的时候忽略了start参数,那么该方法将会使用数组中的第一个元素作为初始值,并从第二个元素开始执行合并操作。 -要使用reduce方法来找出我的家族中最早出生的人,我们可以编写如下代码: +``` +console.log([1, 2, 3, 4].reduce((a, b) => a + b)); +// → 10 +``` + +为了使用`reduce`(两次)来查找字符最多的脚本,我们可以这样写: ``` -console.log(ancestry.reduce(function(min, cur) { - if (cur.born < min.born) return cur; - else return min; +function characterCount(script) { + return script.ranges.reduce((count, [from, to]) => { + return count + (to - from); + }, 0); +} + +console.log(SCRIPTS.reduce((a, b) => { + return characterCount(a) < characterCount(b) ? b : a; })); -// → {name: "Pauwels van Haverbeke", born: 1535, …} +// → {name: "Han", …} ``` -### 5.9 可组合性 +`characterCount`函数通过累加范围的大小,来减少分配给脚本的范围。 请注意归约器函数的参数列表中使用的解构。 `reduce'的第二次调用通过重复比较两个脚本并返回更大的脚本,使用它来查找最大的脚本。 + +Unicode 标准分配了超过 89,000 个字符给汉字脚本,它成为数据集中迄今为止最大的书写系统。 汉字是一种(有时)用于中文,日文和韩文的文字。 这些语言共享很多字符,尽管他们倾向于以不同的方式写它们。 (基于美国的)Unicode 联盟决定将它们看做一个单独的书写系统来保存字符码。 这被称为中日韩越统一表意文字(Han unification),并且仍然使一些人非常生气。 + +## 可组合性 考虑一下,我们怎样才可以在不使用高阶函数的情况下,实现以上示例(找出最早出生的人)当中相同的功能?代码看起来也还不错。 ``` -var min = ancestry[0]; -for (var i = 1; i < ancestry.length; i++) { - var cur = ancestry[i]; - if (cur.born < min.born) - min = cur; +let biggest = null; +for (let script of SCRIPTS) { + if (biggest == null || + characterCount(biggest) < characterCount(script)) { + biggest = script; + } } -console.log(min); -// → {name: "Pauwels van Haverbeke", born: 1535, …} +console.log(biggest); +// → {name: "Han", …} ``` -这段代码中多了一些变量,虽然多了两行代码,但代码逻辑还是很容易让人理解的。 +这段代码中多了一些绑定,虽然多了两行代码,但代码逻辑还是很容易让人理解的。 -当你遇到需要组合函数的情况时,高阶函数的价值就突显出来了。举个例子,我们编写一段代码,找出数据集中男人和女人的平均年龄。 +当你需要组合操作时,高阶函数的价值就突显出来了。举个例子,我们编写一段代码,找出数据集中男人和女人的平均年龄。 ``` function average(array) { - function plus(a, b) { return a + b; } - return array.reduce(plus) / array.length; + return array.reduce((a, b) => a + b) / 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 +console.log(Math.round(average( + SCRIPTS.filter(s => s.living).map(s => s.year)))); +// → 1185 +console.log(Math.round(average( + SCRIPTS.filter(s => !s.living).map(s => s.year)))); +// → 209 ``` -(略显麻烦的是,我们必须定义一个plus函数,因为在JavaScript中运算符不能像函数那样作为参数值传递给函数。) +因此,Unicode 中的死亡脚本,平均比活动脚本更老。 这不是一个非常有意义或令人惊讶的统计数据。 但是我希望你会同意,用于计算它的代码不难阅读。 你可以把它看作是一个流水线:我们从所有脚本开始,过滤出活动的(或死亡的)脚本,从这些脚本中抽出时间,对它们进行平均,然后对结果进行四舍五入。 -这段代码并没有将逻辑放到整个循环体中,而是将逻辑巧妙地组合成了我们所关注的几个方面:判断性别、计算年龄和计算平均数。我们可以依次使用这些函数来获得所需结果。 - -我们可以采用这种方式编写出逻辑清晰的代码。不过,编写这样的代码也是有代价的。 - -### 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])); +let total = 0, count = 0; +for (let script of SCRIPTS) { + if (script.living) { + total += script.year; + count += 1; } - return valueFor(person); } +console.log(Math.round(total / count)); +// → 1185 ``` -内部函数(即valueFor)用于计算一个人的基因组成比例。借助于递归提供强大功能,该函数只需要调用自身来处理待计算者父亲的数据与母亲的数据即可。递归调用的计算结果连同待计算者对象都会作为参数传递给f,由f函数返回这个人的基因组成比例。 +但很难看到正在计算什么以及如何计算。 而且由于中间结果并不表示为一致的值,因此将“平均值”之类的东西提取到单独的函数中,需要更多的工作。 -接下来我们就可以使用该函数来计算我的祖父遗传了Pauwels van Haverbeke的DNA比例,然后将计算结果除以4。 +就计算机实际在做什么而言,这两种方法也是完全不同的。 第一个在运行`filter`和`map`的时候会建立新的数组,而第二个只会计算一些数字,从而减少工作量。 你通常可以采用可读的方法,但是如果你正在处理巨大的数组,并且多次执行这些操作,那么抽象风格的加速就是值得的。 + +## 字符串和字符码 + +这个数据集的一种用途是确定一段文本所使用的脚本。 我们来看看执行它的程序。 + +请记住,每个脚本都有一组与其相关的字符码范围。 所以给定一个字符码,我们可以使用这样的函数来找到相应的脚本(如果有的话): ``` -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); +function characterScript(code) { + for (let script of SCRIPTS) { + if (script.ranges.some(([from, to]) => { + return code >= from && code < to; + })) { + return script; + } } - 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; + return null; } -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 +console.log(characterScript(121)); +// → {name: "Latin", …} ``` -调用bind会返回一个新的函数,该函数调用isInSet时会将theSet作为第一个参数,并将传递给该函数的剩余参数一起传递给isInSet。 +`some`方法是另一个高阶函数。 它需要一个测试函数,并告诉你该函数是否对数组中的任何元素返回`true`。 -与之前的apply一样,我们传递给bind的第一个参数是null,该参数是给方法调用使用的。我们会在下一章当中对这个问题进行详细的解释。 +但是,我们如何获得字符串中的字符码? -### 5.13 本章小结 +在第一章中,我提到 JavaScript 字符串被编码为一个 16 位数字的序列。 这些被称为代码单元。 一个 Unicode 字符代码最初应该能放进这样一个单元(它给你超 65,000 个字符)。 后来人们发现它不够用了,很多人避开了为每个字符使用更多内存的需求。 为了解决这些问题,人们发明了 UTF-16,JavaScript 字符串使用的格式 。它使用单个 16 位代码单元描述了大多数常见字符,但是为其他字符使用一对两个这样的单元。 -将函数类型的值传递给其他函数不仅十分有用,而且还是JavaScript中一个重要的功能。我们可以在编写函数的时候把某些特定的操作预留出来,并在真正的函数调用中将具体的操作作为函数传递进来,实现完整的计算过程。 - -数组中提供了很多实用的高阶函数,其中forEach用于遍历数组元素,实现某些特定的功能。filter用于过滤掉一些元素,构造一个新数组。map会构建一个新数组,并通过一个函数处理每个元素,将处理结果放入新数组中。reduce则将数组元素最终归纳成一个值。 - -函数对象有一个apply方法,我们可以通过该方法调用函数,并使用数组来指定函数参数。另外还有一个bind方法,它用于创建一个新函数,并预先确定其中一部分参数。 - -### 5.14 习题 - -#### 5.14.1 数组降维 - -结合使用reduce与concat方法,将输入的二维数组(数组的数组)中的元素提取出来,并存放到一个一维数组当中。 +今天 UTF-16 通常被认为是一个糟糕的主意。 它似乎总是故意设计来引起错误。 很容易编写程序,假装代码单元和字符是一个东西。 如果你的语言不使用两个单位的字符,显然能正常工作。 但只要有人试图用一些不太常见的中文字符来使用这样的程序,就会中断。 幸运的是,随着 emoji 符号的出现,每个人都开始使用两个单元的字符,处理这些问题的负担更加分散。 ``` -var arrays = [[1, 2, 3], [4, 5], [6]]; +// Two emoji characters, horse and shoe +let horseShoe = "\ud83d\udc34\ud83d\udc5f"; +console.log(horseShoe.length); +// → 4 +console.log(horseShoe[0]); +// → (Invalid half-character) +console.log(horseShoe.charCodeAt(0)); +// → 55357 (Code of the half-character) +console.log(horseShoe.codePointAt(0)); +// → 128052 (Actual code for horse emoji) +``` + +JavaScript的`charCodeAt`方法为您提供了一个代码单元,而不是一个完整的字符代码。 稍后添加的`codePointAt`方法确实提供了完整的 Unicode 字符。 所以我们可以使用它从字符串中获取字符。 但传递给`codePointAt`的参数仍然是代码单元序列的索引。 因此,要运行字符串中的所有字符,我们仍然需要处理一个字符占用一个还是两个代码单元的问题。 + +在上一章中,我提到`for/of`循环也可以用在字符串上。 像`codePointAt`一样,这种类型的循环,是在人们敏锐地意识到 UTF-16 的问题的时候引入的。 当你用它来遍历一个字符串时,它会给你真正的字符,而不是代码单元。 + +``` +let roseDragon = "\ud83c\udf45\ud83d\udc09"; +for (let char of roseDragon) { + console.log(char); +// → (emoji rose) +// → (emoji dragon) +``` + +如果你有一个字符(它是一个或两个代码单元的字符串),你可以使用`codePointAt(0)`来获得它的代码。 + +## 识别文本 + +我们有了`characterScript`函数和一种正确遍历字符的方法。 下一步将是计算属于每个脚本的字符。 下面的计数抽象会很实用: + +``` +function countBy(items, groupName) { + let counts = []; + for (let item of items) { + let name = groupName(item); + let known = counts.findIndex(c => c.name == name); + if (known == -1) { + counts.push({name, count: 1}); + } else { + counts[known].count++; + } + } + return counts; +} + +console.log(countBy([1, 2, 3, 4, 5], n => n > 2)); +// → [{name: false, count: 2}, {name: true, count: 3}] +``` + +`countBy`函数需要一个集合(我们可以用`for/of`来遍历的任何东西)以及一个函数,它计算给定元素的组名。 它返回一个对象数组,每个对象命名一个组,并告诉你该组中找到的元素数量。 + +它使用另一个数组方法`findIndex`。 这个方法有点像`indexOf`,但它不是查找特定的值,而是查找给定函数返回`true`的第一个值。 像`indexOf`一样,当没有找到这样的元素时,它返回 -1。 + +使用`countBy`,我们可以编写一个函数,告诉我们在一段文本中使用了哪些脚本。 + +``` +function textScripts(text) { + let scripts = countBy(text, char => { + let script = characterScript(char.codePointAt(0)); + return script ? script.name : "none"; + }).filter(({name}) => name != "none"); + + let total = scripts.reduce((n, {count}) => n + count, 0); + if (total == 0) return "No scripts found"; + + return scripts.map(({name, count}) => { + return `${Math.round(count * 100 / total)}% ${name}`; + }).join(", "); +} + +console.log(textScripts('英国的狗说"woof", 俄罗斯的狗说"тяв"')); +// → 61% Han, 22% Latin, 17% Cyrillic +``` + +该函数首先按名称对字符进行计数,使用`characterScript`为它们分配一个名称,并且对于不属于任何脚本的字符,回退到字符串`"none"`。 `filter`调用从结果数组中删除`"none"`的条目,因为我们对这些字符不感兴趣。 + +为了能够计算百分比,我们首先需要属于脚本的字符总数,我们可以用`reduce`来计算。 如果没有找到这样的字符,该函数将返回一个特定的字符串。 否则,它使用`map`将计数条目转换为可读的字符串,然后使用`join`合并它们。 + +## 本章小结 + +能够将函数值传递给其他函数,是 JavaScript 的一个非常有用的方面。 它允许我们编写函数,用它们中的“间隙”对计算建模。 调用这些函数的代码,可以通过提供函数值来填补间隙。 + +数组提供了许多有用的高阶方法。 你可以使用`forEach`来遍历数组中的元素。 `filter`方法返回一个新数组,只包含通过谓词函数的元素。 通过将函数应用于每个元素的数组转换,使用`map`来完成。 您可以使用`reduce`将数组中的所有元素合并为一个值。 `some`方法测试任何元素是否匹配给定的谓词函数。 `findIndex`找到匹配谓词的第一个元素的位置。 + +## 习题 + +### 展开 + +联合使用`reduce`方法和`concat`方法,将一个数组的数组“展开”成一个单个数组,包含原始数组的所有元素。 + +``` +let arrays = [[1, 2, 3], [4, 5], [6]]; // Your code here. // → [1, 2, 3, 4, 5, 6] ``` -#### 5.14.2 计算母子年龄差 +### 你自己的循环 -使用本章中提供的数据集,计算母亲与孩子之间的平均年龄差(也就是孩子出生时母亲的年龄)。你可以使用本章前面定义的average函数。 +编写一个高阶函数`loop`,提供类似`for`循环语句的东西。 它接受一个值,一个测试函数,一个更新函数和一个主体函数。 每次迭代中,它首先在当前循环值上运行测试函数,并在返回`false`时停止。 然后它调用主体函数,向其提供当前值。 最后,它调用`update`函数来创建一个新的值,并从头开始。 -需要注意的是,并非数据中所有的父元素都在数组中存在。这里byName对象可以帮助我们很容易地根据人名找出其对应的对象。 +定义函数时,可以使用常规循环来执行实际循环。 ``` -function average(array) { - function plus(a, b) { return a + b; } - return array.reduce(plus) / array.length; +// Your code here. + +loop(3, n => n > 0, n => n - 1, console.log); +// → 3 +// → 2 +// → 1 +``` + +### `every` + +类似于`some`方法,数组也有`every`方法。 当给定函数对数组中的每个元素返回`true`时,此函数返回`true`。 在某种程度上,`some`是作用于数组的`||`运算符的一个版本,`every`就像`&&`运算符。 + +将`every`实现为一个函数,接受一个数组和一个谓词函数作为参数。编写两个版本,一个使用循环,另一个使用`some`方法。 + +``` +function every(array, test) { + // Your code here. } -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)); +console.log(every([1, 3, 5], n => n < 10)); // → true -console.log(every([NaN, NaN, 4], isNaN)); +console.log(every([2, 4, 16], n => n < 10)); // → false -console.log(some([NaN, 3, 4], isNaN)); +console.log(every([], n => n < 10)); // → true -console.log(some([2, 3, 4], isNaN)); -// → false ```