# JavaScript 入門課程 - 第二章 資料型別(Data type) 2-7 陣列(Array)
[竹白記事本](https://chupainotebook.blogspot.com/),學習筆記整理 2019/01/18。
###### tags: `JavaScript 入門課程` `JavaScript` `JavaScript 入門課程 - 第二章 資料型別(Data type)`
>學習書本:
>[JavaScript 教程電子書|作者:阮一峰老師](https://wangdoc.com/javascript/index.html)
「JavaScript 教程」是由阮一峰老師所撰寫的,並且授權方式為「知識共享署名-相同方式共享3.0協議」,非常適合初學者當作 JavaScript 語言的入門課程。
## [目錄](https://hackmd.io/zZe_oRgfQp6YFboDCVAvpg)
* Ch2 資料型別(Data type)
* [2-1 概述](https://hackmd.io/s/Hkr0vljfV)
* [2-2 null、undefined 和 布林值(Boolean)](https://hackmd.io/s/Hkr0vljfV)
* [2-3 數值(Number)](https://hackmd.io/s/rkJlPqsGV)
* [2-4 字串(String)](https://hackmd.io/s/HJvOP5iGE)
* [2-5 物件(Object)](https://hackmd.io/s/H1Aq7D3f4)
* [2-6 函式(Function)](https://hackmd.io/s/S1ls7D3zE)
* [2-7 陣列(Array)](https://hackmd.io/s/BkzjmP3GE)
# 2-7 陣列(Array)
## 1. 定義
陣列(array)是按次序排列的一組值。每個值的位置都有編號(從 0 開始),整個陣列用方括號表示。
```javascript
var arr = ['a', 'b', 'c'];
```
上面代碼中的 `a`、`b`、`c` 就構成一個陣列,兩端的方括號是陣列的標誌。`a` 是 0 號位置,`b` 是 1 號位置,`c` 是 2 號位置。
除了在定義時賦值,陣列也可以先定義後賦值。
```javascript
var arr = [];
arr[0] = 'a';
arr[1] = 'b';
arr[2] = 'c';
```
任何類型的資料,都可以放入陣列。
```javascript
var arr = [
{a: 1},
[1, 2, 3],
function() {return true;}
];
arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}
```
上面陣列 `arr` 的 3 個成員依次是物件、陣列、函式。
如果陣列的元素還是陣列,就形成了多維陣列。
```javascript
var a = [[1, 2], [3, 4]];
a[0][1] // 2
a[1][1] // 4
```
## 2.1 陣列的本質
本質上,陣列屬於一種特殊的物件。`typeof` 運算子會傳回陣列的類型是 `object`。
```javascript
typeof [1, 2, 3] // "object"
```
上面代碼表明,`typeof` 運算子認為陣列的類型就是物件。
陣列的特殊性體現在,它的鍵名是按次序排列的一組整數(0,1,2...)。
```javascript
var arr = ['a', 'b', 'c'];
Object.keys(arr)
// ["0", "1", "2"]
```
上面代碼中,`Object.keys` 方法傳回陣列的所有鍵名。可以看到陣列的鍵名就是整數 0、1、2。
由於陣列成員的鍵名是固定的(默認總是 0、1、2...),因此陣列不用為每個元素指定鍵名,而物件的每個成員都必須指定鍵名。JavaScript 語言規定,物件的鍵名一律為字串,所以,陣列的鍵名其實也是字串。之所以可以用數值讀取,是因為非字串的鍵名會被轉為字串。
```javascript
var arr = ['a', 'b', 'c'];
arr['0'] // 'a'
arr[0] // 'a'
```
上面代碼分別用數值和字串作為鍵名,結果都能讀取陣列。原因是數值鍵名被自動轉為了字串。
:::warning
注意,這點在賦值時也成立。如果一個值總是先轉成字串,再進行賦值。
:::
```javascript
var a = [];
a[1.00] = 6;
a[1] // 6
```
上面代碼中,由於 `1.00` 轉成字串是 `1`,所以通過數字鍵 `1` 可以讀取值。
上一章說過,物件有兩種讀取成員的方法:點結構(`object.key`)和方括號結構(`object[key]`)。但是,對於數值的鍵名,不能使用點結構。
```javascript
var arr = [1, 2, 3];
arr.0 // SyntaxError
```
上面代碼中,`arr.0` 的寫法不合法,因為單獨的數值不能作為標識符(identifier)。所以,陣列成員只能用方括號 `arr[0]` 表示(方括號是運算子,可以接受數值)。
## 3. length 屬性
陣列的 `length` 屬性,傳回陣列的成員數量。
```javascript
['a', 'b', 'c'].length // 3
```
JavaScript 使用一個 32 位整數,保存陣列的元素個數。這意味著,陣列成員最多只有 4294967295 個(2<sup>32</sup> - 1)個,也就是說 `length` 屬性的最大值就是 4294967295。
只要是陣列,就一定有 `length` 屬性。該屬性是一個動態的值,等於鍵名中的最大整數加上 `1`。
```javascript
var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001
```
上面代碼表示,陣列的數字鍵不需要連續,`length` 屬性的值總是比最大的那個整數鍵大 `1`。另外,這也表明陣列是一種動態的資料結構,可以隨時增減陣列的成員。
:::success
`length` 屬性是可寫的。如果人為設置一個小於當前成員個數的值,該陣列的成員會自動減少到 `length` 設置的值。
:::
```javascript
var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]
```
上面代碼表示,當陣列的 `length` 屬性設為 2(即最大的整數鍵只能是 1)那麼整數鍵 2(值為 `c`)就已經不在陣列中了,被自動刪除了。
清空陣列的一個有效方法,就是將 `length` 屬性設為 0。
```javascript
var arr = [ 'a', 'b', 'c' ];
arr.length = 0;
arr // []
```
如果人為設置 `length` 大於當前元素個數,則陣列的成員數量會增加到這個值,新增的位置都是空位。
```javascript
var a = ['a'];
a.length = 3;
a[1] // undefined
```
上面代碼表示,當 `length` 屬性設為大於陣列個數時,讀取新增的位置都會傳回 `undefined`。
如果人為設置 `length` 為不合法的值,JavaScript 會報錯。
```javascript
// 設置負值
[].length = -1
// RangeError: Invalid array length
// 陣列元素個數大於等於2的32次方
[].length = Math.pow(2, 32)
// RangeError: Invalid array length
// 設置字串
[].length = 'abc'
// RangeError: Invalid array length
```
值得注意的是,由於陣列本質上是一種物件,所以可以為陣列添加屬性,但是這不影響 `length` 屬性的值。
```javascript
var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0
```
上面代碼將陣列的鍵分別設為字串和小數,結果都不影響 `length` 屬性。因為,`length` 屬性的值就是等於最大的數字鍵加 1,而這個陣列沒有整數鍵,所以 `length` 屬性保持為`0`。
如果陣列的鍵名是添加超出範圍的數值,該鍵名會自動轉為字串。
```javascript
var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';
arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"
```
上面代碼中,我們為陣列 `arr` 添加了兩個不合法的數字鍵,結果 `length` 屬性沒有發生變化。這些數字鍵都變成了字串鍵名。最後兩行之所以會取到值,是因為取鍵值時,數字鍵名會默認轉為字串。
## 4. in 運算子
檢查某個鍵名是否存在的運算子 `in`,適用於物件,也適用於陣列。
```javascript
var arr = [ 'a', 'b', 'c' ];
2 in arr // true
'2' in arr // true
4 in arr // false
```
上面代碼表明,陣列存在鍵名為 `2` 的鍵。由於鍵名都是字串,所以數值 `2` 會自動轉成字串。
:::warning
注意,如果陣列的某個位置是空位,`in` 運算子傳回 `false`。
:::
```javascript
var arr = [];
arr[100] = 'a';
100 in arr // true
1 in arr // false
```
上面代碼中,陣列 `arr` 只有一個成員 `arr[100]`,其他位置的鍵名都會傳回 `false`。
## 5. for...in 循環和陣列的遍歷
`for...in` 循環不僅可以遍歷物件,也可以遍歷陣列,畢竟陣列只是一種特殊物件。
```javascript
var a = [1, 2, 3];
for (var i in a) {
console.log(a[i]);
}
// 1
// 2
// 3
```
但是,`for...in` 不僅會遍歷陣列所有的數字鍵,還會遍歷非數字鍵。
```javascript
var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
console.log(key);
}
// 0
// 1
// 2
// foo
```
上面代碼在遍歷陣列時,也遍歷到了非整數鍵 `foo`。所以,不推薦使用 `for...in` 遍歷陣列。
陣列的遍歷可以考慮使用 `for` 循環或 `while` 循環。
```javascript
var a = [1, 2, 3];
// for循環
for(var i = 0; i < a.length; i++) {
console.log(a[i]);
}
// while循環
var i = 0;
while (i < a.length) {
console.log(a[i]);
i++;
}
var l = a.length;
while (l--) {
console.log(a[l]);
}
```
上面代碼是三種遍歷陣列的寫法。最後一種寫法是逆向遍歷,即從最後一個元素向第一個元素遍歷。
陣列的 `forEach` 方法,也可以用來遍歷陣列。
```javascript
var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
console.log(color);
});
// red
// green
// blue
```
## 6. 陣列的空位
```javascript
var a = [1, , 1];
a.length // 3
```
上面代碼表明,陣列的空位不影響 `length` 屬性。
需要注意的是,如果最後一個元素後面有逗號,並不會產生空位。也就是說,有沒有這個逗號,結果都是一樣的。
```javascript
var a = [1, 2, 3,];
a.length // 3
a // [1, 2, 3]
```
上面代碼中,陣列最後一個成員後面有一個逗號,這不影響 `length` 屬性的值,與沒有這個逗號時效果一樣。
陣列的空位是可以讀取的,傳回 `undefined`。
```javascript
var a = [, , ,];
a[1] // undefined
```
使用 `delete` 命令刪除一個陣列成員,會形成空位,並且不會影響 `length` 屬性。
```javascript
var a = [1, 2, 3];
delete a[1];
a[1] // undefined
a.length // 3
```
上面代碼用 `delete` 命令刪除了陣列的第二個元素,這個位置就形成了空位,但是對 `length` 屬性沒有影響。也就是說,`length` 屬性不過濾空位。所以,使用 `length` 屬性進行陣列遍歷,一定要非常小心。
陣列的某個位置是空位,與某個位置是 `undefined`,是不一樣的。如果是空位,使用陣列的 `forEach` 方法、`for...in` 結構、以及 `Object.keys` 方法進行遍歷,空位都會被跳過。
```javascript
var a = [, , ,];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
})
// 不產生任何輸出
for (var i in a) {
console.log(i);
}
// 不產生任何輸出
Object.keys(a)
// []
```
如果某個位置是 `undefined`,遍歷的時候就不會被跳過。
```javascript
var a = [undefined, undefined, undefined];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
});
// 0. undefined
// 1. undefined
// 2. undefined
for (var i in a) {
console.log(i);
}
// 0
// 1
// 2
Object.keys(a)
// ['0', '1', '2']
```
這就是說,空位就是陣列沒有這個元素,所以不會被遍歷到,而 `undefined` 則表示陣列有這個元素,值是 `undefined`,所以遍歷不會跳過。
## 7. 類似陣列的物件
如果一個物件的所有鍵名都是正整數或零,並且有 `length` 屬性,那麼這個物件就很像陣列,語法上稱為**類似陣列的物件(array-like object)**。
```javascript
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
obj[0] // 'a'
obj[1] // 'b'
obj.length // 3
obj.push('d') // TypeError: obj.push is not a function
```
上面代碼中,物件 `obj` 就是一個類似陣列的物件。但是,**類似陣列的物件**並不是陣列,因為它們不具備陣列特有的方法。物件 `obj` 沒有陣列的 `push` 方法,使用該方法就會報錯。
**類似陣列的物件**的根本特徵,就是具有 `length` 屬性。只要有 `length` 屬性,就可以認為這個類似於陣列。但是有一個問題,這種 `length` 屬性不是動態值,不會隨著成員的變化而變化。
```javascript
var obj = {
length: 0
};
obj[3] = 'd';
obj.length // 0
```
上面代碼為物件 `obj` 添加了一個數字鍵,但是 `length` 屬性沒變。這就說明了 `obj` 不是陣列。
典型的**類似陣列的物件**是函式的 `arguments` 物件,以及大多數 DOM 元素集,還有字串。
```javascript
// arguments物件
function args() { return arguments }
var arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false
// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false
// 字串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false
```
上面代碼包含三個例子,它們都不是陣列(`instanceof` 運算子傳回 `false`),但是看上去都非常像陣列。
陣列的 `slice` 方法可以將**類似陣列的物件**變成真正的陣列。
```javascript
var arr = Array.prototype.slice.call(arrayLike);
```
除了轉為真正的陣列,**類似陣列的物件**還有一個辦法可以使用陣列的方法,就是通過 `call()` 把陣列的方法放到物件上面。
```javascript
function print(value, index) {
console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);
```
上面代碼中,`arrayLike` 代表一個類似陣列的物件,本來是不可以使用陣列的 `forEach()` 方法的,但是通過 `call()`,可以把 `forEach()` 嫁接到`arrayLike` 上面呼叫。
下面的例子就是通過這種方法,在 `arguments` 物件上面呼叫 `forEach` 方法。
```javascript
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同於for 循環
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
```
字串也是類似陣列的物件,所以也可以用 `Array.prototype.forEach.call` 遍歷。
```javascript
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
```
:::warning
注意,這種方法比直接使用陣列原生的 `forEach` 要慢,所以最好還是先將**類似陣列的物件**轉為真正的陣列,然後再直接呼叫陣列的`forEach`方法。
:::
```javascript
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c
```
---
下個章節:[Ch3 運算子(Operator)](https://hackmd.io/s/ry_4Qy1mE)