## 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' ]
```
 **練習:通過`.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 中。因此,您只能使用它來提取后綴。
 **練習:通過數組實現隊列**
`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']);
```
 **練習:通過`.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>']
);
```
 **練習:`.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']);
```
 **練習:通過`.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]);
```
這個版本更優雅,但也更慢并且使用更多內存。
 **練習:`.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 }] );
```
 **練習:按名稱排序對象**
`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/)
 **測驗**
參見[測驗應用程序](/docs/11.md#91測驗)。
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?