array

少安的砖场發表於2024-11-09

Array

這章主要解釋陣列,陣列是JavaScript中的一類基礎資料型別,在很多語言裡也一樣。陣列是一個有順序的資料集合。其中的每一個資料被叫做一個元素,每個元素都有一個數字下標,這被成為索引,JavaScript陣列不限制資料型別,也就是說裡面的元素型別可以是任意的。陣列元素甚至可以是一個物件或者其他陣列,你可以構建出極其複雜的資料結構,比如一個儲存物件的陣列,或者儲存陣列的陣列。JavaScript陣列的索引從0開始,同時其範圍可以涵蓋32bit,也就是說,你可以儲存不超過4294967294個元素。同時你不需要指定陣列大小,它會根據你對陣列的操作對陣列大小進行修改。JavaScript陣列可以是斷斷續續的,也就是說你允許在元素間存在空隙,並不需要每一個位置都填入元素。每一個陣列都會有一個length屬性,對於非間斷的陣列而言,length代表其元素的個數。然而對於間斷的陣列而言,length的大小取決於陣列中元素索引的最大值。

陣列在JavaScript中是一種特殊的物件。陣列索引也僅僅只是它的屬性名而已,只是恰好用整數表示。訪問陣列元素會比訪問普通物件的值快得多,後面會詳細講到。

陣列繼承了Array.prototype,裡面包含很多陣列操作方法,在7.8中會講到。其中大部分方法都是通用的,這意味著他們不僅僅適用於陣列,還適用於其他像陣列的物件。這部分內容在7.9中會提到。最後一點,JavaScript String 看起來像字元陣列,這裡會在7.10裡講解到。

ES6介紹了一種新型的陣列被稱為型別陣列,不同於傳統的陣列型別,型別陣列擁有一個固定的長度,同時限制其元素的型別,他們在訪問位元層面具有很好的表現,11.2中會講到。

7.1 Creating Arrays

建立一個陣列有多種方法,接下來這些個小章節會詳細討論這幾種情況。

7.1.1 The Array Literals

建立一個陣列最簡單的方法是透過陣列字面量,它僅僅只是被[]框住的以逗號分隔的元素序列,舉個例子。

let empty = [];
let primes = [2,3];
let misc = [1.1,true,"a",];

陣列字面量中的元素不一定就非得是常量,他們也可以是任何表示式:

let base = 1024;
let table = [base,base+1];

陣列字面量可以包含其他物件字面量或者陣列字面量。

let b = [[1,{x:1, y:2}]];

如果一個陣列字面量表示式裡包含連續的逗號,之間也不存在值,這種方式建立的陣列會成為間斷陣列,在這些間斷位置上的元素會被忽略,如果你嘗試去獲取他們,你將得到undefined。

let count = [1,3];
let undefs = [,,];

字面量表示式允許存在一種曼生逗號,在這種條件下[,,]的陣列長度為2,而非3.

7.1.2 The Spread Operator

在ES6以後,你可以透過使用“展開運算子” ... ,以字面量的形式實現將一個陣列元素合併進另一個陣列。

let a = [1,2,3];
let b = [0,...a,4];
...會展開陣列a然後,a其中的所有元素會嵌入到下方這個陣列字面量表示式中。...a會被替代為a陣列中全體元素,排列在陣列b中特定的索引範圍內。(要注意一點,儘管我們將...稱為張開運算子,但它並不是傳統意義上的運算子,因為它僅僅只能用在陣列字面量中,以及函式引用值)

在新建一個陣列的備份時,使用展開運算子是一個非常方便的方法:

let original = [1,2,3];
let copy = [...original];
copy[0] = 0; // 修改備份並不會影響到原陣列
original[0] // => 1

展開運算子適用於任何可以被列舉的物件(可列舉的物件是指被for/of列舉的東西,我們第一次看到他們在5.4.4,12章還會經常看到),String是可列舉的,所以可以透過展開符將任何string轉變成一個字元陣列。

let digits = [..."3792"];
digits // => ['3','7','9','2']
Set 物件(11.1.1)是可列舉的,所以去除一個陣列中重複的元素,一個非常簡單的方式是將其轉變為一個set,然後立刻透過展開符將其轉回一個array.
let letter = [..."hello world"];
[...new Set(letters)] // => ['h','e','l','o','w','r','d']

7.1.3

另一種方式建立物件是透過Array()構造方法,呼叫這個構造方法一共有三種不同的方式。

  • 無引數呼叫:
    let a = new Array();
    這個方法會建立一個空物件,相當於字面量方式建立[]
  • 呼叫時傳入一個數字引數,指明其陣列長度。

let a = new Array(10)

這種方式指定了新建陣列的長度。可以用於提前分配一個陣列,適用於你提前獲知該陣列長度的情況下。但需要注意的是,這個陣列並沒有儲存任何的值。

  • 明確宣告兩個或多個元素,抑或單個非數字型別的元素。

let a = new Array(5,4,"testing, testing");

在這種情況下構造方法內值會成為該陣列的元素,使用一個陣列字面量來建立其實更簡單。

7.1.4 Array.of()

當Array()建構函式以一個數字型別參呼叫時,他會將這個數字作為陣列的長度,而當傳入的引數大於1時,所有的引數會作為陣列的元素,這裡就有個問題,Array()並不能建立一個僅有一位數字元素的陣列。

ES6解決了這個問題,現在你可以透過Array.of()工廠方法來建立物件,不管多少引數,統統被視作新陣列的元素。

Array.of() //=>[];返回一個不含任何引數的空陣列
Array.of(10) //=>[10];建立一個包含10這個元素的陣列
Array.of(1,2,3) //=>[1,2,3]

7.1.5 Array.from()

Array.from() 也是在ES6中提出的另一種工廠方法,該方法第一個引數應為一個可列舉或者array-like物件。然後他會返回一個包含這個引數中所有元素或屬性的陣列。Array.from()的作用和展開符[...iterable]一樣,作為一個複製陣列最簡單的方式。

let copy = Array.from(original)

Array.from()的重要之處在於,它能將一個Array-like物件複製成一個貨真價實的Array陣列,Array-like物件是一個非陣列物件,但卻有著一個數字型別的屬性length,同時所有屬性的屬性名剛好是整數。在JavaScript客戶端,某些瀏覽器的方法是array-like。如果在使用他們之前,你能夠將其轉變為真陣列,可能會讓你用起來更順手。

let truearray = Array.from(arraylike);

Array.from()可以接受兩個引數,如果你將函式作為其第二個引數。在所有元素被複制完成以後,每一個元素都會被傳入該函式中,函式的返回值將作為新陣列對應索引的值。(這和Array的map()方法很相似,但是from()方法會更加高效,當這個陣列被新建。。。)

7.2 Reading and writing Array Elements

你可以透過[]運算子來訪問一個陣列中的元素,陣列引用在左邊,右邊的[]中可以放置任意的表示式,表示式最終可以表達為非負數。你可以透過這種方法來讀取或者寫入資料。因此,下方的表示式都是合法的JavaScript表示式。

let a = ['world']; //以一個元素初始化陣列
let value = a[0]; //讀取索引號為0的元素
a[1] = 3.14; //新增索引號為1的元素
let i = 2;
a[i] = 3; //新增索引號為2的元素
a[i + 1] = 'hello'; //新增索引號為3的元素
a[a[i]] = a[0]; //讀取索引號為0和2的元素,同時寫入索引號為3的元素

陣列的不同之處在於,當你的屬性名為非負整數,同時在0-2ˆ32 - 1,陣列會自動維護length屬性的值,在前面這個例子中,我們建立了一個單元素陣列,當我們給索引1,2,和3賦值時,Length屬性的值會自動改變。

a.length // =>4

你需要記住陣列屬於一種特殊的物件。[]在訪問陣列元素時就像你透過它訪問物件一樣,JavaScript將數字1強轉為字串'1',然後將1作為物件的屬性名去訪問它的值。整個過程沒有什麼特殊的操作,你可以使用一個普通物件來實現這一過程

let o = {} //建立一個空物件
o[] = "one"; //使用整數作為屬性名
o["1"] // => "one"; 數字和字串的作用是一樣的。

搞清楚陣列索引和物件屬性名很有用,所有的索引都屬於屬性名,但是這些屬性名卻是整數,範圍在0-2ˆ32之間。所有的陣列都是物件,也就是說你可以使用任何值作為屬性名使用。如果你使用索引作為其屬性名,JavaScript有著特殊的機制來更新length屬性。

你是可以使用負數來作為陣列屬性名的,當你這麼做的時候,這個負數會轉為字串,同時將其作為陣列的屬性名。然而因為它並不屬於正整數,因此會被當做普通物件屬性來處理。而非陣列索引。同時,如果你透過一個正整數字符串來訪問陣列,它表現得就像一個物件,而不是一個物件屬性。這種同樣的情況還發生在浮點數上,如果浮點數恰好等於一個正整數。

a[-1.23] = true; //建立了一個名字叫“-1.23”的屬性。
a["1000"] = 0; //給第10001個元素賦值。
a[1.000] = 1; //索引1,和a[1] = 1;一樣。

陣列索引僅僅只是一種特殊的屬性名意味著,JavaScript陣列不可能出現任何越界錯誤。當你嘗試去找到一個不存在的屬性,你不會得到任何報錯資訊。而是返回undefined,就像對物件的操作那樣。

let a = [true, false];
a[2] //=>undefined:該索引無值。
a[-1] //=>undefined:沒有這個名字的屬性。

7.3 Sparse Arrays

間斷陣列是指陣列擁有從0開始的非連續索引,一般而言,length屬性代表著陣列元素的個數,可一個間斷陣列的length會比該陣列中的屬性值更大。你可以透過Array()或者將值賦給陣列中一個比最大索引值還大得多的索引。

let a = new Array(5); // 沒有陣列,但是a.length的值為5.
a = []; //建立一個空陣列,length為0.
a[1000] = 0; //僅僅將一個值賦給了陣列,但是length的大小是1001.

後面我們還能用delete運算子實現間斷陣列。

過於鬆散的陣列通常 are implemented in a slow, more memory-efficient way then dense arrays are.同時在這種陣列中查詢元素所需時間幾乎和普通的物件差不多。

需要注意的是,當你在透過陣列字面量方式建立陣列的時候,在兩個元素之家使用了重複的逗號,這個陣列會變成間斷陣列。中間忽略掉的元素值是undefined。

let a1 = [,]; //陣列為空,但length為1。
let a2 = [undefined]; //擁有一個undefined元素的陣列。
0 in a1 //=>false: a1該索引位置無元素。
0 in a2 //=>true: a2在該索引位置值為undefined。

理解間斷陣列對於瞭解JavaScript陣列的本質十分重要。實際上,在實戰中你很少會碰到間斷陣列。就算你碰到了,你大機率還是像對待非間斷陣列那樣,只不過獲取到很多undefined元素。

7.4 Array Length

每一個陣列都有一個Length屬性,同時這也是陣列區別於其他普通物件的一個很重要的特徵。對於連續陣列而言,Length代表該陣列中所有元素的個數。它的大小隻比陣列中最大索引值大1.

[].length //=> 0: the array has no elements.
["a","b","c"].length // => 3: 最大索引值為2,length為3.

對於一個間斷陣列,Length的值將比陣列中元素個數大得多。總的來說,我只能保證length的值將會比任何元素的索引值大,換個說法,這個陣列中將不會有一個陣列的index比它大或者說等於它。為了保證這點,陣列存在兩個特俗的性質。第一種方式已經講過了,如果你將一個超過或者等於當前陣列length的索引賦值了,length的值會自動+1。

第二個性質是,如果你將一個陣列如果你設定的length值比當前陣列的length值更小,每一個索引大於或者等於這個值的元素將被移除。

a = [1,2,3,4,5] //以五個初始元素建立的陣列
a.length = 3 //a現在變成了[1,2,3]
a.length = 0 //所有元素都會被刪掉,a現在變成了[]
a.length = 5 //length現在等於5,但是陣列裡沒有任何元素,就像剛透過new Array()方式建立的那樣。

你仍可給length屬性賦予任何比它大的值,這並不會導致陣列被新增進入任何元素,而是僅僅在陣列後半部分建立了一個間斷區域。

7.5 Adding and Deleting Array Elements.

我們已經用過一種很簡單的方式向陣列中新增一個元素,直接將一個值賦給一個新索引。

let a = []; //建立了一個空陣列。
a[0] = "zero"; //And add elements to it。
a[1] = "one";

你也可以選擇使用push()方法將元素新增到陣列的尾部。

let a = []; //初始化一個空陣列
a.push("zero"); //新增一個元素到陣列尾部。a = ["zero"]
a.push("one", "two"); //同時新增多個元素。a = ["zero","one","two"]

將一個元素放入陣列相當於分配a[a.length]操作,你也可以透過unshift()方法插入一個元素到陣列的最前面,這會導致陣列中原有屬性索引變大。pop()方法是一個和push()方法完全相反的方法,他會一處陣列中最後一個元素。同時返回它的值。同理shift()是一個和unshift()完全相反的方法。它會刪除陣列中第一個元素,同時返回它的值。並且將陣列中所有元素的索引向更小的值移動一位。7.8章節中有更詳細的描述。

你可以使用 delete運算子 刪除任意的陣列元素,就像你對物件屬性做的那樣。

let a = [1,2,3];
delete a[2]; //a在2位置的元素為空
2 in a // => false: 索引2上的值為空。
a.length //=> 3: 刪除元素不會影響其length大小

刪除一個元素和給該索引值賦予undefined相似。需要注意的是,刪除陣列元素並不會改變length的大小,也不會導致陣列高位索引向下移動來填補該位置的空值。如果你刪除了一個連續陣列的元素,該陣列會變為間斷陣列。

前面講過一種方法,你可以透過設定length的方法,變相從陣列尾部移除元素。

最後介紹以下splice()方法,它是通用的插入,刪除或者替換陣列元素方法。他會修改length屬性,同時移動所有索引的值,以填補陣列中的空隙。在7.8章節中會討論到。

7.6 Iterating Arrays

在ES6中,最簡單的遍歷陣列的方法是使用 for/of 這個知識已經在5.5.5章節中講過了。

let letters = [..."Hello World"];
let stirng = "";
for(let letter of letters){
string += letter;
}
string // => "Hello world"; 重組了這個字串。

陣列內建的迭代器將返回陣列內每個元素,按照遞增的順序。但卻對間斷陣列沒有任何處理,而僅僅只是返回間斷點位置的空值(undefined)

如果你想對一個陣列使用for/of迴圈,同時獲知index的值。你可以透過陣列的entries()方法,與拆卸賦值使用時候就像下面這樣。

let everyother = "";
for(let [index, letter] of letters.entries()){
if(index/2 === 0) everyother += letter; //在當前索引位置的字元。
}

另一個很好的便利陣列方式是透過forEach()方法,這是一個新型for迴圈,也就是提供了一種函式式寫法來訪問陣列迭代器。你可以傳送一個函式給forEach()方法,同時forEach()將會在每一輪迭代喚醒你的函式一次。

let uppercase = "";
letters.forEach(letter => {
uppercase += letter.toUpperCase();
})
uppercase //"HELLO WORLD"

就像你想的那樣,forEach()按照index的大小遍歷陣列中的元素,實際上他還會傳給你指定位置的索引值,這個值會賦給函式的第二個引數,有時候會有點用。但不同於 for/of 迴圈,forEach()知曉間斷陣列,而且會跳過這些位置的元素。

7.8.1提供了關於forEach()方法的更多資訊。這章同樣概述了其他一些相關的方法,例如map()和filter(),他們都以某種特殊的形式表現了陣列迭代器。

你同樣可以透過老實迴圈來遍歷所有元素。5.5.3章節中的for loop

let vowels = "";
for(let i = 0; i < letters.length; i++){ //取得陣列中的每個元素
let letter = letters[i] // 獲取相應位置的index元素
if(/[aeiou]/.test(letter)){ // 呼叫正規表示式的test方法
vowels += letter; // 當其他屬於一個vowel
}
}
vowel //=>"eoo"

在巢狀迴圈中,或者其他注重效能的場景,你可能會發現這種基礎的迭代迴圈被使用。因此length將只會被呼叫一次,而不會在每一次的迭代器中使用。下面兩種for迴圈都是合法的表示式。雖然看起來不尋常,但配合著現代JavaScript直譯器,其實並不清楚他們有沒有任何效能影響。

//將length屬性儲存到一個本地變數中
for(let i = 0, len = letters.length; i < len; i++){
//迴圈體保持相同。
}
//反向迭代陣列。
for(let i = letters.length; i >= 0; i__){
//迴圈體內容相同。
}

這些例子假設該陣列為連續陣列,同時所有元素都是合法的。如果事情沒有像你預想中那樣發生,你就應該在使用他們之前驗證以下。如果你想跳過undefined和不存在的元素,你可以這麼寫。

for(let i = 0; i < a.length; i++){
if(a[i] === undefined) continue; //跳過undefined和空值。
// 迴圈體在這。
}

7.7 Multidimensional Arrays

JavaScript並不會真正支援多維陣列。但是你可以透過建立包含陣列元素的陣列來近似模擬出一個。訪問一個二維陣列的方式是透過array[][]的方式來訪問。舉個例子,比如說現在有一個變數matrix是一個二維陣列,matrix中每一個元素都是一個數字陣列。如果你嘗試去訪問其中的某個數值,你可以這樣寫:matrix[x][y].這裡是一個具體的例子,透過二維陣列表示一個乘法表。

//建立一個多維陣列
let table = new Array(10); //陣列中一共有10行。
for(let i = 0; i < table.length; i++){
table[i] = new Array(10) // 每一行有十列。
}
// 初始化該陣列。
for(let row = 0; row < table.length; row++){
for(let col = 0; col < table[row].length; col ++){
table[row][col] = rowcol;
}
}
//透過多維陣列來計算5
7
table[5][7]

7.8 Array Methods

前面的內容主要在講JavaScript陣列的基本語法,然而陣列中定義的方法才是使得其強大的原因。下面的內容重點講解了這些方法。在你理解這些方法的時候,一定要記得區分好,一類是會對陣列產生作用,一類卻不會。很多的陣列方法都會返回一個陣列,有時候這是一個新陣列,原陣列沒有受到任何影響,有時候,一些方法會修改源陣列,同時返回源陣列的引用。

下面這些小章節概括了很多與之相關的陣列方法。

  • 迭代器方法迭代出陣列中的每個元素,通常帶著陣列中每個元素被喚起多次。
  • 棧和佇列方法,增加或者刪除元素從陣列的起點和終點。
  • subarray methods 用於提取,刪除,插入,填補,賦值連續的大型陣列區域。
  • 搜尋和排序方法用於定位以及對陣列進行排序。

下面這些小章節中同樣概述了一些靜態方法和很多亂七八糟的方法,可以用於連線,或者將陣列轉變成字串。

7.8.1 Array Iterator methods

本章中的這些方法迭代所有的陣列元素,透過將陣列元素作為引數傳給回撥函式。可用於迭代,過濾,測試,map,縮小。

在詳細解釋之前,我們應該對這些方法有一種全域性的觀念。所有的這些方法都將接受一個函式作為其第一個引數,同時在迭代每一個元素時被喚起一次。如果這個陣列屬於間斷陣列,在間斷點上會跳過。在大多數情況下,你的函式會帶著三個引數被喚醒,陣列元素值,陣列元素索引,陣列的引用。一般情況下你只需要第一個引數,而第二個第三個引數很少用到。

在這章中討論到的大多數迭代器方法可以接受第二個引數,如果宣告瞭,第一個引數(函式)會作為第二個引數的一個屬性值。也就是說。第二個引數會變成第一個函式的this引用。函式的返回值通常很重要。但是不同的方法有著不同的方式去處理這點。這些方法通常不會對呼叫者產生影響,當然你傳入的函式是可以修改源陣列的。

這些個方法裡,每一個都接受一個函式作為其第一個引數,同時你可以直接將函式定義在方法體內,使用箭頭函式是一種非常常見的方法。

forEach()
forEach()方法遍歷陣列中所有元素,每迭代一次就喚起一次函式。前面以及解釋過,你將一個函式傳入方法中充當第一個元素,forEach()會喚醒這個函式,同時將三個引數傳入其中。元素值,元素索引,陣列引用。如果你僅僅只是關心陣列內的值,那就在寫函式的時候,只提供第一個引數,其他引數忽略掉就行了。

let data = [1,2,3,4,5],sum = 0;
//計算數值之和
data.forEach(value =>{ sum += value;});
//現在遞增陣列中所有元素。
data.forEach(function(v, i, a){ a[i] = v + 1}); //data == [2,3,4,5,6]

要注意的是,forEach()並沒有提供中斷列舉的功能,也就是說,在列舉完所有元素之前,並沒有像普通loop中的迴圈中的break表示式。

map()
Map()方法將所有的陣列元素一個個傳給你宣告的函式中,並將回撥函式的返回值組合成陣列作為該方法的返回值。舉個例子:

let a = [1,2,3];
a.map(x => xx) // => [1,4,9]: 該函式取得輸入的x後返回xx

map()方法的運作方式與forEach()基本類似,不同之處在於map()傳入的回撥函式應該返回一個值。還有就是它會返回一個新陣列,並且源陣列不會被改變。在間斷點上你的函式並不會被喚起,但是新陣列中原先的間斷點會依舊存在。新陣列中依然會保留原有的length與原有的間斷點。

filter()
Filter()方法會返回一組元素。你提供的函式應該屬於一個predicate,一個僅返回true或者false的函式。或者說可以轉為true或false的值。predicate就像forEach()和map()那樣被呼叫。如果返回值為true或者被轉為了true。這時候這個元素會被分配到一個小組裡,最後變成返回的新陣列中的一部分。舉個例子。

let a = [5,4,3,2,1];
a.filter(x => x < 3) // => [2,1]; 小於三的數值。
a.filter((x,i) => i%2 === 0); // => [5,3,1];所有的奇數

需要注意的是,filter()會跳過所有間斷點,同時其返回值將永遠是連續陣列。當你有消除間斷點的需求時,可以透過這個方法來實現。

let dense = sparse.filter(() => true);

同時如果你同時有消除間斷點和移除所有undefined以及null元素的需求,你可以同樣使用filter,就像這樣:

a = a.filter(x => x !== undefined && x !== null);

find()和findIndex()
find()和findIndex()方法和filter()很像,遍歷所有元素並找出那些讓你的predicate函式返回truthy值的元素,但不一樣的點在於,這兩個方法會在遇到第一個讓predicate函式返回true的時候,終止遍歷。這時候,find()返回該元素,findIndex()返回該元素的索引。如果沒有找到這個元素,find()返回undefined,findIndex()返回-1:

let a = [1,2,3,4,5]
a.findIndex(x => x === 3) // => 2; 數值3位於索引2
a.findIndex(x => x <0 ) // => -1; 索引不存在負數
a.find(x => x % 5 === 0 ) // => 5; 這是5的倍數
a.find(x => x % 7 === 0) // => undefined; 沒有找到七的倍數。
every()和some()
every()和some()方法屬於陣列predicates:他們通常依賴於你宣告的predicate function。返回值為true或false。

every()方法就像數學中的"for all"數學符號。它僅僅在你陣列中所有的元素都返回true時才會返回true。

let a = [1,2,3,4,5]
a.every(x => x < 10) // => true: 所有的值都小於10。
a.every(x => x % 2 === 0) //=> false: 並不是說所有的值都是偶數。

some()方法相當於數學符號存在:只要predicate function返回過true,some()就會返回true。且只有當所有經過predicted function都返回false時,它才會返回false。

let a = [1,2,3,4,5]
a.some(x => x % 2 === 0) // => true; a陣列記憶體在偶數。
a.some(isNaN) // => // => false; a陣列沒有非數字元素。

要注意無論是every()或者是some(),它們在知曉應該返回什麼布林值的時候,會立即退出。例如every()存在元素的predicate function返回返回false的時候,立即就返回false了,而不會接著執行後面未列舉的元素,some()也一樣,只要遇到有predicate function 返回了true,會立刻返回true,而不會等到最後列舉完所有元素再返回。同樣記住這點,根據規定,對於一個空陣列,every()返回true,some()返回false。

reduce() 和 reduceRight()
reduce()和reduceRight()方法將陣列中的元素組合起來。透過你提供的函式,產出一個數值。這在函數語言程式設計中是一種常見的操作。同時遵循兩個概念"inject"和"fold".來個例子會更好理解。

let a = [1,2,3,4,5];
a.reduce((x,y) => x+y, 0) // =>15; 所有元素值之和。
a.reduce((x,y) => x*y, 1) // =>120; 所有元素之積。
a.reduce((x,y) => (x > y) ? x : y) // => 5; 陣列中的最大值。

reduce()可以插入兩個引數,第一個是執行reduction操作的函式。reduction 函式的任務是透過某種操作將兩個值組合或者化為一個一個值,然後返回它。在前面那個例子中,reduction 函式透過相加,相乘,排序將兩個值組合到一起。第二個引數作為一個初始值傳給該reduction函式。

reduce()和forEach()的回撥函式的使用方式完全不一樣,元素值,元素索引,陣列引用將作為第二,第三,第四個引數。第一個引數是目前被合併後的值。在第一次喚起回撥函式時,函式的第一個引數是你傳入reduce()方法的第二個引數。在隨後的喚起中,它的值是前一輪喚起返回的值。在第一個例子中,reduction 函式第一次以0和1為引數被呼叫了,隨後返回1,第二次以1和2喚起,返回3,以此類推,最終獲得15,作為reduce()的返回值。

你可能注意到在第三次呼叫reduce()的時候,僅僅只傳入了一個函式,並沒有宣告初始值。而第一次的函式回撥時,reduction function會是陣列的第一個值,而第二個引數會是陣列的第二個值。在和與積的例子裡,我們完全可以忽略傳入初始值。

以一個空陣列去呼叫reduce()方法,且不提供初始值,將會導致一個TypeError錯誤。如果你以單值方式呼叫reduce(),可以是隻含有一個值的陣列,不提供初始值的情況;也可以是空陣列,但是提供了一個初始值。這兩種情況下,陣列會直接返回該值,而不會喚起reduction 函式。

reduceRight()的用法和reduce()基本一樣,但是它從陣列的高索引位置開始工作,而不是從低到高。你可能會有需要從右向左計算的需求,舉例子:

//計算2(34). 指數計算從右邊開始計算。
let a =[2,3,4];
a.reduceRight((acc,val) => Math.pow(val,acc)) // => 2.4178516392292583e+24

值得注意的是,reduce()和reduceRight()可以接受一個額外的引數,該引數會作為reduction函式內部的this。看看Function.bind()8.7.5,如果你需要某個reduction函式作為某個物件的方法被呼叫。

上述例子中到目前為止僅涉及到數字型別,但這僅僅只是為了方便理解。reduce(),reduceRight()絕非僅僅只適用於數學計算。任何可以實現合二為一的函式,且合成後的值與源值型別相同,都可以作為reduction函式。換句話講,使用reductions來實現演算法可能會將你的程式碼變得複雜且難以理解。同時你可能會發現透過普通的迴圈來實現,會更加容易閱讀,書寫與思考。

7.8.2 Flattening arrays with flat() and flatMap()

在ES2019中,flat()方法建立並且返回一個與呼叫它的陣列相同的陣列。除了一些本身就是陣列的元素,其他普通元素都將被展開到返回的陣列中。舉個例子:

[1,[2,3]].flat() // => [1,2,3]
[1,[2,[3]]].flat() // => [1,2,[3]]

當不傳入任何引數去呼叫這個方法的時候,flat()只會將摺疊一次的陣列展開。如果你想更深入地展開元素,那便傳入一個整數引數:

let a = [1,[2,[3,[4]]]];
a.flat(1) // => [1,2[3,[4]]];
a.flat(2) // => [1,2,3,[4]];
a.flat(3) // => [1, 2, 3, 4];
a.flat(4) // => [1, 2, 3, 4];

flatMap()和map()的用法差不多,除了一點,返回值中的陣列將自動展開到新陣列中。其實也就相當於array.map(f).flat():

let phrases = ["hello world", "the definitive guide"];
let words = phrases.flatMap(phrase => phrase.split(" "));
words // => ["hello","world","the","definitive","guide"];

你可以將flatMap()簡單理解為map()和flat()的組合,它使得陣列中的每個元素都可以拆解成多個元素,而如何拆解將由你傳入的函式決定。在特定情況下,flatMap()可以將輸入的元素輸出為空陣列,於是在展開後的陣列中什麼也沒有:

//將所有非負數轉為平方根。
[-2, -1, 1, 2].flatMap(x => x < 0 ? [] : Math.sqrt(x)) //=> [1, 2**0.5]; 0.5表示指數,twice*將表示指數表示式。

7.8.3 Adding arrays with concat()

Concat()方法會返回一個新建立的陣列,這個陣列會包含呼叫它的源陣列。以及那些被傳入concat()中的引數。如果其中有引數屬於陣列,那麼這兩個陣列便會合並。但是concat()並不會將多層陣列完全展開掉,同時也不會對原陣列造成影響。

let a = [1,2,3];
a.concat(4,5) // => [1,2,3,4,5]
a.concat([4,5],[6,7]) // => [1,2,3,4,5,4,5,6,7]; 陣列被展開。
a.concat(4,[5,[6,7]]) // => [1,2,3,4,5,[6,7]]; 多層陣列保留。
a; // => [1,2,3]; 源陣列無變化。

concat()會生成一份源陣列的備份。在許情況下,這麼做是正確的,但這個操作非常的expensive.如果你發現你寫的程式碼就像a = a.concat(x),你應該考慮用push()或者splice()而不是新建一個陣列。

7.8.4 Stacks and /Queues with push(), pop(),shift(),and unshift()

push()和pop()方法允許你像操作一個棧那樣操作一個陣列。push()方法將一個或多個元素新增到一個陣列的尾部,同時返回陣列的length值。但和concat()有所不同,push()不會展開你提供的陣列。pop()方法的操作相反,它刪除陣列中最後一個元素,同時改變length值,這兩個方法會修改陣列原值。

透過組合這兩個方法,你可以實現一個first-in, last-out stack。舉個例子。

let stack = []; //stack == []
stack.push(1,2); //stack == [1,2]
stack.pop(); //stack == [1]; return 2
stack.push(3); //stack == [1,3]
stack.pop(); //stack == [1]; return 3
stack.push([4,5]) // stack == [1,[4,5]]
stack.pop() //stack == [1]; return [4,5]
stack.pop() //stack == []; return 1

push()方法並不會展開你提供的函式,但如果你想要將一個陣列元素全部展開到另一個陣列中,你可以使用展開運算子。

a.push(...values);

unshift()和shift()方法的作用和push(),pop()相似,不同之處在於,unshift()將元素新增到陣列首部,同時將所有元素向更高位置的索引移動,最後返回陣列的length值。shift()刪除數中第一個元素,並將其後所有元素的索引向更低位置移動一位,填補空缺的位置,最後返回被刪除陣列的值。你可以使用shift(),unshift()來實現一個stack,但這沒有push(),pop()用起來高效。因為在你每次新增或者刪除陣列元素之後,你需要左右移動陣列的index。你也可以透過組合push()和shift()來實現一個queen(First in, First out)。

let q = [];
q.push(1,2); //q == []
q.shift(); //q == [2]; return 1
q.push(3); //q == [2,3]; return 2
q.shift(); //q == [3]; return 2
q.shift(); //q == []; return 3

這裡還有一個關於unshift()的特徵值得引起注意,那就是當你一次unshift多個元素進陣列中時,它們會被一次性全部插入進去,這就導致插入的陣列元素順序與一個一個元素插入的時候是完全顛倒的。

let a = []; //a == []
a.unshift(1) //a == [1]
a.unshift(2) //a == [2,1]
a = []; //a == []
a.unshift(1,2); // a == [1,2]

7.8.5 Subarrays with slice(),splice(),fill(),and copyWithin()

陣列定義了很多方法專門對陣列的連續區域,部分,或者裁切。下面這部分內容針對對陣列的提取,替換,填補,以及複製。

slice()
slice()方法會返回一個陣列的片段,或者說部分陣列,它的兩個引數指明瞭這個片段的起點和重點。這個片段會包含這個第一個引數宣告的元素,和隨後能夠取得的元素,但不包含第二個引數所宣告的元素。相當於數學中的[a,b).如果你僅僅只傳入一個元素,那麼,這個片段會包含第一個元素以及隨後的所有元素。如果某個引數為負數,它對應的索引位置會進行換算,-1代表陣列中索引值最大的元素,以此類推,-2代表length-2,slice不會篡改它的呼叫者。下面是一些簡單的例子:

let a = [1,2,3,4,5];
a.slice(0,3); //Returns [1,2,3]
a.slice(3); //Return [4,5]
a.slice(1,-1); //Return [2,3,4]
a.slice(-3,-2); //Return [3]

splice()
splice()是一個綜合性大的方法,可以用於插入或者刪除一個陣列的元素。和slice(),concat(),splice()的操作會影響其呼叫者。splice()和slice()擁有相同的名字,但卻是兩個截然不同的操作。

splice()可以刪除,也可以插入元素,或者同時做這兩件事。陣列進行插入或者移除操作後,相應的元素index會進行修改以保持陣列元素的連續性。方法中的第一個引數確定了在進行插入,移除操作時,開始的位置。第二個引數指明瞭操作量。(注意一點,這裡的引數和slice不一樣,slice的兩個引數指明起點與終點。但在splice()中則指的是,起點與從起點開始,應該操作之後的元素數量),如果第二個引數被忽略,起點後所有元素都會被刪除掉。splice()的返回值是被刪除掉的元素。或者一個空陣列(在沒有刪除任何元素的情況下)。舉個例子:

let a = [1,2,3,4,5,6,7,8];
a.splice(4); // => [5,6,7,8]; a 現在的值為[1,2,3,4]
a.splice(1,2); // => [2,3]; a現在的值為[1,4]
a.splice(1,1); // => [4]; a現在的值為[1]

開頭的兩個引數指明瞭刪除陣列中哪些元素,而兩個引數之後的元素則會成為陣列中被插入的元素。而插入的位置會和第一個引數有關。舉個例子:

let a = [1,2,3,4,5];
a.splice(2,0,'a','b'); // => [1,2,'a','b',3,4,5];
a.splice(2,2,[1,2],3); // => [1,2,[1,2],3,3,4,5];

還是要提醒以下,和concat()不一樣,splice()是直接將引數插入陣列,如果傳入的引數是一個陣列,那麼這個陣列會直接插入,而非裡面的值。

fill()
fill()方法重置一個陣列中的所有元素,或一部分,到一個特定的值。它會修改陣列本身,同時返回修改後的陣列:

let a = new Array(5); // 建立一個長度為5的空陣列。
a.fill(0); // =>[0,0,0,0,0];將所有的元素設定為5。
a.fill(9,1); // => [0,9,9,9,9];將索引為1以及其後的所有元素設定為9。
a.fill(8,2,-1) // => [0,9,8,8,9];將[2,3]區域內的元素重置為8

fill()的第一個引數指明將陣列重置為何值,第二個引數指明重置操作的起點,且可以忽略掉,第三個引數指明瞭操作結束的位置,注意不包含此位置。你可以透過負數來指明陣列反方向的相對位置,就像你在使用slice那樣。

copyWithin()

7.8.6 Array Searching and sorting Methods

Arrays 實現了indexOf(),lastIndexOf(),和includes()方法,這些方法和string中的方法很相似。同時還存在sort(),reverse()方法用於改變陣列的順序。這些方法在下面這些小結中被講到。

indexOf() and lastIndexOf()
indexOf()和lastIndexOf()透過提供的特殊值在陣列中搜尋一個特定的元素。同時返回第一次找到這個元素的位置。如果沒有找到則返回-1。indexOf()從陣列索引的開始搜尋到結束的位置。lastIndexOf()從陣列結束的位置開始向前找。

let a = [0,1,2,1,0];
a.indexOf(1); // => 1: a[1] is 1
a.lastIndexOf(1); // => 3: a[3] is 1
a.indexOf(3); // => -1:no element has value 3.

indexOf()和lastIndexOf()透過“===”來比較你陣列中的屬性。如果你的陣列中存在元素是物件,便會透過比較物件引用來確定是否相等。如果你想透過物件內容來確定兩物件是否相同,改用find方法,透過你自己的predicate函式來查詢。

indexOf()和lastInndexOf()還存在第二個引數,藉此指明應該從陣列的哪個位置開始,預設為0和length-1.負數索引也是可以被使用的,同時被當作一個offset從陣列的尾部。因為它們和slice()方法有關,-1將表示陣列中的最後一個元素。

下面的程式實現了查詢一個陣列中的指定元素,同時返回存在該元素的所有索引的集合。這很好地解釋瞭如何透過第二個引數來找到比第一個匹配值更遠位置的索引。

找到所有的匹配值,並且返回由這些索引所組成的陣列。
function findall(a, x){
let result = [], len = a.length, pos = 0;
while(pos < len){ //當存在元素給我去找的時。
pos = a.indexOf(x, pos); //找
if(pos == -1) break; // 如果沒有找到,退出!!!
results.push(pos); //否則,存起來。
pos++; // 開始下一次的搜尋。
}
return results; // 返回索引陣列。
}

String中的indexOf()和lastIndexOf()方法和它的工作原理差不多,但區別在於,如果你給string的這些方法第二個引數傳負數,會等效於0。

includes
在ES2016中有一個includes()方法,只需要提供一個引數,也僅僅只會返回一個布林值。在陣列包含你提供的引數時,返回true。它不會指明這個值在陣列中的位置,僅僅只告訴你有沒有。It is efficient representation test for arrays。注意,arrays並不是一個高效的set表示方式。如果你要操作的資料量不是一個兩個,你應該使用一個真正的Set物件。

includes()方法相較於indexOf()有一個很不同的點。indexOf()執行時的演算法和'==='一樣。而這個比較演算法會將not-a-number的值看作是完全不同的值,包括他自己。而includes()使用一個稍微有點不同的比較演算法來考慮這個問題,所以它可以查詢陣列中是不是存在NaN值。所以,結論就是indexOf不可以查詢到陣列中是不是存在NaN值,但是includes()可以。

let a = [1,true,3,NaN];
a.includes(true) // => true
a.includes(2) // => false
a.includes(NaN) // => true
a.indexOf(NaN) // => -1; indexOf不能找到NaN

sort()
sort()對陣列元素進行排序,同時返回排序後的值。當你對其進行無參呼叫的時候,會透過字母順序進行排序。(如果有需要的話,暫時將其轉換成String型別進行比較。):

let a = ["banana","cherry","apple"];
a.sort(); // a == ["apple","banana","cherry"];

如果陣列中含有undefined,這些元素會被放置在陣列的尾部。

為了能夠將陣列以其他方式進行排序,你可以傳入一個特定的函式進行比較。這個函式決定了,函式的兩個引數,哪一個應該排在陣列的前面。如果第一個引數在第二個引數前,函式就應該返回一個負數,如果這個引數應該在陣列的後方,則函式應該返回一個正數。同時,如果這兩個引數旗鼓相當,那就返回0。舉個例子,如果你想以數值大小來排序,而非字元大小,你可以這麼做。

let a = [33,4,1111,222];
a.sort(); // a == [1111,222,33,4]
a.sort(function(a,b){ //傳入比較函式
return a - b; // 根據實際情況返回<0,>0,=0。
});
a.sort((a,b) => b-a); // a == [1111,222,33,4]; 倒序

另一個例子中,你可能想透過將字串全部轉成小寫體,使得比較時不被字元的大小寫影響,

let a = ["ant", "Bug", "cat", "Dog"];
a.sort(); // a == ["Bug", "Dog", "ant", "cat"]; 大小寫敏感。
a.sort(function(s,t){
let a = s.toLowerCase();
let b = t.toLowerCase();
if(a < b) return -1;
if(a > b) return 1;
return 0;
}) // a == ["ant", "Bug", "cat", "Dog"]; 大小寫不敏感。
reverse()
reverse()方法將陣列的順序顛倒,同時返回這個反轉後的陣列。它在原有陣列的基礎上操作,換句話說,它並不會建立一個新陣列來儲存這些重新排列後的陣列,而是在已有陣列的基礎上進行重新排序。
let a = [1,2,3];
a.reverse();

7.8.7 Array to String Conversions.

Array類提供了三個方法用於將陣列轉變成字串。這些方法通常在你想要列印日誌或者錯誤資訊時候會用得到。(如果你是想將其儲存為文字狀態,方便在以後使用它,也可以透過JSON.stringify()來序列化該陣列)。

join()方法將陣列中所有元素轉成string,並把他們連線起來,最後將其返回。你可以透過一個可選的引數來指定字元之間的分隔方式,如果沒有宣告這個引數,‘,’會作為分隔符。

let a = [1,2,3];
a.join() // => "1,2,3"
a.join(" ") // => "1 2 3"
a.join("") // => "123"
let b = new Array(10); //一個長度為10的空陣列。
b.join("-") // => "---------":

join()方法是String.split()方法相反方法。它透過拆分一個String將其變成陣列。

Arrays,和所有的JavaScript物件一樣,也擁有toString()方法。對於一個陣列,呼叫這個方法就像無參呼叫join()方法那樣。

[1,2,3].toString() // => "1,2,3"
["a","b","c"].toString // => "a,b,c"
[1,[2,"c"]].toString // => "1,2,c"

但是還是要提一嘴,結果中不會包含方括號或者其他分隔符。

toLocalString() is the localized version of toString(). It converts each array element to a string by calling the toLocaleString() method of the element, and then it concatenates the resulting strings using a locale-specific(and implementation-defined) separator string.

Static Array Functions

關於對陣列方法的補充,Array 類同樣定義了三個靜態方法,你可以直接透過陣列的構造方法來呼叫。Array.of()和Array.from()是構造新陣列的工廠方法。在7.1.4和7.1.5被描述過。

另一個靜態方法是Array.isArray(),這個方法可以查明一個值是否是一個陣列:

Array.isArray([]) // => true
Array.isArray({}) // => false

7.9 Array-Like Objects

我們已經瞭解到,Javascript 陣列存在一些特俗的性質,是普通物件沒有的:

  • length屬性會被自動更新。
  • 減小length值會導致陣列被修改。
  • 繼承了Array.prototype的很多屬性
  • Array.isArray()可以識別陣列。

這些屬性是使得JavaScript陣列區別於其他普通物件的特徵。但它們並不是定義一個陣列所必需的特徵。把任何存在一個整數型的length屬性與一系列以非負整數為屬性名的屬性成為陣列是可以理解的。

這些“array-like”的物件在實操中偶爾會出現。雖然說你不能直接在他們身上喚起哪些針對Array陣列的方法,或者期待著它的length也存在特殊的性質。然而你任舊可以像普通的一個陣列那樣去遍歷它。結果是很多針對陣列的演算法針對它們而言完全通用。尤其在當你的演算法只是將這個陣列視為可讀的陣列時,或者length屬性根本沒有機會被修改時。

下面的程式碼操作著一個普通數字,將元素新增到裡面使其表現得就像一個陣列那樣。然後遍歷這個生成後的假陣列。

let a = {}; // 以一個普通的空陣列開始

//將元素新增到裡面去,使它變成一個"array-like"
let i = 0;
while(i < 10){
a[i] = i*i;
i++;
}
a.length = i;

//現在就像對待一個陣列那樣去遍歷它。
let total = 0;
for(let j = 0; j < a.length; j++){
total += a[j];
}

在客戶端的JavaScript中,很多用於操作HTML documents的方法(比如說document.querySelectorAll())都是基於"array-like"物件的。這裡有一個函式,可能你會用它去測試一個物件是不是一個“array-like”物件。

//確定o是不是一個"array-like" 物件。
//Strings 和 functions存在length屬性,但會被typeof test排除。在client-side JavaScript中,DOM text 節點也有一個數字型別的length屬性。也可能需要透過o.nodeType !== 3被排除掉。
function isArrayLike(o){
if(o && // o一定非空,undefined,etc.
typeof o === "object" && //o 一定是一個物件
Number.isFinite(o.length) && // o.length是一個finite數字
o.length >= 0 && // o.length是一個非負數
Number.isInteger(o.length) && //o.length 是一個整數
o.length < 4294967295 //o.length一定小於2^32-1
){
return true; //o是一個array-like
}else{
return false; // o不是一個array-like
}
}

後面會有一章我們學到String的表現和Array很相似。然而,這類測試通常不會將其視為array-like,它們最好作為string來操作,而不是陣列。

大多數JavaScript陣列方法被故意設計成通用的,所以它們可以直接應用到Array-like物件身上還有真正的陣列。由於array-like物件並不直接從Array.prototype中繼承方法,你不可以直接呼叫陣列的方法。而是間接地透過Function.call方法,however:

let a = {"0":"a", "1":"b", "2":"c", length:3}; //一個array-like陣列。
Array.prototype.join.call(a,"+") // => "a+b+c"。
Array.prototype.map.call(a, x => x.toUpperCase()) // => ["A","B","C"]
Array.prototype.slice.call(a, 0) // => ["a","b","c"]: 真陣列複製。
Array.prototype.from(a) // => ["a","b","c"]: 複製陣列的簡單方式。

倒數第二行的程式碼呼叫了陣列的slice()方法作用於一個array-like物件,以確保將其轉成一個真陣列。這是一個很常用的技巧,而且存在於許多過時的程式碼中,但現在透過Array.from()會更加簡單。

7.10 Strings as Arrays.

Javascript strings 表現得就像一個僅讀的UTF-16方式編碼的字符集合。除了charAt()方法,你還可以透過方括號的語法來獲取其中的單個字元:

let s = "test";
s.charAt(0) // => "t"
s[1] // => "e"

typeof運算子任然返回“string”,同時Array.isArray()方法也返回false。

透過索引去訪問的好處在於,我們不需要再透過charAt()方法去訪問了,相比較這種方式會更加具有可讀性,也可能會更高效。Strings表現得就像一個陣列,於是陣列的通用方法同樣適用於String,舉個例子:

Array.prototype.join.call("JavaScript"," ") // => "J a v a S c r i p t"

但請記住,Strings是一個不可修改的值。所以當它們被當作陣列來使用的時候,它們屬於僅可讀的陣列。陣列中的其他方法比如說push(),sort(),reverse(),還有splice()這些會直接修改陣列本身的方法並不會生效。當你嘗試做這些事情的時候,並不會報錯,僅僅是失敗而已。

Summary

這章深入講解了JavaScript中的陣列,包括難以理解的間斷陣列與array-like物件。

  • 陣列字面量透過一對方括號來表示一個陣列。
  • 單個的陣列元素透過方括號包裹索引值來訪問
  • for/of 迴圈和 ... 展開運算子是ES6中引入的,並且對於列舉陣列元素非常有用。
  • Array類定義了很多方法用於運算元組,同時你應該保證對這些api爛熟於心。

相關文章