26.数组

26.1. JavaScript 中数组的两个角色

数组JavaScript 中扮演两个角色:

  • 元组:Arrays-as-tuples 具有固定数量的索引元素。这些元素中的每一个都可以具有不同的类型。
  • 序列:Arrays-as-sequences 具有可变数量的索引元素。每个元素都具有相同的类型。

实际上,数组一般都由这两个角色组成。值得注意的是,Arrays-as-sequences 非常灵活,您可以将它们用作(传统)数组,堆栈和队列(请参阅本章末尾的练习)。

26.2. 数组基本操作

26.2.1. 数组:创建,读取,更改

  • 创建数组的最佳方法是使用 数组字面量
    const arr = ['a', 'b', 'c'];
    
  • 数组(Array)字面量以方括号[]开头和结尾。它创建了一个包含三个 元素 的数组:'a''b''c'
  • 要读取数组(Array)元素,请在方括号中填入索引(索引从零开始):
    assert.equal(arr[0], 'a');
    
  • 要更改数组(Array)元素,可以使用对应索引修改其值:
    arr[0] = 'x';
    assert.deepEqual(arr, ['x', 'b', 'c']);
    
  • 数组索引的范围是 32 位(不包括最大长度):$ [0,2 ^32^ -1)$

26.2.2. 数组:.length

  • 每个数组(Array)都有一个属性.length,可用于读取和更改(!)数组中元素的数量。
  • 数组的长度始终是最高的索引加一:
    > const arr = ['a', 'b'];
    > arr.length
    2
    
  • 如果使用长度作为索引值写入数组,则会追加一个元素:
    > arr[arr.length] = 'c';
    > arr
    [ 'a', 'b', 'c' ]
    > arr.length
    3
    
  • (破坏性地)附加元素的另一种方法是通过数组(Array)方法.push()
    > arr.push('d');
    > arr
    [ 'a', 'b', 'c', 'd' ]
    
  • 如果设置.length,则会通过删除元素来设置数组长度:
    > arr.length = 1;
    > arr
    [ 'a' ]
    

练习:通过.push() 删除空行

exercises/arrays/remove_empty_lines_push_test.js

26.2.3. 数组字面量展开

  • 在数组(Array)字面量中,扩展运算符 由三个点(...)组成,后跟一个表达式。它导致表达式重新计算迭代,最后形成一个新的数组。例如:
    > const iterable = ['b', 'c'];
    > ['a', ...iterable, 'd']
    [ 'a', 'b', 'c', 'd' ]
    
  • 扩展运算符可以很方便地将两个数组合并起来:
    const arr1 = ['a', 'b'];
    const arr2 = ['c', 'd'];
    const concatenated = [...arr1, ...arr2, 'e'];
    assert.deepEqual(
      concatenated,
      ['a', 'b', 'c', 'd', 'e']);
    

26.2.4. 数组:列出索引和属性

  • 方法.keys()列出数组的索引:
    const arr = ['a', 'b'];
    assert.deepEqual(
      [...arr.keys()], // (A)
      [0, 1]);
    
  • .keys()返回一个可迭代的。在 A 行,我们通过扩展运算符生成了一个数组。
  • 列表数组索引与列表属性不同。当你执行后者时,你得到索引 - 但作为字符串 - 加上非索引属性键:
    const arr = ['a', 'b'];
    arr.prop = true;
    assert.deepEqual(
      Object.keys(arr),
      ['0', '1', 'prop']);
    
  • 方法.entries()将数组的内容列为[index,element]对:
    const arr = ['a', 'b'];
    assert.deepEqual(
      [...arr.entries()],
      [[0, 'a'], [1, 'b']]);
    

26.2.5. 数值是一个数组吗?

  • 这两种方法可以检查值是否为数组:
    > [] instanceof Array
    true
    > Array.isArray([])
    true
    
  • instanceof通常很好。如果某个值可能来自另一个 ,则需要Array.isArray()。粗略地说,领域是 JavaScript 全局范围的一个实例。一些领域彼此隔离(例如浏览器中的 Web Workers ),但也有一些领域可以移动数据(例如浏览器中的同源 iframe)。 x instanceof Array检查x的原型链,因此如果x是来自另一个域的数组,则返回false
  • typeof将数组归类为对象:
    > typeof []
    'object'
    

26.3. for-of和数组

我们已经遇到了for-of循环。本节简要介绍如何将它用于数组。

26.3.1。 for-of:迭代元素

  • 以下for-of循环遍历数组的元素。
    for (const element of ['a', 'b']) {
      console.log(element);
    }
    // Output:
    // 'a'
    // 'b'
    

26.3.2. for-of:迭代[index,element]对

  • 以下for-of循环遍历[index,element]对。解构(稍后描述)为我们提供了在for-of的头部设置indexelement的便捷语法。
    for (const [index, element] of ['a', 'b'].entries()) {
      console.log(index, element);
    }
    // Output:
    // 0, 'a'
    // 1, 'b'
    

26.4. 类数组对象

  • 一些使用 Arrays 的操作只需要最小值:值必须只是 类似于数组。类数组是具有以下属性的对象:
    • .length:保存类似 Array 的对象的长度。
    • ['0']:将元素保持在索引 0 处。(等等)
  • ArrayLike的 TypeScript 接口如下所示。
    interface ArrayLike<T> {
      length: number;
      [n: number]: T;
    }
    
  • Array.from()接受类似 Array 的对象并将它们转换为 Arrays:
    // If you omit .length, it is interpreted as 0
    assert.deepEqual(
      Array.from({}),
      []);
    assert.deepEqual(
      Array.from({length:2, 0:'a', 1:'b'}),
      [ 'a', 'b' ]);
    
  • 类似于数组的对象曾经在 ES6 之前很常见;现在你不经常看到它们。

26.5. 将可迭代和类似数组的值转换为数组

将可迭代和类似数组的值转换为数组有两种常用方法:扩展和Array.from()

26.5.1. 通过扩展运算符(...)将迭代转换为数组

  • 在 Array 字面量中,通过 ... 将任何可迭代对象转换为一系列 Array 元素。例如:
    // Get an Array-like collection from a web browser’s DOM
    const domCollection = document.querySelectorAll('a');
    // Alas, the collection is missing many Array methods
    assert.equal('map' in domCollection, false);
    // Solution: convert it to an Array
    const arr = [...domCollection];
    assert.deepEqual(
      arr.map(x => x.href),
      ['http://2ality.com', 'http://exploringjs.com']);
    
  • 转换有效,因为 DOM 集合是可迭代的。

26.5.2. 通过Array.from()将可迭代和类似数组的对象转换为数组(高级)

Array.from()可以使用两种模式。

26.5.2.1. Array.from()的模式 1:转换
  • 第一种模式具有以下类型签名:
    .from<T>(iterable: Iterable<T> | ArrayLike<T>): T[];
    
  • 接口Iterable在介绍迭代的章节。本章前面出现[HTD2]接口ArrayLike
  • 使用单个参数,Array.from()将任何可迭代或类似 Array 的数据转换为数组:
    > Array.from(new Set(['a', 'b']))
    [ 'a', 'b' ]
    > Array.from({length: 2, 0:'a', 1:'b'})
    [ 'a', 'b' ]
    
26.5.2.2. Array.from()的模式 2:转换和映射
  • Array.from()的第二种模式涉及两个参数:
    .from<T, U>(
      iterable: Iterable<T> | ArrayLike<T>,
      mapFunc: (v: T, i: number) => U,
      thisArg?: any)
      : U[];复制ErrorOK!
    
  • 在这种模式下,Array.from()做了几件事:
    • 它迭代iterable
    • 它将mapFunc应用于每个迭代值。
    • 它将结果收集到一个新数组中并返回它。
  • 可选参数thisArg指定mapFuncthis
  • 这意味着我们将从具有T类型的元素的 iterable 转变为具有U类型的元素的 Array。
  • 这是一个例子:
    > Array.from(new Set(['a', 'b']), x => x + x)
    [ 'aa', 'bb' ]
    

26.6. 创建和填充任意长度的数组

创建数组的最佳方法是通过数组字面量。但是,您不能总是使用一个:数组可能太大,您可能在开发过程中不知道它的长度,或者您可能希望保持其长度灵活。然后我推荐以下用于创建和填充数组的技术:

  • 你需要创建一个你将完全填充的空数组,然后呢?
    > new Array(3)
    [ , , ,]
    
    请注意,结果有 3 个孔 - 数组字面量中的最后一个逗号始终被忽略。
  • 你需要创建一个用原始值初始化的数组吗?
    > new Array(3).fill(0)
    [0, 0, 0]
    
    警告:如果对对象使用.fill(),则每个 Array 元素将引用同一个对象。
  • 你需要创建一个用对象初始化的数组吗?
    > Array.from({length: 3}, () => ({}))
    [{}, {}, {}]
    
  • 你需要创建一系列整数吗?
    const START = 2;
    const END = 5;
    assert.deepEqual(
      Array.from({length: END-START}, (x, i) => i+START),
      [2, 3, 4]);
    

如果您正在处理整数或浮点数的数组,请考虑类型数组 - 这是为此目的而创建的。

26.7. 多维数组

JavaScript 没有真正的多维数组;你需要求助于其元素为数组的数组:

const DIM_X = 4;
const DIM_Y = 3;
const DIM_Z = 2;
const arr = [];
for (let x=0; x<DIM_X; x++) {
  arr[x] = []; // (A)
  for (let y=0; y<DIM_Y; y++) {
    arr[x][y] = []; // (B)
    for (let z=0; z<DIM_Z; z++) {
      arr[x][y][z] = 0; // (C)
    }
  }
}
arr[3][0][1] = 7;
assert.deepEqual(arr, [
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
  [ [ 0, 7 ], [ 0, 0 ], [ 0, 0 ] ],
]);复制ErrorOK!

观察:

  • 我们通过为索引为当前长度的插槽分配值来增长数组。
  • 每个维度 - 除了最后一个维度 - 是一个数组,其元素是下一个维度(行 A,行 B)。
  • 最后一个维度包含实际值(第 C 行)。

26.8, 更多数组功能(高级)

在本节中,我们将介绍在使用 Arrays 时经常遇到的现象。

26.8.1. 数组元素是(稍微特殊)属性

  • 您认为 Array 元素是特殊的,因为您通过数字访问它们。但是这样做的方括号运算符([ ])与用于访问属性的运算符相同。并且,根据语言规范,它将任何值(不是符号)强制转换为字符串。因此:数组元素是(几乎)正常属性(A 行),如果使用数字或字符串作为索引(行 B 和 C),则无关紧要:
    const arr = ['a', 'b'];
    arr.prop = 123;
    assert.deepEqual(
      Object.keys(arr),
      ['0', '1', 'prop']); // (A)
    assert.equal(arr[0], 'a');  // (B)
    assert.equal(arr['0'], 'a'); // (C)
    
  • 更令人困惑的是,这只是语言规范如何定义事物(JavaScript 的理论,如果你愿意的话)。大多数 JavaScript 引擎都经过优化,并且使用数字(甚至是整数)来访问 Array 元素(如果你愿意,可以使用 JavaScript 的实践)。
  • 用于 Array 元素的属性键(字符串!)称为 索引。字符串str是将其转换为 32 位无符号整数并返回后生成原始值的索引。写成公式:
    ToString(ToUint32(key)) === key
    
  • JavaScript 在列出所有对象的属性键时特别对待索引!它们总是排在第一位并按数字排序,而不是按字典顺序排列('10'将在'2'之前出现):
    const arr = 'abcdefghijk'.split('');
    arr.prop = 123;
    assert.deepEqual(
      Object.keys(arr),
      ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'prop']);
    
  • 请注意,.length.entries().keys()将数组索引视为数字并忽略非索引属性:
    const arr = ['a', 'b'];
    arr.prop = true;
    assert.deepEqual(
      Object.keys(arr), ['0', '1', 'prop']);
    assert.equal(arr.length, 2);
    assert.deepEqual(
      [...arr.keys()], [0, 1]);
    assert.deepEqual(
      [...arr.entries()], [[0, 'a'], [1, 'b']]);
    
  • 我们使用扩展元素(...)将.keys().entries()返回的迭代转换为数组。

26.8.2. 数组是字典,可以有空元素

  • JavaScript 支持两种数组:
    • 密集数组:是数组形成连续序列的数组。这是迄今为止我们见过的唯一一种数组。
    • 稀疏数组:包含空元素的数组。也就是说,一些指数缺失。
  • 一般来说,最好避免漏洞,因为它们会使代码更复杂,并且不会被 Array 方法一致地处理。此外,JavaScript 引擎优化密集数组,因此它们更快。
  • 您可以在分配元素时通过跳过索引来创建空数组元素:
    const arr = [];
    arr[0] = 'a';
    arr[2] = 'c';
    assert.deepEqual(Object.keys(arr), ['0', '2']); // (A)
    assert.equal(0 in arr, true); // element
    assert.equal(1 in arr, false); // hole
    
  • 在 A 行中,我们使用Object.keys(),因为arr.keys()将空元素视为undefined元素并且不会显示它们。
  • 另一种创建漏洞的方法是跳过数组字面量中的元素:
    const arr = ['a', , 'c'];
    assert.deepEqual(Object.keys(arr), ['0', '2']);
    
  • 你可以删除数组元素:
    const arr = ['a', 'b', 'c'];
    assert.deepEqual(Object.keys(arr), ['0', '1', '2']);
    delete arr[1];
    assert.deepEqual(Object.keys(arr), ['0', '2']);
    

有关 JavaScript 如何处理数组中的漏洞的更多信息,请参阅 “探索 ES6”

26.9. 添加和删除元素(破坏性和非破坏性)

JavaScript 的Array非常灵活,更像是数组,堆栈和队列的组合。本节探讨添加和删除 Array 元素的方法。大多数操作可以破坏性地(修改数组)和非破坏性地(生成修改的副本)执行。

26.9.1. 预先添加元素和数组

  • 在下面的代码中,我们破坏性地将单个元素添加到arr1,将数组添加到arr2
    const arr1 = ['a', 'b'];
    arr1.unshift('x', 'y'); // prepend single elements
    assert.deepEqual(arr1, ['x', 'y', 'a', 'b']);
    const arr2 = ['a', 'b'];
    arr2.unshift(...['x', 'y']); // prepend Array
    assert.deepEqual(arr2, ['x', 'y', 'a', 'b']);
    
  • 传播让我们将数组移入arr2
  • 非破坏性预先支付通过扩散元素完成:
    const arr1 = ['a', 'b'];
    assert.deepEqual(
      ['x', 'y', ...arr1], // prepend single elements
      ['x', 'y', 'a', 'b']);
    assert.deepEqual(arr1, ['a', 'b']); // unchanged!
    const arr2 = ['a', 'b'];
    assert.deepEqual(
      [...['x', 'y'], ...arr2], // prepend Array
      ['x', 'y', 'a', 'b']);
    assert.deepEqual(arr2, ['a', 'b']); // unchanged!
    

26.9.2. 附加元素和数组

  • 在下面的代码中,我们破坏性地将单个元素附加到arr1,将数组附加到arr2
    const arr1 = ['a', 'b'];
    arr1.push('x', 'y'); // append single elements
    assert.deepEqual(arr1, ['a', 'b', 'x', 'y']);
    const arr2 = ['a', 'b'];
    arr2.push(...['x', 'y']); // append Array
    assert.deepEqual(arr2, ['a', 'b', 'x', 'y']);
    
  • 传播让我们将数组推入arr2
  • 非破坏性附加是通过扩散元素完成的:
    const arr1 = ['a', 'b'];
    assert.deepEqual(
      [...arr1, 'x', 'y'], // append single elements
      ['a', 'b', 'x', 'y']);
    assert.deepEqual(arr1, ['a', 'b']); // unchanged!
    const arr2 = ['a', 'b'];
    assert.deepEqual(
      [...arr2, ...['x', 'y']], // append Array
      ['a', 'b', 'x', 'y']);
    assert.deepEqual(arr2, ['a', 'b']); // unchanged!
    

26.9.3. 删除元素

  • 这是删除 Array 元素的三种破坏性方法:
    // Destructively remove first element:
    const arr1 = ['a', 'b', 'c'];
    assert.equal(arr1.shift(), 'a');
    assert.deepEqual(arr1, ['b', 'c']);
    // Destructively remove last element:
    const arr2 = ['a', 'b', 'c'];
    assert.equal(arr2.pop(), 'c');
    assert.deepEqual(arr2, ['a', 'b']);
    // Remove one or more elements anywhere:
    const arr3 = ['a', 'b', 'c'];
    assert.deepEqual(arr3.splice(1, 1), ['b']);
    assert.deepEqual(arr3, ['a', 'c']);
    
  • 快速参考部分中详细介绍了.splice()
  • 通过 rest 元素进行解构使您可以从数组的开头非破坏性地删除元素(稍后将介绍解构)。
    const arr1 = ['a', 'b', 'c'];
    // Ignore first element, extract remaining elements
    const [, ...arr2] = arr1;
    assert.deepEqual(arr1, ['a', 'b', 'c']); // unchanged!
    assert.deepEqual(arr2, ['b', 'c']);
    
  • 唉,一个 rest 元素必须始终位于 Array 中。因此,您只能使用它来提取后缀。

练习:通过数组实现队列

exercises/arrays/queue_via_array_test.js

26.10. 方法:迭代和转换(.find().map().filter()等)

在本节中,我们将介绍用于迭代数组和转换数组的 Array 方法。在我们这样做之前,让我们考虑两种不同的迭代方法。它将帮助我们理解这些方法的工作原理。

26.10.1. 外部迭代与内部迭代

假设您的代码想要迭代对象“内部”的值。这样做的两种常用方法是:

  • 外部迭代(pull):您的代码通过迭代协议向对象请求值。例如,for-of循环基于 JavaScript 的迭代协议:
    for (const x of ['a', 'b']) {
      console.log(x);
    }
    // Output:
    // 'a'
    // 'b'
    
    有关更长的示例,请参阅有关同步生成器的章节。
  • 内部迭代(推送):您将回调函数传递给对象的方法,并且该方法将值提供给回调。例如,Arrays 有方法.forEach()
    ['a', 'b'].forEach((x) => {
      console.log(x);
    });
    // Output:
    // 'a'
    // 'b'
    
    有关更长的示例,请参阅有关同步生成器的章节。

我们接下来要看的方法都使用内部迭代。

26.10.2. 迭代和转换方法的回调

  • 迭代或转换方法的回调,具有以下签名:
    callback: (value: T, index: number, array: Array<T>) => boolean
    
  • 也就是说,回调有三个参数(可以忽略其中任何一个):
    • value是最重要的一个。此参数保存当前正在处理的迭代值。
    • index还可以告诉回调迭代值的索引是什么。
    • array指向当前 Array(方法调用的接收者)。一些算法需要引用整个数组 - 例如搜索它的答案。此参数允许您为此类算法编写可重用的回调。
  • 预期返回的回调取决于传递给它的方法。可能性包括:
    • 没什么(.forEach())。
    • 布尔值(.find())。
    • 任意值(.map())。

26.10.3. 搜索元素:.find().findIndex()

  • .find()返回其回调返回 truthy 值的第一个元素:
    > [6, -5, 8].find(x => x < 0)
    - 5
    > [6, 5, 8].find(x => x < 0)
    undefined
    
  • .findIndex()返回其回调返回 truthy 值的第一个元素的索引:
    > [6, -5, 8].findIndex(x => x < 0)
    1
    > [6, 5, 8].findIndex(x => x < 0)
    - 1
    
  • .findIndex()可以实现如下:
    function findIndex(arr, callback) {
      for (const [i, x] of arr.entries()) {
        if (callback(x, i, arr)) {
          return i;
        }
      }
      return -1;
    }
    assert.equal(1, findIndex(['a', 'b', 'c'], x => x === 'b'));
    

26.10.4. .map():复制时给予元素新值

  • .map()返回接收器的副本。副本的元素是将map的回调参数应用于接收器元素的结果。
  • 所有这些都通过示例更容易理解:
    > [1, 2, 3].map(x => x * 3)
    [ 3, 6, 9 ]
    > ['how', 'are', 'you'].map(str => str.toUpperCase())
    [ 'HOW', 'ARE', 'YOU' ]
    > [true, true, true].map((_, index) => index)
    [ 0, 1, 2 ]
    
  • 注意:_只是另一个变量名。
  • .map()可以实现如下:
    function map(arr, mapFunc) {
      const result = [];
      for (const [i, x] of arr.entries()) {
        result.push(mapFunc(x, i, arr));
      }
      return result;
    }
    assert.deepEqual(
      map(['a', 'b', 'c'], (x, i) => `${i}.${x}`),
      ['0.a', '1.b', '2.c']);
    

练习:通过.map() 编号行

exercises/arrays/number_lines_test.js

26.10.5. .flatMap():映射到零个或多个值

  • Array<T>.prototype.flatMap()的类型签名是:
    .flatMap<U>(
      callback: (value: T, index: number, array: T[]) => U|Array<U>,
      thisValue?: any
    ): U[]
    
  • .map().flatMap()都将函数f作为控制输入数组如何转换为输出数组的参数:
    • 使用.map(),每个输入数组元素都被转换为一个输出元素。也就是说,f返回单个值。
    • 使用.flatMap(),每个输入数组元素都转换为零个或多个输出元素。也就是说,f返回一个值数组(它也可以返回非数组值,但这很少见)。
  • 这是.flatMap()的作用:
    > ['a', 'b', 'c'].flatMap(x => [x,x])
    [ 'a', 'a', 'b', 'b', 'c', 'c' ]
    > ['a', 'b', 'c'].flatMap(x => [x])
    [ 'a', 'b', 'c' ]
    > ['a', 'b', 'c'].flatMap(x => [])
    []
    
26.10.5.1. 一个简单的实现
  • 您可以按如下方式实现.flatMap()。注意:此实现比内置版本更简单,例如,执行更多检查。
    function flatMap(arr, mapFunc) {
      const result = [];
      for (const [index, elem] of arr.entries()) {
        const x = mapFunc(elem, index, arr);
        // We allow mapFunc() to return non-Arrays
        if (Array.isArray(x)) {
          result.push(...x);
        } else {
          result.push(x);
        }
      }
      return result;
    }
    
  • 什么是.flatMap()有用?我们来看看用例吧!
26.10.5.2. 使用案例:同时过滤和映射
  • Array 方法.map()的结果始终与调用它的 Array 的长度相同。也就是说,它的回调不能跳过它不感兴趣的数组元素。
  • .flatMap()执行此操作的功能在下一个示例中很有用:processArray()返回一个数组,其中每个元素都是包装值或包装错误。
    function processArray(arr, process) {
      return arr.map(x => {
        try {
          return { value: process(x) };
        } catch (e) {
          return { error: e };
        }
      });
    }
    
  • 以下代码显示processArray()正在运行:
    let err;
    function myFunc(value) {
      if (value < 0) {
        throw (err = new Error('Illegal value: '+value));
      }
      return value;
    }
    const results = processArray([1, -5, 6], myFunc);
    assert.deepEqual(results, [
      { value: 1 },
      { error: err },
      { value: 6 },
    ]);
    
  • .flatMap()使我们能够仅从results中提取值或仅提取错误:
    const values = results.flatMap(
      result => result.value ? [result.value] : []);
    assert.deepEqual(values, [1, 6]);
    const errors = results.flatMap(
      result => result.error ? [result.error] : []);
    assert.deepEqual(errors, [err]);
    
26.10.5.3. 用例:映射到多个值
  • Array 方法.map()将每个输入 Array 元素映射到一个输出元素。但是如果我们想将它映射到多个输出元素呢?
  • 这在以下示例中变得必要:
    • 输入:['a', 'b', 'c']
    • 输出:['<span>a</span>', ', ', '<span>b</span>', ', ', '<span>c</span>']
  • 进行此转换的函数wrap()类似于您为前端库 React 编写的代码:
    function wrap(tags) {
      return tags.flatMap(
        (tag, index) => {
          const html = `<span>${tag}</span>`;
          if (index === 0) {
            return [html];
          } else {
            return [', ', html];
          }
        }
      );
    }
    assert.deepEqual(
      wrap(['a', 'b', 'c']),
      ['<span>a</span>', ', ', '<span>b</span>', ', ', '<span>c</span>']
    );
    

练习:.flatMap()

exercises/arrays/convert_to_numbers_test.js

exercises/arrays/replace_objects_test.js

26.10.6. .filter():只保留一些元素

  • Array 方法.filter()返回一个 Array,收集回调返回 truthy 值的所有元素。
  • 例如:
    > [-1, 2, 5, -7, 6].filter(x => x >= 0)
    [ 2, 5, 6 ]
    > ['a', 'b', 'c', 'd'].filter((_,i) => (i%2)===0)
    [ 'a', 'c' ]
    
  • .filter()可以实现如下:
    function filter(arr, filterFunc) {
      const result = [];
      for (const [i, x] of arr.entries()) {
        if (filterFunc(x, i, arr)) {
          result.push(x);
        }
      }
      return result;
    }
    assert.deepEqual(
      filter([ 1, 'a', 5, 4, 'x'], x => typeof x === 'number'),
      [1, 5, 4]);
    assert.deepEqual(
      filter([ 1, 'a', 5, 4, 'x'], x => typeof x === 'string'),
      ['a', 'x']);
    

练习:通过.filter() 删除空行

exercises/arrays/remove_empty_lines_filter_test.js

26.10.7. .reduce():从数组中获取值(高级)

  • 方法.reduce()是一个用于计算数组“摘要”的强大工具。摘要可以是任何类型的值:
    • 一个号码。例如,所有 Array 元素的总和。
    • 数组。例如,Array 的副本,元素乘以 2。
    • 等等。
  • 此操作在函数式编程中也称为foldl(“向左折叠”),这种写法也非常的普遍。需要注意的是,它可能使代码难以理解。
  • .reduce()具有以下类型签名(在Array<T>内):
    .reduce<U>(
      callback: (accumulator: U, element: T, index: number, array: T[]) => U,
      init?: U)
      : U
    
  • T是数组元素的类型,U是摘要的类型。两者可能有所不同,也可能没有。 accumulator只是“摘要”的另一个名称。
  • 要计算数组arr的摘要,.reduce()将所有数组元素一次一个地提供给其回调:
    const accumulator_0 = callback(init, arr[0]);
    const accumulator_1 = callback(accumulator_0, arr[1]);
    const accumulator_2 = callback(accumulator_1, arr[2]);
    // Etc.
    
  • callback将先前计算的结果(存储在其参数accumulator中)与当前的 Array 元素组合,并返回下一个accumulator.reduce()的结果是最终累加器 - callback的最后一个结果,在它访问了所有元素之后。
  • 换句话说:callback完成大部分工作,.reduce()只是以有用的方式调用它。
  • 你可以说回调将数组元素折叠到累加器中。这就是为什么这个操作在函数式编程中称为“折叠”。
26.10.7.1. 第一个例子
  • 让我们看一下.reduce()的实例:函数addAll()计算数组arr中所有数字的总和。
    function addAll(arr) {
      const startSum = 0;
      const callback = (sum, element) => sum + element;
      return arr.reduce(callback, startSum);
    }
    assert.equal(addAll([1,  2, 3]), 6); // (A)
    assert.equal(addAll([7, -4, 2]), 5);
    
  • 在这种情况下,累加器保存callback已经访问过的所有数组元素的总和。
  • 结果6是如何从 A 行的数组中得出的?通过以下callback调用:
    callback(0, 1) --> 1
    callback(1, 2) --> 3
    callback(3, 3) --> 6
    
  • 笔记:
    • 第一个参数是电流累加器(从.reduce()的参数init开始)。
    • 第二个参数是当前的 Array 元素。
    • 结果是下一个累加器。
    • callback的最后结果也是.reduce()的结果。
  • 或者,我们可以通过for-of循环实现addAll()
    function addAll(arr) {
      let sum = 0;
      for (const element of arr) {
        sum = sum + element;
      }
      return sum;
    }
    
  • 很难说这两个实现中的哪一个“更好”:基于.reduce()的实现更简洁,但如果你特别不熟悉函数式编程时,基于for-of的实现可能更容易理解。
26.10.7.2. 示例:通过.reduce()查找索引
  • 以下函数是 Array 方法.indexOf()的实现。它返回给定searchValue出现在 Array arr内的第一个索引:
    const NOT_FOUND = -1;
    function indexOf(arr, searchValue) {
      return arr.reduce(
        (result, elem, index) => {
          if (result !== NOT_FOUND) {
            // We have already found something: don’t change anything
            return result;
          } else if (elem === searchValue) {
            return index;
          } else {
            return NOT_FOUND;
          }
        },
        NOT_FOUND);
    }
    assert.equal(indexOf(['a', 'b', 'c'], 'b'), 1);
    assert.equal(indexOf(['a', 'b', 'c'], 'x'), -1);
    
  • .reduce()的一个限制是您无法提前完成(在for-of循环中,您可以break)。在这里,一旦我们找到了我们想要的东西,我们就不会做任何事情。
26.10.7.3. 示例:加倍数组元素
  • 函数double(arr)返回inArr的副本,其元素全部乘以 2:
    function double(inArr) {
      return inArr.reduce(
        (outArr, element) => {
          outArr.push(element * 2);
          return outArr;
        },
        []);
    }
    assert.deepEqual(
      double([1, 2, 3]),
      [2, 4, 6]);
    
  • 我们通过推入修改初始值[]double()的非破坏性,功能更强的版本如下:
    function double(inArr) {
      return inArr.reduce(
        // Don’t change `outArr`, return a fresh Array
        (outArr, element) => [...outArr, element * 2],
        []);
    }
    assert.deepEqual(
      double([1, 2, 3]),
      [2, 4, 6]);
    
  • 这个版本更优雅,但也更慢并且使用更多内存。

练习:.reduce()

map()通过.reduce()exercises/arrays/map_via_reduce_test.js

filter()通过.reduce()exercises/arrays/filter_via_reduce_test.js

countMatches()通过.reduce()exercises/arrays/count_matches_via_reduce_test.js

26.11. .sort():排序数组

  • .sort()具有以下类型定义:
    sort(compareFunc?: (a: T, b: T) => number): this
    
  • .sort()始终对元素的字符串表示进行排序。这些表示通过<进行比较。该运算符按字典顺序比较 (第一个字符最重要)。您可以在比较数字时看到:
    > [200, 3, 10].sort()
    [ 10, 200, 3 ]
    
  • 比较人类语言字符串时,您需要知道它们是根据它们的代码单元值(字符代码)进行比较的:
    > ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
    [ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]
    
  • 正如您所看到的,所有非重音大写字母都出现在所有重音字母之前,这些字母位于所有重音字母之前。如果要对人类语言进行适当的排序,请使用IntlJavaScript 国际化 API
  • 最后,.sort()排序到位:它改变并返回其接收器:
    > const arr = ['a', 'c', 'b'];
    > arr.sort() === arr
    true
    > arr
    [ 'a', 'b', 'c' ]
    

26.11.1. 自定义排序顺序

  • 您可以通过参数compareFunc自定义排序顺序,该参数返回一个数字:
    • 否定如果a < b
    • 如果a === b为零
    • 如果a > b为正
  • 记住这些规则的提示:负数是 小于 零(等)。

26.11.2. 排序数字

  • 您可以使用以下辅助函数来比较数字:
    function compareNumbers(a, b) {
      if (a < b) {
        return -1;
      } else if (a === b) {
        return 0;
      } else {
        return 1;
      }
    }
    assert.deepEqual(
      [200, 3, 10].sort(compareNumbers),
      [3, 10, 200]);
    
  • 以下是一个快速而肮脏的选择。它的缺点是它很神秘,存在数字溢出的风险:
    > [200, 3, 10].sort((a,b) => a-b)
    [ 3, 10, 200 ]
    

26.11.3. 排序对象

  • 如果要对对象进行排序,还需要使用比较函数。例如,以下代码显示了如何按年龄对对象进行排序。
    const arr = [ {age: 200}, {age: 3}, {age: 10} ];
    assert.deepEqual(
      arr.sort((obj1, obj2) => obj1.age - obj2.age),
      [{ age: 3 }, { age: 10 }, { age: 200 }] );
    

练习:按名称排序对象

exercises/arrays/sort_objects_test.js

26.12. 快速参考:Array<T>

  • R:方法不改变接收器(非破坏性)。
  • W:方法改变接收器(破坏性)。

26.12.1. new Array()

  • new Array(n)创建一个长度为n的数组,其中包含n孔:
    // Trailing commas are always ignored.
    // Therefore: number of commas = number of holes
    assert.deepEqual(new Array(3), [,,,]);
    
  • new Array()创建一个空数组。但是,我建议总是使用[]

26.12.2. Array的静态方法

  • Array.from<T>(iterable: Iterable<T> | ArrayLike<T>): T[] ^[ES6]^
  • Array.from<T,U>(iterable: Iterable<T> | ArrayLike<T>, mapFunc: (v: T, k: number) => U, thisArg?: any): U[] ^[ES6]^ 将可迭代或类似 Array 的对象转换为 Array。可选地,输入值可以在添加到输出数组之前通过mapFunc进行转换。 类数组对象具有.length和索引属性(粗略地,非负整数的字符串表示):
    interface ArrayLike<T> {
      length: number;
      [n: number]: T;
    }
    
    例子:
    > Array.from(new Set(['a', 'b']))
    [ 'a', 'b' ]
    > Array.from({length: 2, 0:'a', 1:'b'})
    [ 'a', 'b' ]
    
  • Array.of<T>(...items: T[]): T[] ^[ES6]^ 这个静态方法主要用于Array和 Typed Arrays 的子类,它用作自定义数组字面量:
    assert.equal(
      Uint8Array.of(1, 2, 3) instanceof Uint8Array, true);
    

26.12.3. Array<T>.prototype的方法

  • .concat(...items: Array<T[] | T>): T[] ^[R,ES3]^ 返回一个新的 Array,它是接收器和所有items的串联。非数组参数被视为具有单个元素的数组。
    > ['a'].concat('b', ['c', 'd'])
    [ 'a', 'b', 'c', 'd' ]
    
  • .copyWithin(target: number, start: number, end=this.length): this ^[W,ES6]^ 将索引范围从start到(Excel。)end的元素复制到以target开头的索引。正确处理重叠。
    > ['a', 'b', 'c', 'd'].copyWithin(0, 2, 4)
    [ 'c', 'd', 'c', 'd' ]
    
  • .entries(): Iterable<[number, T]> ^[R,ES6]^ 返回[index,element]对上的可迭代。
    > Array.from(['a', 'b'].entries())
    [ [ 0, 'a' ], [ 1, 'b' ] ]
    
  • .every(callback: (value: T, index: number, array: Array<T>) => boolean, thisArg?: any): boolean ^[R,ES5]^ 如果callback为每个元素和false返回true,则返回true,否则返回。收到false后立即停止。该方法对应于数学中的通用量化(对于所有,)。
    > [1, 2, 3].every(x => x > 0)
    true
    > [1, -2, 3].every(x => x > 0)
    false
    
  • .fill(value: T, start=0, end=this.length): this ^[W,ES6]^ 将value分配给(incl。)start和(excl。)end之间的每个索引。
    > [0, 1, 2].fill('a')
    [ 'a', 'a', 'a' ]
    
  • .filter(callback: (value: T, index: number, array: Array<T>) => any, thisArg?: any): T[] ^[R,ES5]^ 返回一个只包含callback返回true的元素的数组。
    > [1, -2, 3].filter(x => x > 0)
    [ 1, 3 ]
    
  • .find(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): T | undefined ^[R,ES6]^ 结果是predicate返回true的第一个元素。如果它永远不会,结果是undefined
    > [1, -2, 3].find(x => x < 0)
    - 2
    > [1, 2, 3].find(x => x < 0)
    undefined
    
  • .findIndex(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): number ^[R,ES6]^ 结果是predicate返回true的第一个元素的索引。如果它永远不会,结果是-1
    > [1, -2, 3].findIndex(x => x < 0)
    1
    > [1, 2, 3].findIndex(x => x < 0)
    - 1
    
  • .flat(depth = 1): any[] ^[R,ES2019]^ “展平”数组:它创建数组的副本,其中嵌套数组中的值都出现在顶层。参数depth控制.flat()查找非数组值的深度。
    > [ 1,2, [3,4], [[5,6]] ].flat(0) // no change
    [ 1, 2, [ 3, 4 ], [ [ 5, 6 ] ] ]
    > [ 1,2, [3,4], [[5,6]] ].flat(1)
    [ 1, 2, 3, 4, [ 5, 6 ] ]
    > [ 1,2, [3,4], [[5,6]] ].flat(2)
    [ 1, 2, 3, 4, 5, 6 ]
    
  • .flatMap<U>(callback: (value: T, index: number, array: T[]) => U|Array<U>, thisValue?: any): U[] ^[R,ES2019]^ 通过为原始 Array 的每个元素调用callback()并连接它返回的 Arrays 来生成结果。
    > ['a', 'b', 'c'].flatMap(x => [x,x])
    [ 'a', 'a', 'b', 'b', 'c', 'c' ]
    > ['a', 'b', 'c'].flatMap(x => [x])
    [ 'a', 'b', 'c' ]
    > ['a', 'b', 'c'].flatMap(x => [])
    []
    
  • .forEach(callback: (value: T, index: number, array: Array<T>) => void, thisArg?: any): void ^[R,ES5]^ 为每个元素调用callback
    ['a', 'b'].forEach((x, i) => console.log(x, i))
    // Output:
    // 'a', 0
    // 'b', 1
    
  • .includes(searchElement: T, fromIndex=0): boolean ^[R,ES2016]^ 如果接收器具有值为searchElementfalse的元素,则返回true。搜索从索引fromIndex开始。
    > [0, 1, 2].includes(1)
    true
    > [0, 1, 2].includes(5)
    false
    
  • .indexOf(searchElement: T, fromIndex=0): number ^[R,ES5]^ 返回严格等于searchElement的第一个元素的索引。如果没有这样的元素,则返回-1。开始在索引fromIndex搜索,然后访问更高的索引。
    > ['a', 'b', 'a'].indexOf('a')
    0
    > ['a', 'b', 'a'].indexOf('a', 1)
    2
    > ['a', 'b', 'a'].indexOf('c')
    - 1
    
  • .join(separator = ','): string ^[R,ES1]^ 通过连接所有元素的字符串表示形式创建一个字符串,用separator分隔它们。
    > ['a', 'b', 'c'].join('##')
    'a##b##c'
    > ['a', 'b', 'c'].join()
    'a,b,c'
    
  • .keys(): Iterable<number> ^[R,ES6]^ 返回接收器上可迭代的键。
    > [...['a', 'b'].keys()]
    [ 0, 1 ]
    
  • .lastIndexOf(searchElement: T, fromIndex=this.length-1): number ^[R,ES5]^ 返回严格等于searchElement的最后一个元素的索引。如果没有这样的元素,则返回-1。开始在索引fromIndex搜索,然后访问较低的索引。
    > ['a', 'b', 'a'].lastIndexOf('a')
    2
    > ['a', 'b', 'a'].lastIndexOf('a', 1)
    0
    > ['a', 'b', 'a'].lastIndexOf('c')
    - 1
    
  • .map<U>(mapFunc: (value: T, index: number, array: Array<T>) => U, thisArg?: any): U[] ^[R,ES5]^ 返回一个新的 Array,其中每个元素都是mapFunc应用于接收器的相应元素的结果。
    > [1, 2, 3].map(x => x * 2)
    [ 2, 4, 6 ]
    > ['a', 'b', 'c'].map((x, i) => i)
    [ 0, 1, 2 ]
    
  • .pop(): T | undefined ^[W,ES3]^ 删除并返回接收器的最后一个元素。也就是说,它将接收器的末尾视为堆栈。与.push()相反。
    > const arr = ['a', 'b', 'c'];
    > arr.pop()
    'c'
    > arr
    [ 'a', 'b' ]
    
  • .push(...items: T[]): number ^[W,ES3]^ 在接收器的末尾添加零个或多个items。也就是说,它将接收器的末尾视为堆栈。返回值是更改后接收器的长度。与.pop()相反。
    > const arr = ['a', 'b'];
    > arr.push('c', 'd')
    4
    > arr
    [ 'a', 'b', 'c', 'd' ]
    
  • .reduce<U>(callback: (accumulator: U, element: T, index: number, array: T[]) => U, init?: U): U ^[R,ES5]^ 此方法生成接收器的摘要:它将所有 Array 元素提供给callback,它将当前中间结果(在参数accumulator中)与当前 Array 元素组合并返回下一个accumulator
    const accumulator_0 = callback(init, arr[0]);
    const accumulator_1 = callback(accumulator_0, arr[1]);
    const accumulator_2 = callback(accumulator_1, arr[2]);
    // Etc.
    
    .reduce()的结果是访问所有 Array 元素后callback的最后结果。 如果未提供init,则使用索引 0 处的 Array 元素。
    > [1, 2, 3].reduce((accu, x) => accu + x, 0)
    6
    > [1, 2, 3].reduce((accu, x) => accu + String(x), '')
    '123'!
    
  • .reduceRight<U>(callback: (accumulator: U, element: T, index: number, array: T[]) => U, init?: U): U ^[R,ES5]^ 像.reduce()一样工作,但是从最后一个元素开始向后访问 Array 元素。
    > [1, 2, 3].reduceRight((accu, x) => accu + String(x), '')
    '321'
    
  • .reverse(): this ^[W,ES1]^ 重新排列接收器的元素,使它们的顺序相反,然后返回接收器。
    > const arr = ['a', 'b', 'c'];
    > arr.reverse()
    [ 'c', 'b', 'a' ]
    > arr
    [ 'c', 'b', 'a' ]
    
  • .shift(): T | undefined ^[W,ES3]^ 删除并返回接收器的第一个元素。与.unshift()相反。
    > const arr = ['a', 'b', 'c'];
    > arr.shift()
    'a'
    > arr
    [ 'b', 'c' ]
    
  • .slice(start=0, end=this.length): T[] ^[R,ES3]^ 返回一个新的 Array,包含接收器的元素,其索引在(incl。)start和(excl。)end之间。
    > ['a', 'b', 'c', 'd'].slice(1, 3)
    [ 'b', 'c' ]
    > ['a', 'b'].slice() // shallow copy
    [ 'a', 'b' ]
    
  • .some(callback: (value: T, index: number, array: Array<T>) => boolean, thisArg?: any): boolean ^[R,ES5]^ 如果callback为至少一个元素返回true,则返回true,否则返回false。收到true后立即停止。该方法对应于数学中的存在量化(存在,)。
    > [1, 2, 3].some(x => x < 0)
    false
    > [1, -2, 3].some(x => x < 0)
    true
    
  • .sort(compareFunc?: (a: T, b: T) => number): this ^[W,ES1]^ 对接收器进行排序并将其返回。从 ECMAScript 2019 开始,保证排序是稳定的:如果通过排序认为元素相等,那么排序不会改变这些元素的顺序(相对于彼此)。 默认情况下,它对元素的字符串表示进行排序。它按字典顺序并根据字符的代码单元值(字符代码)执行:
    > ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
    [ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]
    > [200, 3, 10].sort()
    [ 10, 200, 3 ]
    
    您可以通过compareFunc自定义排序顺序,它会返回一个数字:* 否定如果a < b
    • 如果a === b为零
    • 如果a > b为正 排序数字的伎俩(有数字溢出的风险):
    > [200, 3, 10].sort((a, b) => a - b)
    [ 3, 10, 200 ]
    
  • .splice(start: number, deleteCount=this.length-start, ...items: T[]): T[] ^[W,ES3]^ 在索引start处,它删除deleteCount元素并插入items。它返回已删除的元素。
    > const arr = ['a', 'b', 'c', 'd'];
    > arr.splice(1, 2, 'x', 'y')
    [ 'b', 'c' ]
    > arr
    [ 'a', 'x', 'y', 'd' ]
    
  • .toString(): string ^[R,ES1]^ 返回一个字符串,其中包含所有元素的字符串,以逗号分隔。
    > [1, 2, 3].toString()
    '1,2,3'
    > ['a', 'b', 'c'].toString()
    'a,b,c'
    > [].toString()
    ''
    
  • .unshift(...items: T[]): number ^[W,ES3]^ 在接收器的开头插入items并在此修改后返回其长度。
    > const arr = ['c', 'd'];
    > arr.unshift('e', 'f')
    4
    > arr
    [ 'e', 'f', 'c', 'd' ]
    
  • .values(): Iterable<T> ^[R,ES6]^ 返回接收器上可迭代的值。
    > [...['a', 'b'].values()]
    [ 'a', 'b' ]
    

26.12.4. 来源