陣列提供了許多方法。為了讓事情更簡單,在本章中,它們被分組。
新增/移除項目
我們已經知道從頭或尾新增和移除項目的方法
arr.push(...items)
– 將項目新增到尾端,arr.pop()
– 從尾端取出一個項目,arr.shift()
– 從頭端取出一個項目,arr.unshift(...items)
– 將項目新增到頭端。
以下是其他一些方法。
splice
如何從陣列中刪除元素?
陣列是物件,所以我們可以嘗試使用 delete
let arr = ["I", "go", "home"];
delete arr[1]; // remove "go"
alert( arr[1] ); // undefined
// now arr = ["I", , "home"];
alert( arr.length ); // 3
元素已移除,但陣列仍有 3 個元素,我們可以看到 arr.length == 3
。
這是很自然的,因為 delete obj.key
會透過 key
移除值。這就是它所做的。對於物件來說很好。但對於陣列,我們通常希望其餘元素移動並佔用釋放的位置。我們期望現在有一個較短的陣列。
因此,應使用特殊方法。
arr.splice 方法是陣列的瑞士刀。它可以執行所有操作:插入、移除和取代元素。
語法為
arr.splice(start[, deleteCount, elem1, ..., elemN])
它從索引 start
開始修改 arr
:移除 deleteCount
個元素,然後在它們的位置插入 elem1, ..., elemN
。傳回已移除元素的陣列。
透過範例可以輕鬆掌握此方法。
讓我們從刪除開始
let arr = ["I", "study", "JavaScript"];
arr.splice(1, 1); // from index 1 remove 1 element
alert( arr ); // ["I", "JavaScript"]
簡單,對吧?從索引 1
開始,它移除 1
個元素。
在以下範例中,我們移除 3 個元素,並用其他兩個元素取代它們
let arr = ["I", "study", "JavaScript", "right", "now"];
// remove 3 first elements and replace them with another
arr.splice(0, 3, "Let's", "dance");
alert( arr ) // now ["Let's", "dance", "right", "now"]
在這裡,我們可以看到 splice
傳回已移除元素的陣列
let arr = ["I", "study", "JavaScript", "right", "now"];
// remove 2 first elements
let removed = arr.splice(0, 2);
alert( removed ); // "I", "study" <-- array of removed elements
splice
方法也可以在不移除任何元素的情況下插入元素。為此,我們需要將 deleteCount
設定為 0
let arr = ["I", "study", "JavaScript"];
// from index 2
// delete 0
// then insert "complex" and "language"
arr.splice(2, 0, "complex", "language");
alert( arr ); // "I", "study", "complex", "language", "JavaScript"
在這裡和其他陣列方法中,允許使用負索引。它們指定從陣列尾端的位置,如下所示
let arr = [1, 2, 5];
// from index -1 (one step from the end)
// delete 0 elements,
// then insert 3 and 4
arr.splice(-1, 0, 3, 4);
alert( arr ); // 1,2,3,4,5
slice
arr.slice 方法比外觀相似的 arr.splice
簡單得多。
語法為
arr.slice([start], [end])
它傳回一個新陣列,將所有項目從索引 start
複製到 end
(不包含 end
)。start
和 end
兩個都可以是負數,在這種情況下,假設位置從陣列尾端開始。
它類似於字串方法 str.slice
,但它不是建立子字串,而是建立子陣列。
例如
let arr = ["t", "e", "s", "t"];
alert( arr.slice(1, 3) ); // e,s (copy from 1 to 3)
alert( arr.slice(-2) ); // s,t (copy from -2 till the end)
我們也可以在沒有參數的情況下呼叫它:arr.slice()
會建立 arr
的副本。這通常用於取得副本,以進行進一步的轉換,而這些轉換不應影響原始陣列。
concat
arr.concat 方法會建立一個新陣列,其中包含來自其他陣列和附加項目的值。
語法為
arr.concat(arg1, arg2...)
它接受任何數量的參數,無論是陣列或值。
結果是一個新陣列,其中包含來自 arr
、arg1
、arg2
等的項目。
如果參數 argN
是陣列,則會複製其所有元素。否則,複製參數本身。
例如
let arr = [1, 2];
// create an array from: arr and [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4
// create an array from: arr and [3,4] and [5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6
// create an array from: arr and [3,4], then add values 5 and 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6
通常,它只複製陣列中的元素。其他物件,即使看起來像陣列,也會整體加入
let arr = [1, 2];
let arrayLike = {
0: "something",
length: 1
};
alert( arr.concat(arrayLike) ); // 1,2,[object Object]
…但如果類陣列物件有特殊 Symbol.isConcatSpreadable
屬性,則 concat
會將其視為陣列:加入其元素
let arr = [1, 2];
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
alert( arr.concat(arrayLike) ); // 1,2,something,else
反覆運算:forEach
arr.forEach 方法允許對陣列的每個元素執行函式。
語法
arr.forEach(function(item, index, array) {
// ... do something with an item
});
例如,這會顯示陣列的每個元素
// for each element call alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);
而這段程式碼會更詳細說明其在目標陣列中的位置
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
函式的結果(如果有回傳的話)會被丟棄並忽略。
在陣列中搜尋
現在讓我們介紹在陣列中搜尋的方法。
indexOf/lastIndexOf 和 includes
arr.indexOf 和 arr.includes 方法有類似的語法,本質上與其字串對應項相同,但操作的是項目而不是字元
arr.indexOf(item, from)
– 從索引from
開始尋找item
,並回傳找到的索引,否則為-1
。arr.includes(item, from)
– 從索引from
開始尋找item
,如果找到則回傳true
。
通常,這些方法只使用一個參數:要搜尋的 item
。預設從頭開始搜尋。
例如
let arr = [1, 0, false];
alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1
alert( arr.includes(1) ); // true
請注意,indexOf
使用嚴格相等 ===
進行比較。因此,如果我們尋找 false
,它只會找到 false
,而不是零。
如果我們想檢查 item
是否存在於陣列中,不需要索引,則建議使用 arr.includes
。
arr.lastIndexOf 方法與 indexOf
相同,但從右到左尋找。
let fruits = ['Apple', 'Orange', 'Apple']
alert( fruits.indexOf('Apple') ); // 0 (first Apple)
alert( fruits.lastIndexOf('Apple') ); // 2 (last Apple)
includes
方法正確處理 NaN
includes
的一個小但值得注意的功能是它正確處理 NaN
,這與 indexOf
不同
const arr = [NaN];
alert( arr.indexOf(NaN) ); // -1 (wrong, should be 0)
alert( arr.includes(NaN) );// true (correct)
這是因為 includes
是在很久之後才加入 JavaScript,並在內部使用較新的比較演算法。
find 和 findIndex/findLastIndex
想像我們有一個物件陣列。我們如何找到符合特定條件的物件?
這裡 arr.find(fn) 方法派上用場。
語法為
let result = arr.find(function(item, index, array) {
// if true is returned, item is returned and iteration is stopped
// for falsy scenario returns undefined
});
這個函式會對陣列中的元素逐一呼叫
item
是元素。index
是它的索引。array
是陣列本身。
如果它傳回 true
,搜尋就會停止,並傳回 item
。如果什麼都沒找到,就會傳回 undefined
。
例如,我們有一個使用者陣列,每個使用者都有 id
和 name
欄位。我們來找出 id == 1
的使用者
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
let user = users.find(item => item.id == 1);
alert(user.name); // John
在現實生活中,物件陣列是很常見的,所以 find
方法非常有用。
請注意,在這個範例中,我們提供給 find
的函式 item => item.id == 1
有一個參數。這是很典型的,這個函式的其他參數很少使用。
arr.findIndex
方法有相同的語法,但傳回找到元素的索引,而不是元素本身。如果什麼都沒找到,就會傳回 -1
。
arr.findLastIndex
方法類似於 findIndex
,但從右到左搜尋,類似於 lastIndexOf
。
以下是一個範例
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"},
{id: 4, name: "John"}
];
// Find the index of the first John
alert(users.findIndex(user => user.name == 'John')); // 0
// Find the index of the last John
alert(users.findLastIndex(user => user.name == 'John')); // 3
filter
find
方法會尋找單一(第一個)讓函式傳回 true
的元素。
如果可能有很多,我們可以使用 arr.filter(fn)。
語法類似於 find
,但 filter
會傳回一個包含所有符合元素的陣列
let results = arr.filter(function(item, index, array) {
// if true item is pushed to results and the iteration continues
// returns empty array if nothing found
});
例如
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"}
];
// returns array of the first two users
let someUsers = users.filter(item => item.id < 3);
alert(someUsers.length); // 2
轉換陣列
讓我們繼續探討轉換和重新排列陣列的方法。
map
arr.map
方法是最有用且最常使用的其中一個方法。
它會對陣列中的每個元素呼叫函式,並傳回結果陣列。
語法為
let result = arr.map(function(item, index, array) {
// returns the new value instead of item
});
例如,我們在這裡將每個元素轉換為其長度
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6
sort(fn)
呼叫 arr.sort() 會對陣列進行原地排序,改變其元素順序。
它也會傳回已排序的陣列,但傳回的值通常會被忽略,因為 arr
本身已修改。
例如
let arr = [ 1, 2, 15 ];
// the method reorders the content of arr
arr.sort();
alert( arr ); // 1, 15, 2
你注意到結果中有什麼奇怪的地方嗎?
順序變成 1, 15, 2
。不正確。但為什麼?
預設會將項目當成字串排序。
基本上,所有元素都會轉換為字串進行比較。對於字串,會套用字典順序,而 "2" > "15"
確實成立。
若要使用我們自己的排序順序,我們需要提供一個函式作為 arr.sort()
的參數。
這個函式應該比較兩個任意值,並傳回
function compare(a, b) {
if (a > b) return 1; // if the first value is greater than the second
if (a == b) return 0; // if values are equal
if (a < b) return -1; // if the first value is less than the second
}
例如,要當成數字排序
function compareNumeric(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
let arr = [ 1, 2, 15 ];
arr.sort(compareNumeric);
alert(arr); // 1, 2, 15
現在它會按照預期運作。
讓我們暫停一下,想想正在發生什麼事。arr
可以是任何東西的陣列,對吧?它可能包含數字、字串、物件或其他任何東西。我們有一組某些項目。要對它排序,我們需要一個排序函式,知道如何比較其元素。預設是字串順序。
arr.sort(fn)
方法實作了一種通用排序演算法。我們不需要關心它在內部如何運作(大部分時間都是最佳化的 快速排序 或 Timsort)。它會遍歷陣列,使用提供的函式比較其元素並重新排列它們,我們只需要提供執行比較的 fn
即可。
順帶一提,如果我們想要知道哪些元素被比較,沒有什麼可以阻止我們警告它們
[1, -2, 15, 2, 0, 8].sort(function(a, b) {
alert( a + " <> " + b );
return a - b;
});
演算法可能會在過程中將一個元素與多個其他元素比較,但它會盡量減少比較的次數。
實際上,比較函式只需要傳回正數表示「大於」和負數表示「小於」。
這允許編寫更簡短的函式
let arr = [ 1, 2, 15 ];
arr.sort(function(a, b) { return a - b; });
alert(arr); // 1, 2, 15
localeCompare
還記得 字串 比較演算法嗎?它預設會根據字母的代碼來比較字母。
對於許多字母表,最好使用 str.localeCompare
方法來正確排序字母,例如 Ö
。
例如,讓我們用德語對幾個國家進行排序
let countries = ['Österreich', 'Andorra', 'Vietnam'];
alert( countries.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (wrong)
alert( countries.sort( (a, b) => a.localeCompare(b) ) ); // Andorra,Österreich,Vietnam (correct!)
反轉
方法 arr.reverse 會反轉 arr
中元素的順序。
例如
let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert( arr ); // 5,4,3,2,1
它也會在反轉後傳回陣列 arr
。
分割和合併
以下是一個真實生活中的情況。我們正在撰寫一個訊息應用程式,而使用者輸入了以逗號分隔的收件者清單:John, Pete, Mary
。但對我們來說,一個名稱陣列會比一個單一字串來得更方便。要如何取得它?
方法 str.split(delim) 正好可以做到這一點。它會根據給定的分隔符號 delim
將字串分割成一個陣列。
在以下範例中,我們根據逗號後接空格來分割
let names = 'Bilbo, Gandalf, Nazgul';
let arr = names.split(', ');
for (let name of arr) {
alert( `A message to ${name}.` ); // A message to Bilbo (and other names)
}
split
方法有一個可選的第二個數字引數,也就是陣列長度的限制。如果提供了這個引數,則會忽略額外的元素。不過在實務上很少使用它
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);
alert(arr); // Bilbo, Gandalf
使用一個空的 s
來呼叫 split(s)
會將字串分割成一個字母陣列
let str = "test";
alert( str.split('') ); // t,e,s,t
呼叫 arr.join(glue) 會對 split
執行相反的動作。它會建立一個 arr
項目字串,並在它們之間加入 glue
。
例如
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';'); // glue the array into a string using ;
alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight
當我們需要遍歷陣列時,可以使用 forEach
、for
或 for..of
。
當我們需要遍歷並傳回每個元素的資料時,可以使用 map
。
方法 arr.reduce 和 arr.reduceRight 也屬於此類,但稍微複雜一些。它們用於根據陣列計算單一值。
語法為
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
此函式會逐一套用至所有陣列元素,並將其結果「傳遞」至下一次呼叫。
引數
accumulator
– 是前一次函式呼叫的結果,第一次等於initial
(如果提供了initial
)。item
– 是目前的陣列項目。index
– 是其位置。array
– 是陣列。
當套用函式時,前一次函式呼叫的結果會作為第一個引數傳遞至下一次呼叫。
因此,第一個引數基本上是累加器,用於儲存所有前一次執行結果的組合結果。最後,它會成為 reduce
的結果。
聽起來很複雜嗎?
最簡單的理解方式就是透過範例。
以下是在一行中取得陣列總和
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
傳遞至 reduce
的函式只使用 2 個引數,這通常就足夠了。
讓我們看看發生了什麼事。
- 在第一次執行時,
sum
是initial
值(reduce
的最後一個引數),等於0
,而current
是第一個陣列元素,等於1
。因此函式結果為1
。 - 在第二次執行時,
sum = 1
,我們將第二個陣列元素(2
)加到其中並傳回。 - 在第三次執行時,
sum = 3
,我們再將一個元素加到其中,以此類推…
計算流程
或以表格形式表示,其中每一列代表對下一個陣列元素的函式呼叫
sum |
current |
result | |
---|---|---|---|
第一次呼叫 | 0 |
1 |
1 |
第二次呼叫 | 1 |
2 |
3 |
第三次呼叫 | 3 |
3 |
6 |
第四次呼叫 | 6 |
4 |
10 |
第五次呼叫 | 10 |
5 |
15 |
在這裡,我們可以清楚地看到前一個呼叫的結果如何成為下一個呼叫的第一個參數。
我們也可以省略初始值
let arr = [1, 2, 3, 4, 5];
// removed initial value from reduce (no 0)
let result = arr.reduce((sum, current) => sum + current);
alert( result ); // 15
結果是一樣的。這是因為如果沒有初始值,則 reduce
會將陣列的第一個元素作為初始值,並從第二個元素開始迭代。
計算表與上述相同,減去第一行。
但這種用法需要極度小心。如果陣列為空,則沒有初始值的 reduce
呼叫會產生錯誤。
以下是一個範例
let arr = [];
// Error: Reduce of empty array with no initial value
// if the initial value existed, reduce would return it for the empty arr.
arr.reduce((sum, current) => sum + current);
因此建議總是指定初始值。
方法 arr.reduceRight 執行相同的工作,但從右到左。
Array.isArray
陣列不形成單獨的語言類型。它們基於物件。
因此 typeof
無法幫助區分一般物件和陣列
alert(typeof {}); // object
alert(typeof []); // object (same)
…但陣列使用得如此頻繁,因此有一個特殊的方法:Array.isArray(value)。如果 value
是陣列,則傳回 true
,否則傳回 false
。
alert(Array.isArray({})); // false
alert(Array.isArray([])); // true
大多數方法支援「thisArg」
幾乎所有呼叫函式的陣列方法(例如 find
、filter
、map
,但 sort
是個值得注意的例外)都接受一個額外的選用參數 thisArg
。
此參數未在上述各節中說明,因為它很少使用。但為了完整性,我們必須涵蓋它。
以下是這些方法的完整語法
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg is the optional last argument
thisArg
參數的值會成為 func
的 this
。
例如,這裡我們使用 army
物件的方法作為篩選器,而 thisArg
傳遞上下文
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return user.age >= this.minAge && user.age < this.maxAge;
}
};
let users = [
{age: 16},
{age: 20},
{age: 23},
{age: 30}
];
// find users, for who army.canJoin returns true
let soldiers = users.filter(army.canJoin, army);
alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23
如果在上述範例中,我們使用 users.filter(army.canJoin)
,則 army.canJoin
會被呼叫為獨立函式,其中 this=undefined
,因此會立即導致錯誤。
可以將對 users.filter(army.canJoin, army)
的呼叫替換為 users.filter(user => army.canJoin(user))
,這會執行相同的工作。後者使用得更頻繁,因為對大多數人來說,它更容易理解。
摘要
陣列方法的秘笈
-
新增/移除元素
push(...items)
– 在尾端新增項目pop()
– 從尾端移除項目shift()
– 從頭端移除項目unshift(...items)
– 在頭端新增項目splice(pos, deleteCount, ...items)
– 在索引pos
處刪除deleteCount
個元素並插入items
slice(start, end)
– 建立一個新陣列,將索引start
到end
(不包含)的元素複製到其中concat(...items)
– 傳回一個新陣列:複製目前陣列的所有成員並將items
新增到其中。如果任一items
是陣列,則會取出其元素
-
在元素中搜尋
indexOf/lastIndexOf(item, pos)
– 從位置pos
開始尋找item
,並傳回索引或找不到時傳回-1
includes(value)
– 如果陣列有value
,則傳回true
,否則傳回false
find/filter(func)
– 透過函式篩選元素,傳回第一個/所有讓函式傳回true
的值findIndex
類似於find
,但傳回索引而非值
-
在元素中反覆運算
forEach(func)
– 對每個元素呼叫func
,不傳回任何內容
-
轉換陣列
map(func)
– 從對每個元素呼叫func
的結果建立一個新陣列sort(func)
– 就地排序陣列,然後傳回陣列reverse()
– 就地反轉陣列,然後傳回陣列split/join
– 將字串轉換為陣列並反過來reduce/reduceRight(func, initial)
– 透過對每個元素呼叫func
並在呼叫之間傳遞中間結果,計算陣列中的單一值
-
此外
Array.isArray(value)
檢查value
是否為陣列,如果是則傳回true
,否則傳回false
請注意,方法 sort
、reverse
和 splice
會修改陣列本身
這些方法是最常用的方法,它們涵蓋了 99% 的使用案例。但還有其他方法
-
arr.some(fn)/arr.every(fn) 檢查陣列
函式
fn
會在陣列的每個元素上呼叫,類似於map
。如果任一/所有結果為true
,則傳回true
,否則傳回false
這些方法的行為類似於
||
和&&
算子:如果fn
傳回真值,arr.some()
會立即傳回true
並停止反覆運算其餘項目;如果fn
傳回假值,arr.every()
會立即傳回false
並停止反覆運算其餘項目我們可以使用
every
來比較陣列function arraysEqual(arr1, arr2) { return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]); } alert( arraysEqual([1, 2], [1, 2])); // true
-
arr.fill(value, start, end) – 以重複的
value
填滿陣列,從索引start
到end
。 -
arr.copyWithin(target, start, end) – 將其元素從位置
start
複製到位置end
,到 本身,在位置target
(覆寫現有元素)。 -
arr.flat(depth)/arr.flatMap(fn) 從多維陣列建立新的平面陣列。
完整清單,請參閱 手冊。
乍看之下,方法似乎很多,很難記住。但實際上,這容易多了。
瀏覽秘笈,僅僅是為了知道它們。然後,解此章節的作業以練習,這樣您就有使用陣列方法的經驗。
之後,每當您需要對陣列執行某些操作,卻不知道如何執行時,請到這裡來,查看秘笈,並找到正確的方法。範例將協助您正確撰寫。很快地,您將自動記住這些方法,而無需特別費力。
留言
<code>
標籤,對於多行,請將它們包覆在<pre>
標籤中,對於超過 10 行,請使用沙盒 (plnkr、jsbin、codepen…)