抽象

1
2
3
4
5
6
7
8
9
10
let total = 0, count = 1;
while (count <= 10) {
total += count;
count += 1;
}
console.log(total);

//Compared With the Following One

console.log(sum(range(1, 10)));
  • 在程序设计中,我们把这种编写代码的方式称为抽象。抽象可以隐藏底层的实现细节,从更高(或更加抽象)的层次看待我们要解决的问题。

  • 我们可以使用函数来定义我们想做的事,而函数也是值,因此我们可以将期望执行的操作封装成函数,然后传递进来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function 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
    6
    let 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
      6
      function greaterThan(n) {
      return m => m > n;
      }
      let greaterThan10 = greaterThan(10);
      console.log(greaterThan10(11));
      // → true
    • 后来理解,return的是一个函数,等同于
      1
      2
      3
      return (function(m){
      return m>n;
      })
  • 可以使用高阶函数来修改其他的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function 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
    // → B
  • map方法对数组中的每个元素调用函数,然后利用返回值来构建一个新的数组,实现转换数组的操作。新建数组的长度与输入的数组一致,但其中的内容却通过对每个元素调用的函数“映射”成新的形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function 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
    10
    function 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
    10
    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: "Han", …}

可组合性

  • 当你需要组合操作时,高阶函数的价值就突显出来了。举个例子,我们编写一段代码,找出数据集中男人和女人的平均年龄。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function 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
    9
    let 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。。