物件允許你儲存以鍵值對應的集合。這很好。
但我們常常發現我們需要一個有序集合,其中有第 1 個、第 2 個、第 3 個元素,以此類推。例如,我們需要儲存某個清單:使用者、商品、HTML 元素等。
使用物件在此並不方便,因為它沒有提供管理元素順序的方法。我們無法在現有屬性「之間」插入新的屬性。物件根本不是用於這種用途的。
存在一個特殊資料結構名為 陣列
,用於儲存已排序的集合。
宣告
建立一個空陣列有兩種語法
let arr = new Array();
let arr = [];
幾乎所有時間,都會使用第二種語法。我們可以在括號中提供初始元素
let fruits = ["Apple", "Orange", "Plum"];
陣列元素從零開始編號。
我們可以使用方括號中的數字取得元素
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits[0] ); // Apple
alert( fruits[1] ); // Orange
alert( fruits[2] ); // Plum
我們可以替換元素
fruits[2] = 'Pear'; // now ["Apple", "Orange", "Pear"]
…或新增一個到陣列中
fruits[3] = 'Lemon'; // now ["Apple", "Orange", "Pear", "Lemon"]
陣列中元素的總數是它的 長度
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits.length ); // 3
我們也可以使用 alert
來顯示整個陣列。
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits ); // Apple,Orange,Plum
陣列可以儲存任何類型的元素。
例如
// mix of values
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
// get the object at index 1 and then show its name
alert( arr[1].name ); // John
// get the function at index 3 and run it
arr[3](); // hello
陣列就像物件一樣,可能會以逗號結尾
let fruits = [
"Apple",
"Orange",
"Plum",
];
「尾隨逗號」樣式讓插入/移除項目變得更容易,因為所有行都變得相同。
使用「at」取得最後一個元素
假設我們想要陣列中的最後一個元素。
有些程式設計語言允許使用負索引來達到相同目的,例如 fruits[-1]
。
不過,在 JavaScript 中這不起作用。結果將會是 undefined
,因為方括號中的索引是逐字處理的。
我們可以明確計算最後一個元素索引,然後存取它:fruits[fruits.length - 1]
。
let fruits = ["Apple", "Orange", "Plum"];
alert( fruits[fruits.length-1] ); // Plum
有點麻煩,不是嗎?我們需要寫兩次變數名稱。
幸運的是,有一個較短的語法:fruits.at(-1)
let fruits = ["Apple", "Orange", "Plum"];
// same as fruits[fruits.length-1]
alert( fruits.at(-1) ); // Plum
換句話說,arr.at(i)
- 與
arr[i]
完全相同,如果i >= 0
。 - 對於
i
的負值,它會從陣列的末尾往回計算。
pop/push、shift/unshift 方法
一個 佇列 是陣列最常見的用途之一。在電腦科學中,這表示一個支援兩個操作的已排序元素集合
push
將元素附加到末尾。shift
從開頭取得元素,推進佇列,使第二個元素變成第一個。
陣列支援這兩個操作。
在實務上,我們非常常需要它。例如,一個需要在螢幕上顯示的訊息佇列。
陣列還有另一個用例,一個名為 堆疊 的資料結構。
它支援兩個操作
push
將元素新增到末尾。pop
從末尾取得元素。
因此,新元素總是從「末尾」新增或取得。
堆疊通常被描繪成一疊卡片:新卡片會新增到頂部或從頂部取得
對於堆疊,最新推入的項目會最先收到,這也稱為 LIFO(後進先出)原則。對於佇列,我們有 FIFO(先進先出)。
JavaScript 中的陣列可以同時作為佇列和堆疊。它們允許您新增/移除元素,從開頭或末尾新增/移除。
在電腦科學中,允許這樣做的資料結構稱為 雙端佇列。
使用陣列末端的方法
pop
-
擷取陣列的最後一個元素並傳回
let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.pop() ); // remove "Pear" and alert it alert( fruits ); // Apple, Orange
fruits.pop()
和fruits.at(-1)
都會傳回陣列的最後一個元素,但fruits.pop()
也會修改陣列並將其移除。 push
-
將元素附加到陣列的末端
let fruits = ["Apple", "Orange"]; fruits.push("Pear"); alert( fruits ); // Apple, Orange, Pear
呼叫
fruits.push(...)
等於fruits[fruits.length] = ...
。
使用陣列開頭的方法
shift
-
擷取陣列的第一個元素並傳回
let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // remove Apple and alert it alert( fruits ); // Orange, Pear
unshift
-
將元素新增到陣列的開頭
let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); alert( fruits ); // Apple, Orange, Pear
方法 push
和 unshift
可以一次新增多個元素
let fruits = ["Apple"];
fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");
// ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
alert( fruits );
內部結構
陣列是一種特殊的物件。用於存取屬性的方括號 arr[0]
實際上來自物件語法。這基本上與 obj[key]
相同,其中 arr
是物件,而數字用作鍵。
它們擴充物件,提供特殊的方法來處理資料的有序集合,以及 length
屬性。但其核心仍然是一個物件。
請記住,JavaScript 中只有八種基本資料類型(有關更多資訊,請參閱 資料類型 章節)。陣列是一個物件,因此會像物件一樣運作。
例如,它是透過參考複製的
let fruits = ["Banana"]
let arr = fruits; // copy by reference (two variables reference the same array)
alert( arr === fruits ); // true
arr.push("Pear"); // modify the array by reference
alert( fruits ); // Banana, Pear - 2 items now
…但讓陣列真正特別的是它們的內部表示。引擎會嘗試將其元素儲存在連續的記憶體區域中,一個接一個,就像本章節插圖中所描繪的那樣,還有其他最佳化,讓陣列可以非常快速地運作。
但如果我們不再將陣列視為「有序集合」來處理,而是開始將它視為常規物件來處理,它們就會全部中斷。
例如,技術上我們可以這樣做
let fruits = []; // make an array
fruits[99999] = 5; // assign a property with the index far greater than its length
fruits.age = 25; // create a property with an arbitrary name
這是可能的,因為陣列在它們的基礎上是物件。我們可以向它們新增任何屬性。
但引擎會看到我們將陣列視為常規物件來處理。陣列特定的最佳化不適合此類情況,並且會被關閉,它們的優點也會消失。
誤用陣列的方式
- 新增非數字屬性,例如
arr.test = 5
。 - 建立間隙,例如:新增
arr[0]
,然後新增arr[1000]
(它們之間沒有任何東西)。 - 以相反的順序填入陣列,例如
arr[1000]
、arr[999]
等。
請將陣列視為處理有序資料的特殊結構。它們提供特殊的方法來處理這些資料。JavaScript 引擎中仔細調整了陣列,以處理連續的有序資料,請使用這種方式。如果您需要任意鍵,很有可能您實際上需要一個常規物件 {}
。
效能
方法 push/pop
執行速度快,而 shift/unshift
則很慢。
為什麼從陣列的尾端處理比從頭部處理來得快?讓我們看看執行期間發生了什麼事
fruits.shift(); // take 1 element from the start
光取用和移除索引為 0
的元素還不夠。其他元素也需要重新編號。
shift
執行序必須執行 3 件事
- 移除索引為
0
的元素。 - 將所有元素向左移動,將其索引從
1
重新編號為0
,從2
重新編號為1
,以此類推。 - 更新
length
屬性。
陣列中的元素越多,移動它們所需的時間就越多,記憶體內運算也越多。
unshift
發生類似的情況:若要將元素新增到陣列的開頭,我們必須先將現有的元素向右移動,增加其索引。
而 push/pop
呢?它們不需要移動任何東西。若要從尾端擷取元素,pop
方法會清除索引並縮短 length
。
pop
執行的動作
fruits.pop(); // take 1 element from the end
pop
方法不需要移動任何東西,因為其他元素會保留其索引。這就是為什麼它執行速度非常快。
push
方法也有類似的狀況。
迴圈
迴圈陣列項目最古老的方法之一是在索引上使用 for
迴圈
let arr = ["Apple", "Orange", "Pear"];
for (let i = 0; i < arr.length; i++) {
alert( arr[i] );
}
但是對於陣列,還有另一種迴圈形式,for..of
let fruits = ["Apple", "Orange", "Plum"];
// iterates over array elements
for (let fruit of fruits) {
alert( fruit );
}
for..of
無法存取目前元素的數字,只能存取其值,但在大多數情況下,這就已足夠。而且它比較簡短。
技術上來說,由於陣列是物件,因此也可以使用 for..in
let arr = ["Apple", "Orange", "Pear"];
for (let key in arr) {
alert( arr[key] ); // Apple, Orange, Pear
}
但這其實是個壞主意。它可能會產生問題
-
迴圈
for..in
會反覆處理所有屬性,不只數字屬性。瀏覽器和其他環境中存在所謂的「類陣列」物件,它們看起來像陣列。也就是說,它們有
length
和索引屬性,但它們也可能具有其他非數字屬性和方法,而這些通常不是我們需要的。不過,for..in
迴圈會列出它們。因此,如果我們需要處理類陣列物件,那麼這些「額外」屬性可能會成為一個問題。 -
for..in
迴圈是針對一般物件最佳化,而非陣列,因此速度會慢 10 到 100 倍。當然,它仍然非常快。速度提升可能只會在瓶頸時顯著。但我們仍應了解兩者之間的差異。
一般來說,我們不應對陣列使用 for..in
。
關於「長度」的一句話
當我們修改陣列時,length
屬性會自動更新。精確來說,它實際上並非陣列中值的計數,而是最大的數字索引值加一。
例如,具有大索引值的單一元素會產生較大的長度
let fruits = [];
fruits[123] = "Apple";
alert( fruits.length ); // 124
請注意,我們通常不會這樣使用陣列。
關於 length
屬性的另一個有趣之處是它可寫入。
如果我們手動增加它,不會發生任何有趣的事情。但如果我們減少它,陣列會被截斷。此程序不可逆,以下為範例
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // truncate to 2 elements
alert( arr ); // [1, 2]
arr.length = 5; // return length back
alert( arr[3] ); // undefined: the values do not return
因此,清除陣列最簡單的方法是:arr.length = 0;
。
new Array()
還有一個語法可建立陣列
let arr = new Array("Apple", "Pear", "etc");
它很少使用,因為方括號 []
較短。此外,它還有一個棘手的功能。
如果 new Array
呼叫時只有一個數字引數,則它會建立一個陣列,不包含項目,但具有指定的長度。
讓我們看看如何自找麻煩
let arr = new Array(2); // will it create an array of [2] ?
alert( arr[0] ); // undefined! no elements.
alert( arr.length ); // length 2
為避免此類意外,我們通常使用方括號,除非我們真的知道自己在做什麼。
多維陣列
陣列可以包含也是陣列的項目。我們可以將其用於多維陣列,例如儲存矩陣
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
alert( matrix[1][1] ); // 5, the central element
toString
陣列有自己的 toString
方法實作,會傳回以逗號分隔的元素清單。
例如
let arr = [1, 2, 3];
alert( arr ); // 1,2,3
alert( String(arr) === '1,2,3' ); // true
另外,讓我們試試這個
alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"
陣列沒有 Symbol.toPrimitive
,也沒有可行的 valueOf
,它們只實作 toString
轉換,因此這裡的 []
會變成空字串,[1]
會變成 "1"
,而 [1,2]
會變成 "1,2"
。
當二元加號 "+"
算子將某個項目加入字串時,它也會將其轉換為字串,因此下一個步驟如下所示
alert( "" + 1 ); // "1"
alert( "1" + 1 ); // "11"
alert( "1,2" + 1 ); // "1,21"
不要使用 == 比較陣列
與其他一些程式語言不同,JavaScript 中的陣列不應使用 ==
算子進行比較。
此算子沒有針對陣列的特殊處理,它會將陣列視為一般物件處理。
讓我們回顧一下規則
- 只有當兩個物件是同一個物件的參考時,它們才相等
==
。 - 如果
==
的其中一個引數是物件,而另一個是原始值,則會將物件轉換為原始值,如 物件轉換為原始值 章節中所述。 - …除了
null
和undefined
,它們彼此相等==
,且不與其他任何項目相等。
嚴格比較 ===
甚至更簡單,因為它不會轉換類型。
所以,如果我們使用 ==
比較陣列,它們永遠不會相同,除非我們比較兩個參照完全相同陣列的變數。
例如
alert( [] == [] ); // false
alert( [0] == [0] ); // false
這些陣列在技術上是不同的物件。所以它們不相等。==
算子不會進行逐項比較。
與基本類型比較也可能產生看似奇怪的結果
alert( 0 == [] ); // true
alert('0' == [] ); // false
這裡,在兩種情況下,我們將一個基本類型與一個陣列物件進行比較。因此,陣列 []
會在比較時轉換為基本類型,並成為一個空字串 ''
。
然後比較過程繼續使用基本類型,如章節 類型轉換 中所述
// after [] was converted to ''
alert( 0 == '' ); // true, as '' becomes converted to number 0
alert('0' == '' ); // false, no type conversion, different strings
那麼,如何比較陣列?
這很簡單:不要使用 ==
算子。相反,在迴圈中逐項比較它們或使用下一章節中說明的迭代方法。
摘要
陣列是一種特殊類型的物件,適用於儲存和管理已排序的資料項目。
宣告
// square brackets (usual)
let arr = [item1, item2...];
// new Array (exceptionally rare)
let arr = new Array(item1, item2...);
呼叫 new Array(number)
會建立一個具有給定長度的陣列,但沒有元素。
length
屬性是陣列長度,或者更精確地說,是其最後一個數字索引加一。它會由陣列方法自動調整。- 如果我們手動縮短
length
,陣列會被截斷。
取得元素
- 我們可以使用其索引取得元素,例如
arr[0]
- 我們也可以使用
at(i)
方法,它允許負索引。對於i
的負值,它會從陣列的尾端往回計算。如果i >= 0
,它的作用與arr[i]
相同。
我們可以使用以下操作將陣列作為雙端佇列
push(...items)
將items
新增到尾端。pop()
從尾端移除元素並傳回它。shift()
從開頭移除元素並傳回它。unshift(...items)
將items
新增到開頭。
要迴圈陣列的元素
for (let i=0; i<arr.length; i++)
– 運作最快,與舊瀏覽器相容。for (let item of arr)
– 僅適用於項目的現代語法,for (let i in arr)
– 永遠不要使用。
要比較陣列,請不要使用 ==
算子(以及 >
、<
等),因為它們沒有針對陣列進行特殊處理。它們將陣列視為任何物件,而這通常不是我們想要的。
相反,你可以使用 for..of
迴圈逐項比較陣列。
我們將繼續探討陣列,並在下一章節 陣列方法 中研究更多新增、移除、擷取元素和排序陣列的方法。
留言
<code>
標籤,若要插入多行程式碼,請將它們包裝在<pre>
標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)