学习Javascript-高阶函数
抽象
1 | let total = 0, count = 1; |
在程序设计中,我们把这种编写代码的方式称为抽象。抽象可以隐藏底层的实现细节,从更高(或更加抽象)的层次看待我们要解决的问题。
我们可以使用函数来定义我们想做的事,而函数也是值,因此我们可以将期望执行的操作封装成函数,然后传递进来。
1
2
3
4
5
6
7
8
9
10function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, console.log);
// → 0
// → 1
// → 2你不必将预定义的函数传递给repeat。 通常情况下,你希望原地创建一个函数值。
1
2
3
4
5
6let labels = [];
repeat(5, i => {
labels.push(`Unit ${i + 1}`);
});
console.log(labels);
// → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]脚本数据集
第 1 章中的 Unicode,该系统为书面语言中的每个字符分配一个数字。 大多数这些字符都与特定的脚本相关联。 该标准包含 140 个不同的脚本 - 81 个今天仍在使用,59 个是历史性的。
本章的编码沙箱中提供了SCRIPTS绑定。 该绑定包含一组对象,其中每个对象都描述了一个脚本。
1
2
3
4
5
6
7
8{
name: "Coptic",
ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
direction: "ltr",
year: -200,
living: false,
link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}ranges属性包含 Unicode 字符范围数组,每个数组都有两元素,包含下限和上限。 这些范围内的任何字符码都会分配给脚本。 下限是包括的(代码 994 是一个科普特字符),并且上限排除在外(代码 1008 不是)。
高阶函数
如果一个函数操作其他函数,即将其他函数作为参数或将函数作为返回值,那么我们可以将其称为高阶函数。
疑问
- 怎么传参的
1
2
3
4
5
6function greaterThan(n) {
return m => m > n;
}
let greaterThan10 = greaterThan(10);
console.log(greaterThan10(11));
// → true - 后来理解,return的是一个函数,等同于
1
2
3return (function(m){
return m>n;
})
- 怎么传参的
可以使用高阶函数来修改其他的函数
1
2
3
4
5
6
7
8
9
10
11function noisy(f) {
return (...args) => {
console.log("calling with", args);
let result = f(...args);
console.log("called with", args, ", returned", result);
return result;
};
}
noisy(Math.min)(3, 2, 1);
// → calling with [3, 2, 1]
// → called with [3, 2, 1] , returned 1有一个内置的数组方法,forEach,它提供了类似for/of循环的东西,作为一个高阶函数。
1
2
3["A", "B"].forEach(l => console.log(l));
// → A
// → Bmap方法对数组中的每个元素调用函数,然后利用返回值来构建一个新的数组,实现转换数组的操作。新建数组的长度与输入的数组一致,但其中的内容却通过对每个元素调用的函数“映射”成新的形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function filter(array, test) {
let passed = [];
for (let element of array) {
if (test(element)) {
passed.push(element);
}
}
return passed;
}
console.log(filter(SCRIPTS, script => script.living));
// → [{name: "Adlam", …}, …]
function map(array, transform) {
let mapped = [];
for (let element of array) {
mapped.push(transform(element));
}
return mapped;
}
let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl"); //稍微有些难理解
console.log(map(rtlScripts, s => s.name));
// → ["Adlam", "Arabic", "Imperial Aramaic", …]使用
reduce
汇总数据与数组有关的另一个常见事情是从它们中计算单个值。表示这种模式的高阶操作称为归约(reduce)(有时也称为折叠(fold))。 它通过反复从数组中获取单个元素,并将其与当前值合并来构建一个值。
reduce函数包含三个参数:数组、执行合并操作的函数和初始值。该函数没有filter和map那样直观,所以仔细看看:
1
2
3
4
5
6
7
8
9
10function reduce(array, combine, start) {
let current = start; //初始值,类似于i = 0;
for (let element of array) {
current = combine(current, element); //处理函数
}
return current; //返回结果
}
console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));
// → 10数组中有一个标准的reduce方法,当然和我们上面看到的那个函数一致,可以简化合并操作。如果你的数组中包含多个元素,在调用reduce方法的时候忽略了start参数,那么
该方法将会使用数组中的第一个元素作为初始值,并从第二个元素开始执行合并操作
。为了使用reduce(两次)来查找字符最多的脚本,我们可以这样写:(看懂了一些)
characterCount函数通过累加范围的大小,来减少分配给脚本的范围。 请注意归约器函数的参数列表中使用的解构。 `reduce’的第二次调用通过重复比较两个脚本并返回更大的脚本,使用它来查找最大的脚本。也就是说characterCount函数的reduce是为了统计一段字符的字符数,而SCRIPTS.reduce就像是循环一样,反复比较,每次拿出选出一个最大的进行下一轮的对比。最后得出字符数最多的元素
1
2
3
4
5
6
7
8
9
10function 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: "Han", …}
可组合性
- 当你需要组合操作时,高阶函数的价值就突显出来了。举个例子,我们编写一段代码,找出数据集中男人和女人的平均年龄。
1
2
3
4
5
6
7
8
9
10function average(array) {
return array.reduce((a, b) => a + b) / array.length;
}
//疑问:Filter是不是就是一个reduce?
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 - 你当然也可以把这个计算写成一个大循环。
1
2
3
4
5
6
7
8
9let total = 0, count = 0;
for (let script of SCRIPTS) {
if (script.living) {
total += script.year;
count += 1;
}
}
console.log(Math.round(total / count));
// → 1185但很难看到正在计算什么以及如何计算。 而且由于中间结果并不表示为一致的值,因此将“平均值”之类的东西提取到单独的函数中,需要更多的工作。
就计算机实际在做什么而言,这两种方法也是完全不同的。 第一个在运行filter和map的时候会建立新的数组,而第二个只会计算一些数字,从而减少工作量。 你通常可以采用可读的方法,但是如果你正在处理巨大的数组,并且多次执行这些操作,那么抽象风格的加速就是值得的。
本章小结
能够将函数值传递给其他函数,是 JavaScript 的一个非常有用的方面。 它允许我们编写函数,用它们中的“间隙”对计算建模。 调用这些函数的代码,可以通过提供函数值来填补间隙。
数组提供了许多有用的高阶方法。 你可以使用forEach来遍历数组中的元素。 filter方法返回一个新数组,只包含通过谓词函数的元素。 通过将函数应用于每个元素的数组转换,使用map来完成。 你可以使用reduce将数组中的所有元素合并为一个值。 some方法测试任何元素是否匹配给定的谓词函数。 findIndex找到匹配谓词的第一个元素的位置, 这个方法有点像indexOf,但它不是查找特定的值,而是查找给定函数返回true的第一个值。 像indexOf一样,当没有找到这样的元素时,它返回 -1。。