ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## 28.数组(`Array`) > 原文: [http://exploringjs.com/impatient-js/ch_arrays.html](http://exploringjs.com/impatient-js/ch_arrays.html) > > 贡献者:[52admln](https://github.com/52admln) ### 28.1。 JavaScript 中数组的两个角色 数组在 JavaScript 中扮演两个角色: * 元组:Arrays-as-tuples 具有固定数量的索引元素。这些元素中的每一个都可以具有不同的类型。 * 序列:Arrays-as-sequences 具有可变数量的索引元素。每个元素都具有相同的类型。 实际上,数组一般都由这两个角色组成。 值得注意的是,Arrays-as-sequences 非常灵活,您可以将它们用作(传统)数组,堆栈和队列(请参阅本章末尾的练习)。 ### 28.2。数组基本操作 #### 28.2.1。数组:创建,读取,更改 创建数组的最佳方法是使用 _数组字面量_: ```js const arr = ['a', 'b', 'c']; ``` 数组(`Array`)字面量以方括号`[]`开头和结尾。它创建了一个包含三个 _元素_ 的数组:`'a'`,`'b'`和`'c'`。 要读取数组(`Array`)元素,请在方括号中填入索引(索引从零开始): ```js assert.equal(arr[0], 'a'); ``` 要更改数组(`Array`)元素,可以使用对应索引修改其值: ```js arr[0] = 'x'; assert.deepEqual(arr, ['x', 'b', 'c']); ``` 数组索引的范围是 32 位(不包括最大长度):[0,2 <sup>32</sup> -1) #### 28.2.2。数组:`.length` 每个数组(`Array`)都有一个属性`.length`,可用于读取和更改(!)数组中元素的数量。 数组的长度始终是最高的索引加一: ```js > const arr = ['a', 'b']; > arr.length 2 ``` 如果使用长度作为索引值写入数组,则会追加一个元素: ```js > arr[arr.length] = 'c'; > arr [ 'a', 'b', 'c' ] > arr.length 3 ``` (破坏性地)附加元素的另一种方法是通过数组(`Array`)方法`.push()`: ```js > arr.push('d'); > arr [ 'a', 'b', 'c', 'd' ] ``` 如果设置`.length`,则会通过删除元素来设置数组长度: ```js > arr.length = 1; > arr [ 'a' ] ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:通过`.push()`** 删除空行 `exercises/arrays/remove_empty_lines_push_test.js` #### 28.2.3。数组字面量展开 在数组(`Array`)字面量中,_扩展运算符_ 由三个点(`...`)组成,后跟一个表达式。它导致表达式重新计算迭代,最后形成一个新的数组。例如: ```js > const iterable = ['b', 'c']; > ['a', ...iterable, 'd'] [ 'a', 'b', 'c', 'd' ] ``` 扩展运算符可以很方便地将两个数组合并起来: ```js const arr1 = ['a', 'b']; const arr2 = ['c', 'd']; const concatenated = [...arr1, ...arr2, 'e']; assert.deepEqual( concatenated, ['a', 'b', 'c', 'd', 'e']); ``` #### 28.2.4。数组:列出索引和属性 方法`.keys()`列出数组的索引: ```js const arr = ['a', 'b']; assert.deepEqual( [...arr.keys()], // (A) [0, 1]); ``` `.keys()`返回一个可迭代的。在 A 行,我们通过扩展运算符生成了一个数组。 列表数组索引与列表属性不同。当你执行后者时,你得到索引 - 但作为字符串 - 加上非索引属性键: ```js const arr = ['a', 'b']; arr.prop = true; assert.deepEqual( Object.keys(arr), ['0', '1', 'prop']); ``` 方法`.entries()`将数组的内容列为[index,element]对: ```js const arr = ['a', 'b']; assert.deepEqual( [...arr.entries()], [[0, 'a'], [1, 'b']]); ``` #### 28.2.5。数值是一个数组吗? 这两种方法可以检查值是否为数组: ```js > [] instanceof Array true > Array.isArray([]) true ``` `instanceof`通常很好。如果某个值可能来自另一个 _域_,则需要`Array.isArray()`。粗略地说,领域是 JavaScript 全局范围的一个实例。一些领域彼此隔离(例如浏览器中的 [Web Workers](ch_async-js.html#web-workers) ),但也有一些领域可以移动数据(例如浏览器中的同源 iframe)。 `x instanceof Array`检查`x`的原型链,因此如果`x`是来自另一个域的数组,则返回`false`。 `typeof`将数组归类为对象: ```js > typeof [] 'object' ``` ### 28.3。 `for-of`和数组 我们已经遇到了`for-of`循环。本节简要介绍如何将它用于数组。 #### 28.3.1。 `for-of`:迭代元素 以下`for-of`循环遍历数组的元素。 ```js for (const element of ['a', 'b']) { console.log(element); } // Output: // 'a' // 'b' ``` #### 28.3.2。 `for-of`:迭代[index,element]对 以下`for-of`循环遍历[index,element]对。解构(稍后描述)为我们提供了在`for-of`的头部设置`index`和`element`的便捷语法。 ```js for (const [index, element] of ['a', 'b'].entries()) { console.log(index, element); } // Output: // 0, 'a' // 1, 'b' ``` ### 28.4。类数组对象 一些使用 Arrays 的操作只需要最小值:值必须只是 _类似于数组_。类数组是具有以下属性的对象: * `.length`:保存类似 Array 的对象的长度。 * `['0']`:将元素保持在索引 0 处。(等等) `ArrayLike`的 TypeScript 接口如下所示。 ```js interface ArrayLike<T> { length: number; [n: number]: T; } ``` `Array.from()`接受类似 Array 的对象并将它们转换为 Arrays: ```js // 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 之前很常见;现在你不经常看到它们。 ### 28.5。将可迭代和类似数组的值转换为数组 将可迭代和类似数组的值转换为数组有两种常用方法:扩展和`Array.from()`。 #### 28.5.1。通过扩展运算符(`...`)将迭代转换为数组 在 Array 字面量中,通过 `...` 将任何可迭代对象转换为一系列 Array 元素。例如: ```js // 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 集合是可迭代的。 #### 28.5.2。通过`Array.from()`将可迭代和类似数组的对象转换为数组(高级) `Array.from()`可以使用两种模式。 ##### 28.5.2.1。 `Array.from()`的模式 1:转换 第一种模式具有以下类型签名: ```js .from<T>(iterable: Iterable<T> | ArrayLike<T>): T[]; ``` 接口`Iterable`在[中显示了介绍迭代](ch_sync-iteration.html#iterable-iterator-iteratorresult)的章节。本章前面出现[HTD2]接口`ArrayLike`。 使用单个参数,`Array.from()`将任何可迭代或类似 Array 的数据转换为数组: ```js > Array.from(new Set(['a', 'b'])) [ 'a', 'b' ] > Array.from({length: 2, 0:'a', 1:'b'}) [ 'a', 'b' ] ``` ##### 28.5.2.2。 `Array.from()`的模式 2:转换和映射 `Array.from()`的第二种模式涉及两个参数: ```js .from<T, U>( iterable: Iterable<T> | ArrayLike<T>, mapFunc: (v: T, i: number) => U, thisArg?: any) : U[]; ``` 在这种模式下,`Array.from()`做了几件事: * 它迭代`iterable`。 * 它将`mapFunc`应用于每个迭代值。 * 它将结果收集到一个新数组中并返回它。 可选参数`thisArg`指定`mapFunc`的`this`。 这意味着我们将从具有`T`类型的元素的 iterable 转变为具有`U`类型的元素的 Array。 这是一个例子: ```js > Array.from(new Set(['a', 'b']), x => x + x) [ 'aa', 'bb' ] ``` ### 28.6。创建和填充任意长度的数组 创建数组的最佳方法是通过数组字面量。但是,您不能总是使用一个:数组可能太大,您可能在开发过程中不知道它的长度,或者您可能希望保持其长度灵活。然后我推荐以下用于创建和填充数组的技术: * 你需要创建一个你将完全填充的空数组,然后呢? ```js > new Array(3) [ , , ,] ``` 请注意,结果有 3 个孔 - 数组字面量中的最后一个逗号始终被忽略。 * 你需要创建一个用原始值初始化的数组吗? ```js > new Array(3).fill(0) [0, 0, 0] ``` 警告:如果对对象使用`.fill()`,则每个 Array 元素将引用同一个对象。 * 你需要创建一个用对象初始化的数组吗? ```js > Array.from({length: 3}, () => ({})) [{}, {}, {}] ``` * 你需要创建一系列整数吗? ```js const START = 2; const END = 5; assert.deepEqual( Array.from({length: END-START}, (x, i) => i+START), [2, 3, 4]); ``` 如果您正在处理整数或浮点数的数组,请考虑 [_类型数组_](ch_typed-arrays.html) - 这是为此目的而创建的。 ### 28.7。多维数组 JavaScript 没有真正的多维数组;你需要求助于其元素为数组的数组: ```js 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 ] ], ]); ``` 观察: * 我们通过为索引为当前长度的插槽分配值来增长数组。 * 每个维度 - 除了最后一个维度 - 是一个数组,其元素是下一个维度(行 A,行 B)。 * 最后一个维度包含实际值(第 C 行)。 ### 28.8。更多数组功能(高级) 在本节中,我们将介绍在使用 Arrays 时经常遇到的现象。 #### 28.8.1。数组元素是(稍微特殊)属性 您认为 Array 元素是特殊的,因为您通过数字访问它们。但是这样做的方括号运算符(`[ ]`)与用于访问属性的运算符相同。并且,根据语言规范,它将任何值(不是符号)强制转换为字符串。因此:数组元素是(几乎)正常属性(A 行),如果使用数字或字符串作为索引(行 B 和 C),则无关紧要: ```js 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 位无符号整数并返回后生成原始值的索引。写成公式: ```js ToString(ToUint32(key)) === key ``` JavaScript 在列出所有对象的属性键时特别对待索引!它们总是排在第一位并按数字排序,而不是按字典顺序排列(`'10'`将在`'2'`之前出现): ```js 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()`将数组索引视为数字并忽略非索引属性: ```js 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()`返回的迭代转换为数组。 #### 28.8.2。数组是字典,可以有空元素 JavaScript 支持两种数组: * 密集数组:是数组形成连续序列的数组。这是迄今为止我们见过的唯一一种数组。 * 稀疏数组:包含空元素的数组。也就是说,一些指数缺失。 一般来说,最好避免漏洞,因为它们会使代码更复杂,并且不会被 Array 方法一致地处理。此外,JavaScript 引擎优化密集数组,因此它们更快。 您可以在分配元素时通过跳过索引来创建空数组元素: ```js 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`元素并且不会显示它们。 另一种创建漏洞的方法是跳过数组字面量中的元素: ```js const arr = ['a', , 'c']; assert.deepEqual(Object.keys(arr), ['0', '2']); ``` 你可以删除数组元素: ```js 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”](http://exploringjs.com/es6/ch_arrays.html#sec_array-holes)。 ### 28.9。添加和删​​除元素(破坏性和非破坏性) JavaScript 的`Array`非常灵活,更像是数组,堆栈和队列的组合。本节探讨添加和删除 Array 元素的方法。大多数操作可以破坏性地(修改数组)和非破坏性地(生成修改的副本)执行。 #### 28.9.1。预先添加元素和数组 在下面的代码中,我们破坏性地将单个元素添加到`arr1`,将数组添加到`arr2`: ```js 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`。 非破坏性预先支付通过扩散元素完成: ```js 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! ``` #### 28.9.2。附加元素和数组 在下面的代码中,我们破坏性地将单个元素附加到`arr1`,将数组附加到`arr2`: ```js 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`。 非破坏性附加是通过扩散元素完成的: ```js 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! ``` #### 28.9.3。删除元素 这是删除 Array 元素的三种破坏性方法: ```js // 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']); ``` [快速参考部分](ch_arrays.html#quickref-arrays)中详细介绍了`.splice()`。 通过 rest 元素进行解构使您可以从数组的开头非破坏性地删除元素(稍后将介绍解构)。 ```js 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 中。因此,您只能使用它来提取后缀。 ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:通过数组实现队列** `exercises/arrays/queue_via_array_test.js` ### 28.10。方法:迭代和转换(`.find()`,`.map()`,`.filter()`等) 在本节中,我们将介绍用于迭代数组和转换数组的 Array 方法。在我们这样做之前,让我们考虑两种不同的迭代方法。它将帮助我们理解这些方法的工作原理。 #### 28.10.1。外部迭代与内部迭代 假设您的代码想要迭代对象“内部”的值。这样做的两种常用方法是: * 外部迭代(pull):您的代码通过迭代协议向对象请求值。例如,`for-of`循环基于 JavaScript 的迭代协议: ```js for (const x of ['a', 'b']) { console.log(x); } // Output: // 'a' // 'b' ``` 有关更长的示例,请参阅[有关同步生成器](ch_sync-generators.html#external-iteration-example)的章节。 * 内部迭代(推送):您将[回调函数](ch_callables.html#callback-function)传递给对象的方法,并且该方法将值提供给回调。例如,Arrays 有方法`.forEach()`: ```js ['a', 'b'].forEach((x) => { console.log(x); }); // Output: // 'a' // 'b' ``` 有关更长的示例,请参阅[有关同步生成器](ch_sync-generators.html#internal-iteration-example)的章节。 我们接下来要看的方法都使用内部迭代。 #### 28.10.2。迭代和转换方法的回调 迭代或转换方法的回调,具有以下签名: ```js callback: (value: T, index: number, array: Array<T>) => boolean ``` 也就是说,回调有三个参数(可以忽略其中任何一个): * `value`是最重要的一个。此参数保存当前正在处理的迭代值。 * `index`还可以告诉回调迭代值的索引是什么。 * `array`指向当前 Array(方法调用的接收者)。一些算法需要引用整个数组 - 例如搜索它的答案。此参数允许您为此类算法编写可重用的回调。 预期返回的回调取决于传递给它的方法。可能性包括: * 没什么(`.forEach()`)。 * 布尔值(`.find()`)。 * 任意值(`.map()`)。 #### 28.10.3。搜索元素:`.find()`,`.findIndex()` `.find()`返回其回调返回 truthy 值的第一个元素: ```js > [6, -5, 8].find(x => x < 0) -5 > [6, 5, 8].find(x => x < 0) undefined ``` `.findIndex()`返回其回调返回 truthy 值的第一个元素的索引: ```js > [6, -5, 8].findIndex(x => x < 0) 1 > [6, 5, 8].findIndex(x => x < 0) -1 ``` `.findIndex()`可以实现如下: ```js 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')); ``` #### 28.10.4。 `.map()`:复制时给予元素新值 `.map()`返回接收器的副本。副本的元素是将`map`的回调参数应用于接收器元素的结果。 所有这些都通过示例更容易理解: ```js > [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()`可以实现如下: ```js 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']); ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:通过`.map()`** 编号行 `exercises/arrays/number_lines_test.js` #### 28.10.5。 `.flatMap()`:映射到零个或多个值 `Array<T>.prototype.flatMap()`的类型签名是: ```js .flatMap<U>( callback: (value: T, index: number, array: T[]) => U|Array<U>, thisValue?: any ): U[] ``` `.map()`和`.flatMap()`都将函数`f`作为控制输入数组如何转换为输出数组的参数: * 使用`.map()`,每个输入数组元素都被转换为一个输出元素。也就是说,`f`返回单个值。 * 使用`.flatMap()`,每个输入数组元素都转换为零个或多个输出元素。也就是说,`f`返回一个值数组(它也可以返回非数组值,但这很少见)。 这是`.flatMap()`的作用: ```js > ['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 => []) [] ``` ##### 28.10.5.1。一个简单的实现 您可以按如下方式实现`.flatMap()`。注意:此实现比内置版本更简单,例如,执行更多检查。 ```js 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()`有用?我们来看看用例吧! ##### 28.10.5.2。使用案例:同时过滤和映射 Array 方法`.map()`的结果始终与调用它的 Array 的长度相同。也就是说,它的回调不能跳过它不感兴趣的数组元素。 `.flatMap()`执行此操作的功能在下一个示例中很有用:`processArray()`返回一个数组,其中每个元素都是包装值或包装错误。 ```js function processArray(arr, process) { return arr.map(x => { try { return { value: process(x) }; } catch (e) { return { error: e }; } }); } ``` 以下代码显示`processArray()`正在运行: ```js 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`中提取值或仅提取错误: ```js 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]); ``` ##### 28.10.5.3。用例:映射到多个值 Array 方法`.map()`将每个输入 Array 元素映射到一个输出元素。但是如果我们想将它映射到多个输出元素呢? 这在以下示例中变得必要: * 输入:`['a', 'b', 'c']` * 输出:`['<span>a</span>', ', ', '<span>b</span>', ', ', '<span>c</span>']` 进行此转换的函数`wrap()`类似于您为前端库 React 编写的代码: ```js 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>'] ); ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:`.flatMap()`** * `exercises/arrays/convert_to_numbers_test.js` * `exercises/arrays/replace_objects_test.js` #### 28.10.6。 `.filter()`:只保留一些元素 Array 方法`.filter()`返回一个 Array,收集回调返回 truthy 值的所有元素。 例如: ```js > [-1, 2, 5, -7, 6].filter(x => x >= 0) [ 2, 5, 6 ] > ['a', 'b', 'c', 'd'].filter((_,i) => (i%2)===0) [ 'a', 'c' ] ``` `.filter()`可以实现如下: ```js 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']); ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:通过`.filter()`** 删除空行 `exercises/arrays/remove_empty_lines_filter_test.js` #### 28.10.7。 `.reduce()`:从数组中获取值(高级) 方法`.reduce()`是一个用于计算数组“摘要”的强大工具。摘要可以是任何类型的值: * 一个号码。例如,所有 Array 元素的总和。 * 数组。例如,Array 的副本,元素乘以 2。 * 等等。 此操作在函数式编程中也称为`foldl`(“向左折叠”),这种写法也非常的普遍。需要注意的是,它可能使代码难以理解。 `.reduce()`具有以下类型签名(在`Array<T>`内): ```js .reduce<U>( callback: (accumulator: U, element: T, index: number, array: T[]) => U, init?: U) : U ``` `T`是数组元素的类型,`U`是摘要的类型。两者可能有所不同,也可能没有。 `accumulator`只是“摘要”的另一个名称。 要计算数组`arr`的摘要,`.reduce()`将所有数组元素一次一个地提供给其回调: ```js 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()`只是以有用的方式调用它。 你可以说回调将数组元素折叠到累加器中。这就是为什么这个操作在函数式编程中称为“折叠”。 ##### 28.10.7.1。第一个例子 让我们看一下`.reduce()`的实例:函数`addAll()`计算数组`arr`中所有数字的总和。 ```js 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`调用: ```js callback(0, 1) --> 1 callback(1, 2) --> 3 callback(3, 3) --> 6 ``` 笔记: * 第一个参数是电流累加器(从`.reduce()`的参数`init`开始)。 * 第二个参数是当前的 Array 元素。 * 结果是下一个累加器。 * `callback`的最后结果也是`.reduce()`的结果。 或者,我们可以通过`for-of`循环实现`addAll()`: ```js function addAll(arr) { let sum = 0; for (const element of arr) { sum = sum + element; } return sum; } ``` 很难说这两个实现中的哪一个“更好”:基于`.reduce()`的实现更简洁,但如果你特别不熟悉函数式编程时,基于`for-of`的实现可能更容易理解。 ##### 28.10.7.2。示例:通过`.reduce()`查找索引 以下函数是 Array 方法`.indexOf()`的实现。它返回给定`searchValue`出现在 Array `arr`内​​的第一个索引: ```js 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`)。在这里,一旦我们找到了我们想要的东西,我们就不会做任何事情。 ##### 28.10.7.3。示例:加倍数组元素 函数`double(arr)`返回`inArr`的副本,其元素全部乘以 2: ```js function double(inArr) { return inArr.reduce( (outArr, element) => { outArr.push(element * 2); return outArr; }, []); } assert.deepEqual( double([1, 2, 3]), [2, 4, 6]); ``` 我们通过推入修改初始值`[]`。 `double()`的非破坏性,功能更强的版本如下: ```js 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]); ``` 这个版本更优雅,但也更慢并且使用更多内存。 ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:`.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` ### 28.11。 `.sort()`:排序数组 `.sort()`具有以下类型定义: ```js sort(compareFunc?: (a: T, b: T) => number): this ``` `.sort()`始终对元素的字符串表示进行排序。这些表示通过`<`进行比较。该运算符按字典顺序比较 _(第一个字符最重要)。您可以在比较数字时看到:_ ```js > [200, 3, 10].sort() [ 10, 200, 3 ] ``` 比较人类语言字符串时,您需要知道它们是根据它们的代码单元值(字符代码)进行比较的: ```js > ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort() [ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ] ``` 正如您所看到的,所有非重音大写字母都出现在所有重音字母之前,这些字母位于所有重音字母之前。如果要对人类语言进行适当的排序,请使用`Intl`, [JavaScript 国际化 API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) 。 最后,`.sort()`将 _排序到位_:它改变并返回其接收器: ```js > const arr = ['a', 'c', 'b']; > arr.sort() === arr true > arr [ 'a', 'b', 'c' ] ``` #### 28.11.1。自定义排序顺序 您可以通过参数`compareFunc`自定义排序顺序,该参数返回一个数字: * 否定如果`a < b` * 如果`a === b`为零 * 如果`a > b`为正 记住这些规则的提示:负数是 _小于_ 零(等)。 #### 28.11.2。排序数字 您可以使用以下辅助函数来比较数字: ```js 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]); ``` 以下是一个快速而肮脏的选择。它的缺点是它很神秘,存在数字溢出的风险: ```js > [200, 3, 10].sort((a,b) => a-b) [ 3, 10, 200 ] ``` #### 28.11.3。排序对象 如果要对对象进行排序,还需要使用比较函数。例如,以下代码显示了如何按年龄对对象进行排序。 ```js const arr = [ {age: 200}, {age: 3}, {age: 10} ]; assert.deepEqual( arr.sort((obj1, obj2) => obj1.age - obj2.age), [{ age: 3 }, { age: 10 }, { age: 200 }] ); ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:按名称排序对象** `exercises/arrays/sort_objects_test.js` ### 28.12。快速参考:`Array<T>` 传说: * `R`:方法不改变接收器(非破坏性)。 * `W`:方法改变接收器(破坏性)。 #### 28.12.1。 `new Array()` `new Array(n)`创建一个长度为`n`的数组,其中包含`n`孔: ```js // Trailing commas are always ignored. // Therefore: number of commas = number of holes assert.deepEqual(new Array(3), [,,,]); ``` `new Array()`创建一个空数组。但是,我建议总是使用`[]`。 #### 28.12.2。 `Array`的静态方法 * `Array.from<T>(iterable: Iterable<T> | ArrayLike<T>): T[]` <sup>[ES6]</sup> * `Array.from<T,U>(iterable: Iterable<T> | ArrayLike<T>, mapFunc: (v: T, k: number) => U, thisArg?: any): U[]` <sup>[ES6]</sup> 将可迭代或类似 Array 的对象转换为 Array。可选地,输入值可以在添加到输出数组之前通过`mapFunc`进行转换。 类数组对象具有`.length`和索引属性(粗略地,非负整数的字符串表示): ```js interface ArrayLike<T> { length: number; [n: number]: T; } ``` 例子: ```js > Array.from(new Set(['a', 'b'])) [ 'a', 'b' ] > Array.from({length: 2, 0:'a', 1:'b'}) [ 'a', 'b' ] ``` * `Array.of<T>(...items: T[]): T[]` <sup>[ES6]</sup> 这个静态方法主要用于`Array`和 Typed Arrays 的子类,它用作自定义数组字面量: ```js assert.equal( Uint8Array.of(1, 2, 3) instanceof Uint8Array, true); ``` #### 28.12.3。 `Array<T>.prototype`的方法 * `.concat(...items: Array<T[] | T>): T[]` <sup>[R,ES3]</sup> 返回一个新的 Array,它是接收器和所有`items`的串联。非数组参数被视为具有单个元素的数组。 ```js > ['a'].concat('b', ['c', 'd']) [ 'a', 'b', 'c', 'd' ] ``` * `.copyWithin(target: number, start: number, end=this.length): this` <sup>[W,ES6]</sup> 将索引范围从`start`到(excel。)`end`的元素复制到以`target`开头的索引。正确处理重叠。 ```js > ['a', 'b', 'c', 'd'].copyWithin(0, 2, 4) [ 'c', 'd', 'c', 'd' ] ``` * `.entries(): Iterable<[number, T]>` <sup>[R,ES6]</sup> 返回[index,element]对上的可迭代。 ```js > Array.from(['a', 'b'].entries()) [ [ 0, 'a' ], [ 1, 'b' ] ] ``` * `.every(callback: (value: T, index: number, array: Array<T>) => boolean, thisArg?: any): boolean` <sup>[R,ES5]</sup> 如果`callback`为每个元素和`false`返回`true`,则返回`true`,否则返回。收到`false`后立即停止。该方法对应于数学中的通用量化(对于所有,`∀`)。 ```js > [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` <sup>[W,ES6]</sup> 将`value`分配给(incl。)`start`和(excl。)`end`之间的每个索引。 ```js > [0, 1, 2].fill('a') [ 'a', 'a', 'a' ] ``` * `.filter(callback: (value: T, index: number, array: Array<T>) => any, thisArg?: any): T[]` <sup>[R,ES5]</sup> 返回一个只包含`callback`返回`true`的元素的数组。 ```js > [1, -2, 3].filter(x => x > 0) [ 1, 3 ] ``` * `.find(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): T | undefined` <sup>[R,ES6]</sup> 结果是`predicate`返回`true`的第一个元素。如果它永远不会,结果是`undefined`。 ```js > [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` <sup>[R,ES6]</sup> 结果是`predicate`返回`true`的第一个元素的索引。如果它永远不会,结果是`-1`。 ```js > [1, -2, 3].findIndex(x => x < 0) 1 > [1, 2, 3].findIndex(x => x < 0) -1 ``` * `.flat(depth = 1): any[]` <sup>[R,ES2019]</sup> “展平”数组:它创建数组的副本,其中嵌套数组中的值都出现在顶层。参数`depth`控制`.flat()`查找非数组值的深度。 ```js > [ 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[]` <sup>[R,ES2019]</sup> 通过为原始 Array 的每个元素调用`callback()`并连接它返回的 Arrays 来生成结果。 ```js > ['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` <sup>[R,ES5]</sup> 为每个元素调用`callback`。 ```js ['a', 'b'].forEach((x, i) => console.log(x, i)) // Output: // 'a', 0 // 'b', 1 ``` * `.includes(searchElement: T, fromIndex=0): boolean` <sup>[R,ES2016]</sup> 如果接收器具有值为`searchElement`和`false`的元素,则返回`true`。搜索从索引`fromIndex`开始。 ```js > [0, 1, 2].includes(1) true > [0, 1, 2].includes(5) false ``` * `.indexOf(searchElement: T, fromIndex=0): number` <sup>[R,ES5]</sup> 返回严格等于`searchElement`的第一个元素的索引。如果没有这样的元素,则返回`-1`。开始在索引`fromIndex`搜索,然后访问更高的索引。 ```js > ['a', 'b', 'a'].indexOf('a') 0 > ['a', 'b', 'a'].indexOf('a', 1) 2 > ['a', 'b', 'a'].indexOf('c') -1 ``` * `.join(separator = ','): string` <sup>[R,ES1]</sup> 通过连接所有元素的字符串表示形式创建一个字符串,用`separator`分隔它们。 ```js > ['a', 'b', 'c'].join('##') 'a##b##c' > ['a', 'b', 'c'].join() 'a,b,c' ``` * `.keys(): Iterable<number>` <sup>[R,ES6]</sup> 返回接收器上可迭代的键。 ```js > [...['a', 'b'].keys()] [ 0, 1 ] ``` * `.lastIndexOf(searchElement: T, fromIndex=this.length-1): number` <sup>[R,ES5]</sup> 返回严格等于`searchElement`的最后一个元素的索引。如果没有这样的元素,则返回`-1`。开始在索引`fromIndex`搜索,然后访问较低的索引。 ```js > ['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[]` <sup>[R,ES5]</sup> 返回一个新的 Array,其中每个元素都是`mapFunc`应用于接收器的相应元素的结果。 ```js > [1, 2, 3].map(x => x * 2) [ 2, 4, 6 ] > ['a', 'b', 'c'].map((x, i) => i) [ 0, 1, 2 ] ``` * `.pop(): T | undefined` <sup>[W,ES3]</sup> 删除并返回接收器的最后一个元素。也就是说,它将接收器的末尾视为堆栈。与`.push()`相反。 ```js > const arr = ['a', 'b', 'c']; > arr.pop() 'c' > arr [ 'a', 'b' ] ``` * `.push(...items: T[]): number` <sup>[W,ES3]</sup> 在接收器的末尾添加零个或多个`items`。也就是说,它将接收器的末尾视为堆栈。返回值是更改后接收器的长度。与`.pop()`相反。 ```js > 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` <sup>[R,ES5]</sup> 此方法生成接收器的摘要:它将所有 Array 元素提供给`callback`,它将当前中间结果(在参数`accumulator`中)与当前 Array 元素组合并返回下一个`accumulator`: ```js 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 元素。 ```js > [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` <sup>[R,ES5]</sup> 像`.reduce()`一样工作,但是从最后一个元素开始向后访问 Array 元素。 ```js > [1, 2, 3].reduceRight((accu, x) => accu + String(x), '') '321' ``` * `.reverse(): this` <sup>[W,ES1]</sup> 重新排列接收器的元素,使它们的顺序相反,然后返回接收器。 ```js > const arr = ['a', 'b', 'c']; > arr.reverse() [ 'c', 'b', 'a' ] > arr [ 'c', 'b', 'a' ] ``` * `.shift(): T | undefined` <sup>[W,ES3]</sup> 删除并返回接收器的第一个元素。与`.unshift()`相反。 ```js > const arr = ['a', 'b', 'c']; > arr.shift() 'a' > arr [ 'b', 'c' ] ``` * `.slice(start=0, end=this.length): T[]` <sup>[R,ES3]</sup> 返回一个新的 Array,包含接收器的元素,其索引在(incl。)`start`和(excl。)`end`之间。 ```js > ['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` <sup>[R,ES5]</sup> 如果`callback`为至少一个元素返回`true`,则返回`true`,否则返回`false`。收到`true`后立即停止。该方法对应于数学中的存在量化(存在,`∃`)。 ```js > [1, 2, 3].some(x => x < 0) false > [1, -2, 3].some(x => x < 0) true ``` * `.sort(compareFunc?: (a: T, b: T) => number): this` <sup>[W,ES1]</sup> 对接收器进行排序并将其返回。从 ECMAScript 2019 开始,保证排序是稳定的:如果通过排序认为元素相等,那么排序不会改变这些元素的顺序(相对于彼此)。 默认情况下,它对元素的字符串表示进行排序。它按字典顺序并根据字符的代码单元值(字符代码)执行: ```js > ['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`为正 排序数字的伎俩(有数字溢出的风险): ```js > [200, 3, 10].sort((a, b) => a - b) [ 3, 10, 200 ] ``` * `.splice(start: number, deleteCount=this.length-start, ...items: T[]): T[]` <sup>[W,ES3]</sup> 在索引`start`处,它删除`deleteCount`元素并插入`items`。它返回已删除的元素。 ```js > const arr = ['a', 'b', 'c', 'd']; > arr.splice(1, 2, 'x', 'y') [ 'b', 'c' ] > arr [ 'a', 'x', 'y', 'd' ] ``` * `.toString(): string` <sup>[R,ES1]</sup> 返回一个字符串,其中包含所有元素的字符串,以逗号分隔。 ```js > [1, 2, 3].toString() '1,2,3' > ['a', 'b', 'c'].toString() 'a,b,c' > [].toString() '' ``` * `.unshift(...items: T[]): number` <sup>[W,ES3]</sup> 在接收器的开头插入`items`并在此修改后返回其长度。 ```js > const arr = ['c', 'd']; > arr.unshift('e', 'f') 4 > arr [ 'e', 'f', 'c', 'd' ] ``` * `.values(): Iterable<T>` <sup>[R,ES6]</sup> 返回接收器上可迭代的值。 ```js > [...['a', 'b'].values()] [ 'a', 'b' ] ``` #### 28.12.4。来源 * [TypeScript 的内置类型](https://github.com/Microsoft/TypeScript/blob/master/lib/) * [适用于 JavaScript 的 MDN 网络文档](https://developer.mozilla.org/en-US/docs/Web/JavaScript) * [ECMAScript 语言规范](https://tc39.github.io/ecma262/) ![](https://img.kancloud.cn/ff/a8/ffa8e16628cad59b09c786b836722faa.svg) **测验** 参见[测验应用程序](/docs/11.md#91测验)。