18. 新陣列功能
目錄
請支持這本書:購買它(PDF、EPUB、MOBI)捐款
(廣告,請不要封鎖。)

18. 新陣列功能



18.1 概觀

新的靜態Array方法

新的Array.prototype方法

18.2 新的靜態Array方法

Array物件有新的方法。

18.2.1 Array.from(arrayLike, mapFunc?, thisArg?)

Array.from()的基本功能是將兩種值轉換成陣列

以下是將類陣列物件轉換為陣列的範例

const arrayLike = { length: 2, 0: 'a', 1: 'b' };

// for-of only works with iterable values
for (const x of arrayLike) { // TypeError
    console.log(x);
}

const arr = Array.from(arrayLike);
for (const x of arr) { // OK, iterable
    console.log(x);
}
// Output:
// a
// b
18.2.1.1 透過 Array.from() 進行對應

Array.from() 也是使用 map() 泛型 的便利替代方案

const spans = document.querySelectorAll('span.name');

// map(), generically:
const names1 = Array.prototype.map.call(spans, s => s.textContent);

// Array.from():
const names2 = Array.from(spans, s => s.textContent);

在此範例中,document.querySelectorAll() 的結果再次是類陣列物件,而不是陣列,這就是我們無法在其上呼叫 map() 的原因。先前,我們將類陣列物件轉換為陣列,以便呼叫 forEach()。在此,我們透過泛型方法呼叫和 Array.from() 的兩個參數版本略過中間步驟。

18.2.1.2 Array 子類別中的 from()

Array.from() 的另一個使用案例是將類陣列或可迭代值轉換為 Array 子類別的執行個體。例如,如果您建立 Array 的子類別 MyArray,並想要將此類物件轉換為 MyArray 的執行個體,您只需使用 MyArray.from()。之所以可行,是因為建構函式在 ECMAScript 6 中會彼此繼承(超級建構函式是其子建構函式的原型)。

class MyArray extends Array {
    ···
}
const instanceOfMyArray = MyArray.from(anIterable);

您也可以將此功能與對應結合,以取得您控制結果建構函式的對應操作

// from() – determine the result’s constructor via the receiver
// (in this case, MyArray)
const instanceOfMyArray = MyArray.from([1, 2, 3], x => x * x);

// map(): the result is always an instance of Array
const instanceOfArray   = [1, 2, 3].map(x => x * x);

種類模式讓您可以設定非靜態內建方法(例如 slice()filter()map())傳回的執行個體。它在「類別」章節的「種類模式」節中說明。

18.2.2 Array.of(...items)

Array.of(item_0, item_1, ···) 會建立一個元素為 item_0item_1 等的陣列。

18.2.2.1 Array.of() 作為 Array 子類別的陣列文字

如果您想要將幾個值轉換為陣列,您應該總是使用陣列文字,特別是如果有一個值是數字,陣列建構函式無法正常運作(有關此怪癖的更多資訊

> new Array(3, 11, 8)
[ 3, 11, 8 ]
> new Array(3)
[ , ,  ,]
> new Array(3.1)
RangeError: Invalid array length

但是,您應該如何將值轉換為 Array 的子建構函式的執行個體呢?這時 Array.of() 就派上用場了(請記住,Array 的子建構函式會繼承 Array 的所有方法,包括 of())。

class MyArray extends Array {
    ···
}
console.log(MyArray.of(3, 11, 8) instanceof MyArray); // true
console.log(MyArray.of(3).length === 1); // true

18.3 新的 Array.prototype 方法

陣列實例有幾個新的方法可用。

18.3.1 迭代陣列

下列方法有助於迭代陣列

上述每個方法的結果都是一個值序列,但它們不會作為陣列傳回;它們會透過一個迭代器逐一顯示。我們來看一個範例。我使用 Array.from() 將迭代器的內容放入陣列中

> Array.from(['a', 'b'].keys())
[ 0, 1 ]
> Array.from(['a', 'b'].values())
[ 'a', 'b' ]
> Array.from(['a', 'b'].entries())
[ [ 0, 'a' ],
  [ 1, 'b' ] ]

我也可以使用 展開運算子 (...) 將迭代器轉換為陣列

> [...['a', 'b'].keys()]
[ 0, 1 ]
18.3.1.1 迭代 [index, element] 成對

您可以將 entries() 與 ECMAScript 6 的 for-of 迴圈和解構結合,以方便地迭代 [index, element] 成對

for (const [index, element] of ['a', 'b'].entries()) {
    console.log(index, element);
}

18.3.2 搜尋陣列元素

Array.prototype.find(predicate, thisArg?)
傳回第一個陣列元素,其中呼叫回函 predicate 傳回 true。如果沒有這樣的元素,它會傳回 undefined。範例

> [6, -5, 8].find(x => x < 0)
-5
> [6, 5, 8].find(x => x < 0)
undefined

Array.prototype.findIndex(predicate, thisArg?)
傳回第一個元素的索引,其中呼叫回函 predicate 傳回 true。如果沒有這樣的元素,它會傳回 -1。範例

> [6, -5, 8].findIndex(x => x < 0)
1
> [6, 5, 8].findIndex(x => x < 0)
-1

呼叫回函 predicate 的完整簽章是

predicate(element, index, array)
18.3.2.1 透過 findIndex() 尋找 NaN

Array.prototype.indexOf() 的一個眾所周知的 限制 是它找不到 NaN,因為它會透過 === 搜尋元素

> [NaN].indexOf(NaN)
-1

使用 findIndex(),您可以使用 Object.is()(在 OOP 章節 中說明),並且不會有這樣的問題

> [NaN].findIndex(y => Object.is(NaN, y))
0

您也可以採用更通用的方法,建立一個輔助函式 elemIs()

> function elemIs(x) { return Object.is.bind(Object, x) }
> [NaN].findIndex(elemIs(NaN))
0

18.3.3 Array.prototype.copyWithin()

此方法的簽章是

Array.prototype.copyWithin(target : number,
    start : number, end = this.length) : This

它會將索引在範圍 [start,end) 內的元素複製到索引 target 及後續索引。如果兩個索引範圍重疊,則會小心處理,以確保所有來源元素在覆寫之前都已複製。

範例

> const arr = [0,1,2,3];
> arr.copyWithin(2, 0, 2)
[ 0, 1, 0, 1 ]
> arr
[ 0, 1, 0, 1 ]

18.3.4 Array.prototype.fill()

此方法的簽章是

Array.prototype.fill(value : any, start=0, end=this.length) : This

它會使用提供的 value 填滿陣列

> const arr = ['a', 'b', 'c'];
> arr.fill(7)
[ 7, 7, 7 ]
> arr
[ 7, 7, 7 ]

選擇性地,你可以限制填滿的起始和結束位置

> ['a', 'b', 'c'].fill(7, 1, 2)
[ 'a', 7, 'c' ]

18.4 ES6 和陣列中的洞

洞是陣列「內部」的索引,沒有關聯的元素。換句話說:如果陣列 arr 在索引 i 處有一個洞,則

例如:以下陣列在索引 1 處有一個洞。

> const arr = ['a',,'b']
'use strict'
> 0 in arr
true
> 1 in arr
false
> 2 in arr
true
> arr[1]
undefined

你會在本章節中看到許多包含洞的範例。如果任何地方不清楚,你可以參閱「Speaking JavaScript」中的「陣列中的洞」章節,以取得更多資訊。

18.4.1 ECMAScript 6 將洞視為 undefined 元素

ES6 中陣列方法的一般規則是:每個洞都視為元素 undefined。範例

> Array.from(['a',,'b'])
[ 'a', undefined, 'b' ]
> [,'a'].findIndex(x => x === undefined)
0
> [...[,'a'].entries()]
[ [ 0, undefined ], [ 1, 'a' ] ]

這個想法是引導人們遠離洞,並從長遠來看簡化。不幸的是,這表示事情現在更加不一致。

18.4.2 陣列操作和洞

18.4.2.1 反覆運算

Array.prototype[Symbol.iterator] 建立的迭代器將每個洞視為元素 undefined。例如,以下迭代器 iter

> var arr = [, 'a'];
> var iter = arr[Symbol.iterator]();

如果我們呼叫 next() 兩次,我們會取得索引 0 處的洞和索引 1 處的元素 'a'。如你所見,前者會產生 undefined

> iter.next()
{ value: undefined, done: false }
> iter.next()
{ value: 'a', done: false }

在其他方法中,有兩個操作是基於 反覆運算協定。因此,這些操作也會將洞視為 undefined 元素。

首先,展開運算子 (...)

> [...[, 'a']]
[ undefined, 'a' ]

其次,for-of 迴圈

for (const x of [, 'a']) {
  console.log(x);
}
// Output:
// undefined
// a

請注意,陣列原型方法 (filter() 等) 沒有使用反覆運算協定。

18.4.2.2 Array.from()

如果其參數是可迭代的,Array.from() 使用迭代將其轉換為陣列。然後它會完全像展開運算子一樣運作

> Array.from([, 'a'])
[ undefined, 'a' ]

但是 Array.from() 也可以將 類陣列物件 轉換為陣列。然後洞也會變成 undefined

> Array.from({1: 'a', length: 2})
[ undefined, 'a' ]

使用第二個參數時,Array.from() 的運作方式與 Array.prototype.map() 幾乎相同。

然而,Array.from() 將洞視為 undefined

> Array.from([,'a'], x => x)
[ undefined, 'a' ]
> Array.from([,'a'], (x,i) => i)
[ 0, 1 ]

Array.prototype.map() 會略過它們,但會保留它們

> [,'a'].map(x => x)
[ , 'a' ]
> [,'a'].map((x,i) => i)
[ , 1 ]
18.4.2.3 Array.prototype 方法

在 ECMAScript 5 中,行為已經略有不同。例如

ECMAScript 6 加入新的行為類型

下表說明 Array.prototype 方法如何處理洞。

方法  
concat 保留 ['a',,'b'].concat(['c',,'d']) → ['a',,'b','c',,'d']
copyWithinES6 保留 [,'a','b',,].copyWithin(2,0) → [,'a',,'a']
entriesES6 元素 [...[,'a'].entries()] → [[0,undefined], [1,'a']]
every 忽略 [,'a'].every(x => x==='a') → true
fillES6 填滿 new Array(3).fill('a') → ['a','a','a']
filter 移除 ['a',,'b'].filter(x => true) → ['a','b']
findES6 元素 [,'a'].find(x => true) → undefined
findIndexES6 元素 [,'a'].findIndex(x => true) → 0
forEach 忽略 [,'a'].forEach((x,i) => log(i)); → 1
indexOf 忽略 [,'a'].indexOf(undefined) → -1
join 元素 [,'a',undefined,null].join('#') → '#a##'
keysES6 元素 [...[,'a'].keys()] → [0,1]
lastIndexOf 忽略 [,'a'].lastIndexOf(undefined) → -1
map 保留 [,'a'].map(x => 1) → [,1]
pop 元素 ['a',,].pop() → undefined
push 保留 new Array(1).push('a') → 2
reduce 忽略 ['#',,undefined].reduce((x,y)=>x+y) → '#undefined'
reduceRight 忽略 ['#',,undefined].reduceRight((x,y)=>x+y) → 'undefined#'
reverse 保留 ['a',,'b'].reverse() → ['b',,'a']
shift 元素 [,'a'].shift() → undefined
slice 保留 [,'a'].slice(0,1) → [,]
some 忽略 [,'a'].some(x => x !== 'a') → false
sort 保留 [,undefined,'a'].sort() → ['a',undefined,,]
splice 保留 ['a',,].splice(1,1) → [,]
toString 元素 [,'a',undefined,null].toString() → ',a,,'
unshift 保留 [,'a'].unshift('b') → 3
valuesES6 元素 [...[,'a'].values()] → [undefined,'a']

注意事項

18.4.3 建立填入值的陣列

ES6 新運算將空洞視為 undefined 元素,有助於建立填入值的陣列。

18.4.3.1 填入固定值

Array.prototype.fill() 會將所有陣列元素(包括空洞)替換為固定值

> new Array(3).fill(7)
[ 7, 7, 7 ]

new Array(3) 會建立一個有三個空洞的陣列,而 fill() 會將每個空洞替換為值 7

18.4.3.2 填入遞增數字

Array.prototype.keys() 會回報金鑰,即使陣列只有空洞。它會回傳一個可迭代物件,你可以透過展開運算子將其轉換為陣列

> [...new Array(3).keys()]
[ 0, 1, 2 ]
18.4.3.3 填入計算值

Array.from() 第二個參數中的對應函式會收到空洞的通知。因此,你可以使用 Array.from() 進行更複雜的填入

> Array.from(new Array(5), (x,i) => i*2)
[ 0, 2, 4, 6, 8 ]
18.4.3.4 填入 undefined

如果你需要一個填入 undefined 的陣列,你可以利用迭代(由展開運算子觸發)會將空洞轉換為 undefined 的事實

> [...new Array(3)]
[ undefined, undefined, undefined ]

18.4.4 從陣列中移除空洞

ES5 方法 filter() 讓你移除空洞

> ['a',,'c'].filter(() => true)
[ 'a', 'c' ]

ES6 迭代(透過展開運算子觸發)讓你將空洞轉換成 undefined 元素

> [...['a',,'c']]
[ 'a', undefined, 'c' ]

18.5 設定哪些物件會被 concat() 展開(Symbol.isConcatSpreadable

你可以透過新增一個(自有或繼承的)屬性,其鍵值為眾所周知的符號 Symbol.isConcatSpreadable,且其值為布林值,來設定 Array.prototype.concat() 如何處理物件。

18.5.1 陣列的預設值:展開

預設情況下,Array.prototype.concat() 會將陣列展開到其結果中:陣列的索引元素會成為結果的元素

const arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e');
    // ['a', 'b', 'c', 'd', 'e']

使用 Symbol.isConcatSpreadable,你可以覆寫預設值並避免展開陣列

const arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e');
    // ['a', 'b', ['c','d'], 'e']

18.5.2 非陣列的預設值:不展開

對於非陣列,預設值是不展開

const arrayLike = {length: 2, 0: 'c', 1: 'd'};

console.log(['a', 'b'].concat(arrayLike, 'e'));
    // ['a', 'b', arrayLike, 'e']

console.log(Array.prototype.concat.call(
    arrayLike, ['e','f'], 'g'));
    // [arrayLike, 'e', 'f', 'g']

你可以使用 Symbol.isConcatSpreadable 來強制展開

arrayLike[Symbol.isConcatSpreadable] = true;

console.log(['a', 'b'].concat(arrayLike, 'e'));
    // ['a', 'b', 'c', 'd', 'e']

console.log(Array.prototype.concat.call(
    arrayLike, ['e','f'], 'g'));
    // ['c', 'd', 'e', 'f', 'g']

18.5.3 偵測陣列

concat() 如何判斷參數是否為陣列?它使用與 Array.isArray() 相同的演算法。Array.prototype 是否在原型鏈中,對該演算法沒有影響。這一點很重要,因為在 ES5 及更早版本中,駭客手法用於子類化 Array,而這些手法必須繼續運作(請參閱本書中關於 __proto__ 的章節

> const arr = [];
> Array.isArray(arr)
true

> Object.setPrototypeOf(arr, null);
> Array.isArray(arr)
true

18.5.4 標準函式庫中的 Symbol.isConcatSpreadable

ES6 標準函式庫中沒有任何物件具有鍵值為 Symbol.isConcatSpreadable 的屬性。因此,此機制純粹存在於瀏覽器 API 和使用者程式碼中。

後果

18.6 陣列索引的數字範圍

對於陣列,ES6 仍然有 與 ES5 相同的規則

字串和 Typed Array 有較大的索引範圍:0 ≤ i < 253−1。該範圍的上限是因為 253−1 是 JavaScript 浮點數可以安全表示的最大整數。有關詳細資訊,請參閱章節「安全整數」。

一般陣列較小索引範圍的唯一原因是向後相容性。

下一篇:19. Map 和 Set