玩轉 JavaScript 數組
今天來看點 JavaScript 基礎:數組,看看數組有哪些相關的知識點~
一、數組基礎
1. 數組概述
數組是我們最常用的數據類型之一,ECMAScript 數組跟其他語言的數組一樣,都是一組有序的數據,但跟其他語言不同的是,數組中每個槽位可以存儲任意類型的數據。除此之外,ECMAScript 數組的長度也是動態的,會隨着數據的增刪而改變。
數組是被等分爲許多小塊的連續內存段,每個小塊都和一個整數關聯,可以通過這個整數快速訪問對應的小塊。除此之外,數組擁有一個 length 屬性,該屬性表示的並不是數組元素的數量,而是指數組元素的最高序號加 1。
let a = [1, 2, 3];
a.length === 3 // true
在 ES6 中,可以使用擴展運算符(...)來獲取數組元素:
let a = [1, 2, 3];
let b = [0, ...a, 4]; // [0, 1, 2, 3, 4]
2. 數組創建
數組的創建方式有以下兩種。
(1)字面量
最常用的創建數組的方式就是 ** 數組字面量,** 數組元素的類型可以是任意的,如下:
let colors = ["red", [1, 2, 3], true];
(2)構造函數
使用構造函數創建數組的形式如下:
let array = new Array();
如果已知數組元素數量,那麼就可以給構造函數傳入一個數值,然後 length 屬性就會被自動創建並保存這個值,比如創建一個長度爲 10 的數組:
let array = new Array(); // [undefined × 10]
這樣,就可以創建一個長度爲 10 的數組,數組每個元素的值都是 undefined。
還可以給 Array 構造函數傳入要保存的元素,比如:
let colors = new Array("red", "blue", "green");
這就出現問題了,當我們創建數組時,如果給數組傳入一個值,如果傳入的值是數字,那麼就會創建一個長度爲指定數字的數組;如果這個值是其他類型,就會創建一個質保函該特定製度額數組。這樣我們就無法直接創建一個只包含一個數字的數組了。
Array 構造函數根據參數長度的不同,有如下兩種不同的處理方式:
-
new Array(arg1, arg2,…):參數長度爲 0 或長度大於等於 2 時,傳入的參數將按照順序依次成爲新數組的第 0 至第 N 項(參數長度爲 0 時,返回空數組);
-
new Array(length):當 length 不是數值時,返回一個只包含 length 元素一項的數組;當 length 爲數值時,length 最大不能超過 32 位無符號整型,即需要小於 2,否則將拋出 RangeError。
在使用 Array 構造函數時,也可以省略 new 操作符,結果是一樣的:
let array = Array();
(3)ES6 構造器
鑑於數組的常用性,ES6 專門擴展了數組構造器 Array ,新增了 2 個方法:Array.of 和 Array.from。Array.of 用得比較少,Array.from 具有很強的靈活性。
1)Array.of
Array.of 用於將參數依次轉化爲數組項,然後返回這個新數組。它基本上與 Array 構造器功能一致,唯一的區別就在單個數字參數的處理上。
比如,在下面的代碼中,可以看到:當參數爲 2 個時,返回的結果是一致的;當參數是一個時,Array.of 會把參數變成數組裏的一項,而構造器則會生成長度和第一個參數相同的空數組:
Array.of(8.0); // [8]
Array(8.0); // [empty × 8]
Array.of(8.0, 5); // [8, 5]
Array(8.0, 5); // [8, 5]
Array.of('8'); // ["8"]
Array('8'); // ["8"]
2)Array.from
Array.from 的設計初衷是快速基於其他對象創建新數組,準確來說就是從一個類似數組的可迭代對象中創建一個新的數組實例。其實,只要一個對象有迭代器,Array.from 就能把它變成一個數組(注意:該方法會返回一個的數組,不會改變原對象)。
從語法上看,Array.from 有 3 個參數:
-
類似數組的對象,必選;
-
加工函數,新生成的數組會經過該函數的加工再返回;
-
this 作用域,表示加工函數執行時 this 的值。
這三個參數裏面第一個參數是必選的,後兩個參數都是可選的:
var obj = {0: 'a', 1: 'b', 2:'c', length: 3};
Array.from(obj, function(value, index){
console.log(value, index, this, arguments.length);
return value.repeat(3); //必須指定返回值,否則返回 undefined
}, obj);
結果如圖:
以上結果表明,通過 Array.from 這個方法可以自定義加工函數的處理方式,從而返回想要得到的值;如果不確定返回值,則會返回 undefined,最終生成的是一個包含若干個 undefined 元素的空數組。
實際上,如果這裏不指定 this,加工函數就可以是一個箭頭函數。上述代碼可以簡寫爲以下形式。
Array.from(obj, (value) => value.repeat(3));
// 控制檯打印 (3) ["aaa", "bbb", "ccc"]
除了上述 obj 對象以外,擁有迭代器的對象還包括 String、Set、Map 等,Array.from
都可以進行處理:
// String
Array.from('abc'); // ["a", "b", "c"]
// Set
Array.from(new Set(['abc', 'def'])); // ["abc", "def"]
// Map
Array.from(new Map([[1, 'ab'], [2, 'de']])); // [[1, 'ab'], [2, 'de']]
3. 數組空位
當我們使用數組字面量初始化數組時,可以使用一串逗號來創建空位,ECMAScript 會將逗號之間相應索引位置的值當成空位,ES6 重新定義了該如何處理這些空位。
我們可以這樣來創建一個空位數組:
let array = [,,,,,];
console.log(array.length);
console.log(array)
運行結果如下:
ES6 新增的方法和迭代器與早期版本中存在的方法的行爲不同,ES6 新增方法普遍將這些空位當成存在的元素,只不過值爲 undefined,使用字面量形式創建如下數組:
let array = [1,,,5];
for(let i of array){
console.log(i === undefined)
}
// 輸出結果:false true true false
使用 ES6 的 Array.form 創建數組:
let array = Array.from([1,,,5]);
for(let i of array){
console.log(i === undefined)
}
// 輸出結果:false true true false
而 ES6 之前的方法則會忽略這個空位:
let array = [1,,,5];
console.log(array.map(() => 10))
// 輸出結果:[10, undefined, undefined, 10]
由於不同方法對空位數組的處理方式不同,因此儘量避免使用空位數組。
4. 數組索引
在數組中,我們可以通過使用數組的索引來獲取數組的值:
let colors = new Array("red", "blue", "green");
console.log(array[1]) // blue
如果指定的索引值小於數組的元素數,就會返回存儲在相應位置的元素,也可以通過這種方式來設置一個數組元素的值。如果設置的索引值大於數組的長度,那麼就會將數組長度擴充至該索引值加一。
數組長度 length 的獨特之處在於,他不是隻讀的。通過 length 屬性,可以在數組末尾增加刪除元素:
let colors = new Array("red", "blue", "green");
colors.length = 2
console.log(colors[2]) // undefined
colors.length = 4
console.log(colors[3]) // undefined
數組長度始終比數組最後一個值的索引大 1,這是因爲索引值都是從 0 開始的。
5. 數組判斷
一個很經典的 ECMASript 問題就是如何判斷一個對象是不是數組,下面來看常用的數據類型檢測的方法。
在 ES6 之前,至少有如下 5 種方式去判斷一個對象是否爲數組。
- 通過 Object.prototype.toString.call() 做判斷:
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
- 通過 constructor 做判斷:
obj.constructor === Array;
- 通過 instanceof 做判斷:
obj instanceof Array
- 通過 Array.prototype.isPrototypeOf 做判斷:
Array.prototype.isPrototypeOf(obj)
- 通過基於 getPrototypeOf 做判斷:
Object.getPrototypeOf(obj) === Array.prototype;
如果 obj 是一個數組,那麼上面這 5 個判斷全部爲 true,推薦通過 Object.prototype.toString 去判斷一個值的類型。
ES6 新增了 Array.isArray
方法,可以直接判斷數據類型是否爲數組:
Array.isArrray(obj);
如果 isArray 不存在,那麼 Array.isArray
的 polyfill 通常可以這樣寫:
if (!Array.isArray){
Array.isArray = function(arg){
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
二、數組方法
數字就像是一個森林,裏面有很多函 “樹”,有些方法純淨如水,並不會改變原數組,有些則會改變原數組。
-
改變原數組的方法:fill()、pop()、push()、shift()、splice()、unshift()、reverse()、sort();
-
不改變原數組的方法:concat()、every()、filter()、find()、findIndex()、forEach()、indexOf()、join()、lastIndexOf()、map()、reduce()、reduceRight()、slice()、some。
1. 複製和填充方法
ES 提供了兩個方法:批量複製方法 copeWithin(),以及填充數組方法 fill()。這兩個方法的簽名類似,都需要指定已有數組實例上的一個範圍,包含開始索引,不包含結束索引。下面就分別來看一下這兩個方法。
(1)fill()
使用 fill() 方法可以向一個已有數組中插入全部或部分相同的值,開始索引用於指定開始填充的位置,它是可選的。如果不提供結束索引,則一直填充到數組末尾。如果是負值,則將從負值加上數組的長度而得到的值開始。該方法的語法如下:
array.fill(value, start, end)
其參數如下:
-
value:必需。填充的值;
-
start:可選。開始填充位置;
-
end:可選。停止填充位置 (默認爲 array.length)。
使用示例如下:
const arr = [0, 0, 0, 0, 0];
// 用5填充整個數組
arr.fill(5);
console.log(arr); // [5, 5, 5, 5, 5]
arr.fill(0); // 重置
// 用5填充索引大於等於3的元素
arr.fill(5, 3);
console.log(arr); // [0, 0, 0, 5, 5]
arr.fill(0); // 重置
// 用5填充索引大於等於1且小於等於3的元素
arr.fill(5, 3);
console.log(arr); // [0, 5, 5, 0, 0]
arr.fill(0); // 重置
// 用5填充索引大於等於-1的元素
arr.fill(5, -1);
console.log(arr); // [0, 0, 0, 0, 5]
arr.fill(0); // 重置
(2)copyWithin()
copyWithin() 方法會按照指定範圍來淺複製數組中的部分內容,然後將它插入到指定索引開始的位置,開始與結束索引的計算方法和 fill 方法一樣。該方法的語法如下:
array.copyWithin(target, start, end)
其參數如下:
-
target:必需。複製到指定目標索引位置;
-
start:可選。元素複製的起始位置;
-
end:可選。停止複製的索引位置 (默認爲 array.length)。如果爲負值,表示倒數。
使用示例如下:
const array = [1,2,3,4,5];
console.log(array.copyWithin(0,3)); // [4, 5, 3, 4, 5]
2. 轉化方法
數組的轉化方法主要有四個:toLocaleString()、toString()、valueOf()、join()。下面就分別來看一下這 4 個方法。
(1)toString()
toString() 方法返回的是由數組中每個值的等效字符串拼接而成的一個逗號分隔的字符串,也就是說,對數組的每個值都會調用 toString() 方法,以得到最終的字符串:
let colors = ["red", "blue", "green"];
console.log(colors.toString()) // red,blue,green
(2)valueOf()
valueOf() 方法返回的是數組本身,如下面代碼:
let colors = ["red", "blue", "green"];
console.log(colors.valueOf()) // ["red", "blue", "green"]
(3)toLocaleString()
toLocaleString() 方法可能會返回和 toString() 方法相同的結果,但也不一定。在調用 toLocaleString() 方法時會得到一個逗號分隔的數組值的字符串,它與 toString() 方法的區別是,爲了得到最終的字符串,會調用每個值的 toLocaleString() 方法,而不是 toString() 方法,看下面的例子:
let array= [{name:'zz'}, 123, "abc", new Date()];
let str = array.toLocaleString();
console.log(str); // [object Object],123,abc,2016/1/5 下午1:06:23
需要注意,如果數組中的某一項是 null 或者 undefined,則在調用上述三個方法後,返回的結果中會以空字符串來表示。
(4)join()
join() 方法用於把數組中的所有元素放入一個字符串。元素是通過指定的分隔符進行分隔的。其使用語法如下:
arrayObject.join(separator)
其中參數 separator 是可選的,用來指定要使用的分隔符。如果省略該參數,則使用逗號作爲分隔符。
該方法返回一個字符串。該字符串是通過把 arrayObject 的每個元素轉換爲字符串,然後把這些字符串連接起來,在兩個元素之間插入 separator 字符串而生成的。
使用示例如下:
let array = ["one", "two", "three","four", "five"];
console.log(array.join()); // one,two,three,four,five
console.log(array.join("-")); // one-two-three-four-five
3. 棧方法
ECMAScript 給數組添加了幾個方法來使它像棧一樣。衆所周知,棧是一種後進先出的結構,也就是最近添加的項先被刪除。數據項的插入(稱爲推入,push),和刪除(稱爲彈出,pop)只在棧頂發生。數組提高了 push() 和 pop() 來實現類似棧的行爲。下面就分別來看看這兩個方法。
(1)push()
push() 方法可以接收任意數量的參數,並將它們添加了數組末尾,並返回數組新的長度。該方法會改變原數組。 其語法形式如下:
arrayObject.push(newelement1,newelement2,....,newelementX)
使用示例如下:
let array = ["football", "basketball", "badminton"];
let i = array.push("golfball");
console.log(array); // ["football", "basketball", "badminton", "golfball"]
console.log(i); // 4
(2)pop()
pop() 方法用於刪除並返回數組的最後一個元素。它沒有參數。該方法會改變原數組。 其語法形式如下:
arrayObject.pop()
使用示例如下:
let array = ["cat", "dog", "cow", "chicken", "mouse"];
let item = array.pop();
console.log(array); // ["cat", "dog", "cow", "chicken"]
console.log(item); // mouse
4. 隊列方法
隊列是一種先進先出的數據結構,隊列在隊尾添加元素,在對頭刪除元素。上面我們已經說了在結果添加數據的方法 push(),下面就再來看看從數組開頭刪除和添加元素的方法:shift() 和 unshift()。實際上 unshift() 並不屬於操作隊列的方法,不過這裏也一起說了。
(1)shift()
shift() 方法會刪除數組的第一項,並返回它,然後數組長度減一,該方法會改變原數組。 語法形式如下:
arrayObject.shift()
使用示例如下:
let array = [1,2,3,4,5];
let item = array.shift();
console.log(array); // [2,3,4,5]
console.log(item); // 1
注意:如果數組是空的,那麼 shift() 方法將不進行任何操作,返回 undefined 值。
(2)unshift()
unshift() 方法可向數組的開頭添加一個或更多元素,並返回新的長度。該方法會改變原數組。 其語法形式如下:
arrayObject.unshift(newelement1,newelement2,....,newelementX)
使用示例如下:
let array = ["red", "green", "blue"];
let length = array.unshift("yellow");
console.log(array); // ["yellow", "red", "green", "blue"]
console.log(length); // 4
5. 排序方法
數組有兩個方法可以對數組進行重新排序:sort() 和 reverse()。下面就分別來看看這兩個方法。
(1)sort()
sort() 方法是我們常用給的數組排序方法,該方法會在原數組上進行排序,會改變原數組,其使用語法如下:
arrayObject.sort(sortby)
其中參數 sortby 是可選參數,用來規定排序順序,它是一個比較函數,用來判斷哪個值應該排在前面。默認情況下,sort() 方法會按照升序重新排列數組元素。爲此,sort() 方法會在每一個元素上調用 String 轉型函數,然後比較字符串來決定順序,即使數組的元素都是數值,也會將數組元素先轉化爲字符串在進行比較、排序。這就造成了排序不準確的情況,如下代碼:
let array = [5, 4, 3, 2, 1];
let array2 = array.sort();
console.log(array2) // [1, 2, 3, 4, 5]
let array = [0, 1, 5, 10, 15];
let array2 = array.sort();
console.log(array2) // [0, 1, 10, 15, 5]
可以看到,上面第二段代碼就出現了問題,雖然 5 是小於 10 的,但是字符串 10 在 5 的前面,所以 10 還是會排在 5 前面,因此可知,在很多情況下,不添加參數是不行的。
對於 sort() 方法的參數,它是一個比較函數,它接收兩個參數,如果第一個參數應該排在第二個參數前面,就返回 - 1;如果兩個參數相等,就返回 0;如果第一個參數應該排在第二個參數後面,就返回 1。一個比較函數的形式可以如下:
function compare(value1, value2) {
if(value1 < value2){
return -1
} else if(value1 > value2){
return 1
} else{
return 0
}
}
let array = [0, 1, 5, 10, 15];
let array2 = array.sort(compare);
console.log(array2) // [0, 1, 5, 10, 15]
使用箭頭函數來定義:
let array = [0, 1, 5, 10, 15];
let array2 = array.sort((a, b) => a - b); // 正序排序
console.log(array2) // [0, 1, 5, 10, 15]
let array3 = array.sort((a, b) => b - a); // 倒序排序
console.log(array3) // [15, 10, 5, 1, 0]
(2)reverse()
reverse() 方法用於顛倒數組中元素的順序。該方法會改變原來的數組,而不會創建新的數組。其使用語法如下:
arrayObject.reverse()
使用示例如下:
let array = [1,2,3,4,5];
let array2 = array.reverse();
console.log(array); // [5,4,3,2,1]
console.log(array2 === array); // true
6. 操作方法
對於數組,還有很多操作方法,下面我們就來看看常用的 concat()、slice()、splice() 方法。
(1)concat()
concat() 方法用於連接兩個或多個數組。該方法不會改變現有的數組,而僅僅會返回被連接數組的一個副本。其適用語法如下:
arrayObject.concat(arrayX,arrayX,......,arrayX)
其中參數 arrayX 是必需的。該參數可以是具體的值,也可以是數組對象。可以是任意多個。
使用示例如下:
let array = [1, 2, 3];
let array2 = array.concat(4, [5, 6], [7, 8, 9]);
console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array); // [1, 2, 3], 可見原數組並未被修改
該方法還可以用於數組扁平化,後面會介紹。
(2)slice()
slice() 方法可從已有的數組中返回選定的元素。返回一個新的數組,包含從 start 到 end (不包括該元素)的數組元素。方法並不會修改數組,而是返回一個子數組。其使用語法如下:
arrayObject.slice(start,end)
其參數如下:
-
start:必需。規定從何處開始選取。如果是負數,那麼它規定從數組尾部開始算起的位置。也就是說,-1 指最後一個元素,-2 指倒數第二個元素,以此類推;
-
end:可選。規定從何處結束選取。該參數是數組片斷結束處的數組下標。如果沒有指定該參數,那麼切分的數組包含從 start 到數組結束的所有元素。如果這個參數是負數,那麼它規定的是從數組尾部開始算起的元素。
使用示例如下:
let array = ["one", "two", "three", "four", "five"];
console.log(array.slice(0)); // ["one", "two", "three","four", "five"]
console.log(array.slice(2,3)); // ["three"]
(3)splice()
splice() 方法可能是數組中的最強大的方法之一了,使用它的形式有很多種,它會向 / 從數組中添加 / 刪除項目,然後返回被刪除的項目。該方法會改變原始數組。其使用語法如下:
arrayObject.splice(index, howmany, item1,.....,itemX)
其參數如下:
-
index:必需。整數,規定添加 / 刪除項目的位置,使用負數可從數組結尾處規定位置。
-
howmany:必需。要刪除的項目數量。如果設置爲 0,則不會刪除項目。
-
item1, ..., itemX:可選。向數組添加的新項目。
從上面參數可知,splice 主要有三種使用形式:
-
刪除: 需要給 splice() 傳遞兩個參數,即要刪除的第一個元素的位置和要刪除的元素的數量;
-
插入: 需要給 splice() 傳遞至少三個參數,即開始位置、0(要刪除的元素數量)、要插入的元素。
-
替換: splice() 方法可以在刪除元素的同事在指定位置插入新的元素。同樣需要傳入至少三個參數,即開始位置、要刪除的元素數量、要插入的元素。要插入的元素數量是任意的,不一定和刪除的元素數量相等。
使用示例如下:
let array = ["one", "two", "three","four", "five"];
console.log(array.splice(1, 2)); // 刪除:["two", "three"]
let array = ["one", "two", "three","four", "five"];
console.log(array.splice(2, 0, 996)); // 插入:[]
let array = ["one", "two", "three","four", "five"];
console.log(array.splice(2, 1, 996)); // 替換:["three"]
7. 歸併方法
ECMAScript 爲數組提供了兩個歸併方法:reduce() 和 reduceRight()。下面就分別來看看這兩個方法。
(1)reduce()
reduce() 方法對數組中的每個元素執行一個 reducer 函數 (升序執行),將其結果彙總爲單個返回值。其使用語法如下:
arr.reduce(callback,[initialValue])
reduce 爲數組中的每一個元素依次執行回調函數,不包括數組中被刪除或從未被賦值的元素,接受四個參數:初始值(或者上一次回調函數的返回值),當前元素值,當前索引,調用 reduce 的數組。(1) callback
(執行數組中每個值的函數,包含四個參數)
-
previousValue (上一次調用回調返回的值,或者是提供的初始值(initialValue))
-
currentValue (數組中當前被處理的元素)
-
index (當前元素在數組中的索引)
-
array (調用 reduce 的數組)
(2) initialValue
(作爲第一次調用 callback 的第一個參數。)
let arr = [1, 2, 3, 4]
let sum = arr.reduce((prev, cur, index, arr) => {
console.log(prev, cur, index);
return prev + cur;
})
console.log(arr, sum);
輸出結果如下:
1 2 1
3 3 2
6 4 3
[1, 2, 3, 4] 10
再來加一個初始值看看:
let arr = [1, 2, 3, 4]
let sum = arr.reduce((prev, cur, index, arr) => {
console.log(prev, cur, index);
return prev + cur;
}, 5)
console.log(arr, sum);
輸出結果如下:
5 1 0
6 2 1
8 3 2
11 4 3
[1, 2, 3, 4] 15
通過上面例子,可以得出結論:如果沒有提供 initialValue,reduce 會從索引 1 的地方開始執行 callback 方法,跳過第一個索引。如果提供 initialValue,從索引 0 開始。
注意,該方法如果添加初始值,就會改變原數組,將這個初始值放在數組的最後一位。
(2)reduceRight()
該方法和的上面的reduce()
用法幾乎一致,只是該方法是對數組進行倒序查找的。而reduce()
方法是正序執行的。
let arr = [1, 2, 3, 4]
let sum = arr.reduceRight((prev, cur, index, arr) => {
console.log(prev, cur, index);
return prev + cur;
}, 5)
console.log(arr, sum);
輸出結果如下:
5 4 3
9 3 2
12 2 1
14 1 0
[1, 2, 3, 4] 15
8. 搜索和位置方法
ECMAScript 提供了兩類搜索數組的方法:按照嚴格相等搜索和按照斷言函數搜索。
(1)嚴格相等
ECMAScript 通過了 3 個嚴格相等的搜索方法:indexOf()、lastIndexOf()、includes()。這些方法都接收兩個參數:要查找的元素和可選的其實搜索位置。lastIndexOf() 方法會從數組結尾元素開始向前搜索,其他兩個方法則會從數組開始元素向後進行搜索。indexOf() 和 lastIndexOf() 返回的是查找元素在數組中的索引值,如果沒有找到,則返回 - 1。includes() 方法會返回布爾值,表示是否找到至少一個與指定元素匹配的項。在比較第一個參數和數組的每一項時,會使用全等(===)比較,也就是說兩項必須嚴格相等。
使用示例如下:
let arr = [1, 2, 3, 4, 5];
console.log(arr.indexOf(2)) // 1
console.log(arr.lastIndexOf(3)) // 2
console.log(arr.includes(4)) // true
(2)斷言函數
ECMAScript 也允許按照定義的斷言函數搜索數組,每個索引都會調用這個函數,斷言函數的返回值決定了相應索引的元素是否被認爲匹配。使用斷言函數的方法有兩個,分別是 find() 和 findIndex() 方法。這兩個方法對於空數組,函數是不會執行的。並且沒有改變數組的原始值。他們的都有三個參數:元素、索引、元素所屬的數組對象,其中元素是數組中當前搜索的元素,索引是當前元素的索引,而數組是當前正在搜索的數組。
這兩個方法都從數組的開始進行搜索,find() 返回的是第一個匹配的元素,如果沒有符合條件的元素返回 undefined;findIndex() 返回的是第一個匹配的元素的索引,如果沒有符合條件的元素返回 -1。
使用示例如下:
let arr = [1, 2, 3, 4, 5]
arr.find(item => item > 2) // 結果:3
arr.findIndex(item => item > 2) // 結果:2
9. 迭代器方法
在 ES6 中,Array 的原型上暴露了 3 個用於檢索數組內容的方法:keys()、values()、entries()。keys() 方法返回數組索引的迭代器,values() 方法返回數組元素的迭代器,entries() 方法返回索引值對的迭代器。
使用示例如下(因爲這些方法返回的都是迭代器,所以可以將他們的內容通過 Array.from 直接轉化爲數組實例):
let array = ["one", "two", "three", "four", "five"];
console.log(Array.from(array.keys())) // [0, 1, 2, 3, 4]
console.log(Array.from(array.values())) // ["one", "two", "three", "four", "five"]
console.log(Array.from(array.entries())) // [[0, "one"], [1, "two"], [2, "three"], [3, "four"], [4, "five"]]
10. 迭代方法
ECMAScript 爲數組定義了 5 個迭代方法,分別是 every()、filter()、forEach()、map()、some()。這些方法都不會改變原數組。這五個方法都接收兩個參數:以每一項爲參數運行的函數和可選的作爲函數運行上下文的作用域對象(影響函數中的 this 值)。傳給每個方法的函數接收三個參數,分別是當前元素、當前元素的索引值、當前元素所屬的數對象。
(1)forEach()
forEach
方法用於調用數組的每個元素,並將元素傳遞給回調函數。該方法沒有返回值,使用示例如下:
let arr = [1,2,3,4,5]
arr.forEach((item, index, arr) => {
console.log(index+":"+item)
})
該方法還可以有第二個參數,用來綁定回調函數內部 this 變量(回調函數不能是箭頭函數,因爲箭頭函數沒有 this):
let arr = [1,2,3,4,5]
let arr1 = [9,8,7,6,5]
arr.forEach(function(item, index, arr){
console.log(this[index]) // 9 8 7 6 5
}, arr1)
(2)map()
map()
方法會返回一個新數組,數組中的元素爲原始數組元素調用函數處理後的值。該方法按照原始數組元素順序依次處理元素。該方法不會對空數組進行檢測,它會返回一個新數組,不會改變原始數組。使用示例如下:
let arr = [1, 2, 3];
arr.map(item => {
return item+1;
})
// 結果: [2, 3, 4]
第二個參數用來綁定參數函數內部的 this 變量:
var arr = ['a', 'b', 'c'];
[1, 2].map(function (e) {
return this[e];
}, arr)
// 結果: ['b', 'c']
該方法可以進行鏈式調用:
let arr = [1, 2, 3];
arr.map(item => item+1).map(item => item+1)
// 結果: [3, 4, 5]
forEach 和 map 區別如下:
-
forEach() 方法:會針對每一個元素執行提供的函數,對數據的操作會改變原數組,該方法沒有返回值;
-
map() 方法:不會改變原數組的值,返回一個新數組,新數組中的值爲原數組調用函數處理之後的值;
(3)filter()
filter()
方法用於過濾數組,滿足條件的元素會被返回。它的參數是一個回調函數,所有數組元素依次執行該函數,返回結果爲 true 的元素會被返回。該方法會返回一個新的數組,不會改變原數組。
let arr = [1, 2, 3, 4, 5]
arr.filter(item => item > 2)
// 結果:[3, 4, 5]
可以使用filter()
方法來移除數組中的 undefined、null、NAN 等值
let arr = [1, undefined, 2, null, 3, false, '', 4, 0]
arr.filter(Boolean)
// 結果:[1, 2, 3, 4]
(4)every()
該方法會對數組中的每一項進行遍歷,只有所有元素都符合條件時,才返回 true,否則就返回 false。
let arr = [1, 2, 3, 4, 5]
arr.every(item => item > 0)
// 結果:true
(5)some()
該方法會對數組中的每一項進行遍歷,只要有一個元素符合條件,就返回 true,否則就返回 false。
let arr = [1, 2, 3, 4, 5]
arr.some(item => item > 4)
// 結果:true
11. 其他方法
除了上述方法,遍歷數組的方法還有 for...in 和 for...of。下面就來簡單看一下。
(1)for…in
for…in
主要用於對數組或者對象的屬性進行循環操作。循環中的代碼每執行一次,就會對對象的屬性進行一次操作。其使用語法如下:
for (var item in object) {
執行的代碼塊
}
其中兩個參數:
-
item:必須。指定的變量可以是數組元素,也可以是對象的屬性。
-
object:必須。指定迭代的的對象。
使用示例如下:
const arr = [1, 2, 3];
for (var i in arr) {
console.log('鍵名:', i);
console.log('鍵值:', arr[i]);
}
輸出結果如下:
鍵名: 0
鍵值: 1
鍵名: 1
鍵值: 2
鍵名: 2
鍵值: 3
需要注意,該方法不僅會遍歷當前的對象所有的可枚舉屬性,還會遍歷其原型鏈上的屬性。 除此之外,該方法遍歷數組時候,遍歷出來的是數組的索引值,遍歷對象的時候,遍歷出來的是鍵值名。
(2)for...of
for...of
語句創建一個循環來迭代可迭代的對象。在 ES6 中引入的 for...of
循環,以替代 for...in
和 forEach()
,並支持新的迭代協議。for...of
允許遍歷 Arrays(數組), Strings(字符串), Maps(映射), Sets(集合)等可迭代的數據結構等。
語法:
for (var item of iterable) {
執行的代碼塊
}
其中兩個參數:
-
item:每個迭代的屬性值被分配給該變量。
-
iterable:一個具有可枚舉屬性並且可以迭代的對象。
該方法允許獲取對象的鍵值:
var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}
該方法只會遍歷當前對象的屬性,不會遍歷其原型鏈上的屬性。
注意:
-
for...of 適用遍歷 數組 / 類數組 / 字符串 / map/set 等擁有迭代器對象的集合;
-
它可以正確響應 break、continue 和 return 語句;
-
for...of 循環不支持遍歷普通對象,因爲沒有迭代器對象。如果想要遍歷一個對象的屬性,可以用
for-in
循環。
總結,for…of 和 for…in 的區別如下:
-
for…of 遍歷獲取的是對象的鍵值,for…in 獲取的是對象的鍵名;
-
for… in 會遍歷對象的整個原型鏈,性能非常差不推薦使用,而 for … of 只遍歷當前對象不會遍歷原型鏈;
-
對於數組的遍歷,for…in 會返回數組中所有可枚舉的屬性 (包括原型鏈上可枚舉的屬性),for…of 只返回數組的下標對應的屬性值;
(3)flat()
在 ES2019 中,flat() 方法用於創建並返回一個新數組,這個新數組包含與它調用 flat() 的數組相同的元素,只不過其中任何本身也是數組的元素會被打平填充到返回的數組中:
[1, [2, 3]].flat() // [1, 2, 3]
[1, [2, [3, 4]]].flat() // [1, 2, [3, 4]]
在不傳參數時,flat() 默認只會打平一級嵌套,如果想要打平更多的層級,就需要傳給 flat() 一個數值參數,這個參數表示要打平的層級數:
[1, [2, [3, 4]]].flat(2) // [1, 2, 3, 4]
三、類數組對象
JavaScript 中一直存在一種類數組的對象,它們不能直接調用數組的方法,但是又和數組比較類似,在某些特定的編程場景中會出現,下面就來看一下什麼是類數組。
在 JavaScript 中,主要有以下情況中的對象是類數組:
-
函數里面的參數對象 arguments;
-
用 getElementsByTagName/ClassName/Name 獲得的 HTMLCollection;
-
用 querySelector 獲得的 NodeList。
1. 類數組概述
(1)arguments
在日常開發中經常會遇到各種類數組對象,最常見的就是在函數中使用的 arguments,它的對象只定義在函數體中,包括了函數的參數和其他屬性。先來看下 arguments 的使用方法:
function foo(name, age, sex) {
console.log(arguments);
console.log(typeof arguments);
console.log(Object.prototype.toString.call(arguments));
}
foo('jack', '18', 'male');
打印結果如下:
length 屬性就是函數參數的長度。另外 arguments 還有一個 callee 屬性,下面看看這個 callee 是幹什麼的:
function foo(name, age, sex) {
console.log(arguments.callee);
}
foo('jack', '18', 'male');
打印結果如下:
ƒ foo(name, age, sex) {
console.log(arguments.callee);
}
可以看出,輸出的就是函數自身,如果在函數內部直接執行調用 callee,那它就會不停地執行當前函數,直到執行到內存溢出。
(2)HTMLCollection
HTMLCollection 簡單來說是 HTML DOM 對象的一個接口,這個接口包含了獲取到的 DOM 元素集合,返回的類型是類數組對象,如果用 typeof 來判斷的話,它返回的是 object。它是及時更新的,當文檔中的 DOM 變化時,它也會隨之變化。
下面來 HTMLCollection 最後返回的是什麼,在一個有 form 表單的頁面中,在控制檯中執行下述代碼:
var elem1, elem2;
// document.forms 是一個 HTMLCollection
elem1 = document.forms[0];
elem2 = document.forms.item(0);
console.log(elem1);
console.log(elem2);
console.log(typeof elem1);
console.log(Object.prototype.toString.call(elem1));
打印結果如下:
可以看到,這裏打印出來了頁面第一個 form 表單元素,同時也打印出來了判斷類型的結果,說明打印的判斷的類型和 arguments 返回的也比較類似,typeof 返回的都是 object,和上面的類似。
注意:HTML DOM 中的 HTMLCollection 是即時更新的,當其所包含的文檔結構發生改變時,它會自動更新。
(3)NodeList
NodeList 對象是節點的集合,通常是由 querySlector 返回的。NodeList 不是一個數組,也是一種類數組。雖然 NodeList 不是一個數組,但是可以使用 for...of 來迭代。在一些情況下,NodeList 是一個實時集合,也就是說,如果文檔中的節點樹發生變化,NodeList 也會隨之變化。
var list = document.querySelectorAll('input[type=checkbox]');
for (var checkbox of list) {
checkbox.checked = true;
}
console.log(list);
console.log(typeof list);
console.log(Object.prototype.toString.call(list));
打印結果如下:
2. 類數組應用場景
(1)遍歷參數操作
在函數內部可以直接獲取 arguments 這個類數組的值,那麼也可以對於參數進行一些操作,比如下面這段代碼可以將函數的參數默認進行求和操作:
function add() {
var sum =0,
len = arguments.length;
for(var i = 0; i < len; i++){
sum += arguments[i];
}
return sum;
}
add() // 0
add(1) // 1
add(1,2) // 3
add(1,2,3,4); // 10
結合上面這段代碼,在函數內部可以將參數直接進行累加操作,以達到預期的效果,參數多少也可以不受限制,根據長度直接計算,返回出最後函數的參數的累加結果,其他操作也類似。
(2)定義連接字符串函數
可以通過 arguments 這個例子定義一個函數來連接字符串。這個函數唯一正式聲明瞭的參數是一個字符串,該參數指定一個字符作爲銜接點來連接字符串。該函數定義如下:
function myConcat(separa) {
var args = Array.prototype.slice.call(arguments, 1);
return args.join(separa);
}
myConcat(", ", "red", "orange", "blue");
// "red, orange, blue"
myConcat("; ", "elephant", "lion", "snake");
// "elephant; lion; snake"
myConcat(". ", "one", "two", "three", "four", "five");
// "one. two. three. four. five"
這段代碼說明可以傳遞任意數量的參數到該函數,並使用每個參數作爲列表中的項創建列表進行拼接。從這個例子中也可以看出,可以在日常編碼中採用這樣的代碼抽象方式,把需要解決的這一類問題,都抽象成通用的方法,來提升代碼的可複用性。
(3)傳遞參數
可以藉助 apply 或 call 與 arguments 相結合,將參數從一個函數傳遞到另一個函數:
1. // 使用 apply 將 foo 的參數傳遞給 bar
2. function foo() {
3. bar.apply(this, arguments);
4. }
5. function bar(a, b, c) {
6. console.log(a, b, c);
7. }
8. foo(1, 2, 3) //1 2 3
上述代碼中,通過在 foo 函數內部調用 apply 方法,用 foo 函數的參數傳遞給 bar 函數,這樣就實現了借用參數的妙用。
3. 類數組轉爲數組
(1)借用數組方法
類數組因爲不是真正的數組,所以沒有數組類型上自帶的那些方法,所以就需要利用下面這幾個方法去借用數組的方法。比如借用數組的 push 方法,代碼如下:
var arrayLike = {
0: 'java',
1: 'script',
length: 2
}
Array.prototype.push.call(arrayLike, 'jack', 'lily');
console.log(typeof arrayLike); // 'object'
console.log(arrayLike);
// {0: "java", 1: "script", 2: "jack", 3: "lily", length: 4}
可以看到,arrayLike 其實是一個對象,模擬數組的一個類數組,從數據類型上說它是一個對象,新增了一個 length 的屬性。還可以看出,用 typeof 來判斷輸出的是 object,它自身是不會有數組的 push 方法的,這裏用 call 的方法來借用 Array 原型鏈上的 push 方法,可以實現一個類數組的 push 方法,給 arrayLike 添加新的元素。
從打印結果可以看出,數組的 push 方法滿足了我們想要實現添加元素的訴求。再來看下 arguments 如何轉換成數組:
function sum(a, b) {
let args = Array.prototype.slice.call(arguments);
// let args = [].slice.call(arguments); // 這樣寫也是一樣效果
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
function sum(a, b) {
let args = Array.prototype.concat.apply([], arguments);
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
可以看到,借用 Array 原型鏈上的各種方法,來實現 sum 函數的參數相加的效果。一開始都是將 arguments 通過借用數組的方法轉換爲真正的數組,最後都又通過數組的 reduce 方法實現了參數轉化的真數組 args 的相加,最後返回預期的結果。
(2)借用 ES6 方法
還可以採用 ES6 新增的 Array.from 方法以及展開運算符的方法來將類數組轉化爲數組。那麼還是圍繞上面這個 sum 函數來進行改變,看下用 Array.from 和展開運算符是怎麼實現轉換數組的:
function sum(a, b) {
let args = Array.from(arguments);
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
function sum(a, b) {
let args = [...arguments];
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
function sum(...args) {
console.log(args.reduce((sum, cur) => sum + cur));
}
sum(1, 2); // 3
可以看到,Array.from 和 ES6 的展開運算符,都可以把 arguments 這個類數組轉換成數組 args,從而實現調用 reduce 方法對參數進行累加操作。其中第二種和第三種都是用 ES6 的展開運算符,雖然寫法不一樣,但是基本都可以滿足多個參數實現累加的效果。
四、數組常見操作
1. 數組扁平化
下面再來看看數組的扁平化。所謂扁平化,其實就是將一個嵌套多層的數組 array(嵌套可以是任何層數)轉換爲只有一層的數組。舉個簡單的例子,假設有個名爲 flatten 的函數可以做到數組扁平化,那麼輸出效果如下:
let arr = [1, [2, [3, 4,5]]];
console.log(flatten(arr)); // [1, 2, 3, 4,5]
簡單來說就是把多維的數組 “拍平”,輸出最後的一維數組。下面來看看實現 flatten 函數的方式。
(1)遞歸實現
普通的遞歸思路很容易理解,就是通過循環遞歸的方式,一項一項地去遍歷,如果某一項還是一個數組,那麼就繼續往下遍歷,利用遞歸來實現數組的每一項的連接:
let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
let result = [];
for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
flatten(arr); // [1, 2, 3, 4,5]
可以看到,最後返回的結果是扁平化的結果,這段代碼核心就是循環遍歷過程中的遞歸操作,就是在遍歷過程中發現數組元素還是數組的時候進行遞歸操作,把數組的結果通過數組的 concat 方法拼接到最後要返回的 result 數組上,那麼最後輸出的結果就是扁平化後的數組。
(2)reduce 函數迭代
從上面的遞歸函數可以看出,其實就是對數組的每一項進行處理,那麼其實也可以用 reduce 來實現數組的拼接,從而簡化上面方法的代碼,改造後的代碼如下:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
console.log(flatten(arr));// [1, 2, 3, 4,5]
這段代碼在控制檯執行之後,也可以得到想要的結果。上面我們說了 reduce 的第一個參數用來返回最後累加的結果,思路和第一種遞歸方法是一樣的,但是通過使用 reduce 之後代碼變得更簡潔了,也同樣解決了扁平化的問題。
(3)擴展運算符實現
這個方法的實現,採用了擴展運算符和 some 的方法,兩者共同使用,達到數組扁平化的目的:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
從執行的結果中可以發現,先用數組的 some 方法把數組中仍然是組數的項過濾出來,然後執行 concat 操作,利用 ES6 的展開運算符,將其拼接到原數組中,最後返回原數組,達到了預期的效果。
(4)split 和 toString
可以通過 split 和 toString 兩個方法來共同實現數組扁平化,由於數組會默認帶一個 toString 的方法,所以可以把數組直接轉換成逗號分隔的字符串,然後再用 split 方法把字符串重新轉換爲數組,如下面的代碼所示:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.toString().split(',');
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
通過這兩個方法可以將多維數組直接轉換成逗號連接的字符串,然後再重新分隔成數組。
(5)ES6 中的 flat
我們還可以直接調用 ES6 中的 flat 方法來實現數組扁平化。flat 方法的語法:arr.flat([depth])
其中 depth 是 flat 的參數,depth 是可以傳遞數組的展開深度(默認不填、數值是 1),即展開一層數組。如果層數不確定,參數可以傳進 Infinity,代表不論多少層都要展開:
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.flat(Infinity);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
可以看出,一個嵌套了兩層的數組,通過將 flat 方法的參數設置爲 Infinity,達到了我們預期的效果。其實同樣也可以設置成 2,也能實現這樣的效果。在編程過程中,如果數組的嵌套層數不確定,最好直接使用 Infinity,可以達到扁平化的效果。
(6)正則和 JSON 方法
在第 4 種方法中已經使用 toString 方法,其中仍然採用了將 JSON.stringify 的方法先轉換爲字符串,然後通過正則表達式過濾掉字符串中的數組的方括號,最後再利用 JSON.parse 把它轉換成數組:
let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
let str = JSON.stringify(arr);
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
return JSON.parse(str);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
可以看到,其中先把傳入的數組轉換成字符串,然後通過正則表達式的方式把括號過濾掉,匹配規則是:全局匹配(g)左括號或者右括號,將它們替換成空格,最後返回處理後的結果。之後拿着正則處理好的結果重新在外層包裹括號,最後通過 JSON.parse 轉換成數組返回。
2. 數組去重
去除無序數組中的重複元素並且返回新的無重複數組。
(1)Set 實現
ES6 方法(使用數據結構集合):
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
(2)map 實現
ES5 方法:使用 map 存儲不重複的數字
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
function uniqueArray(array) {
let map = {};
let res = [];
for(var i = 0; i < array.length; i++) {
if(!map.hasOwnProperty([array[i]])) {
map[array[i]] = 1;
res.push(array[i]);
}
}
return res;
}
uniqueArray(array); // [1, 2, 3, 5, 9, 8]
3. 數組求和
(1)reduce 實現
let arr = [1, 2, 3, 4, 5, 6]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum); // 21
(2)遞歸實現
let arr = [1, 2, 3, 4, 5, 6]
function add(arr) {
if (arr.length == 1) return arr[0]
return arr[0] + add(arr.slice(1))
}
console.log(add(arr)) // 21
4. 數組亂序
(1)正向遍歷
主要的實現思路就是:
-
取出數組的第一個元素,隨機產生一個索引值,將該第一個元素和這個索引對應的元素進行交換;
-
第二次取出數據數組第二個元素,隨機產生一個除了索引爲 1 的之外的索引值,並將第二個元素與該索引值對應的元素進行交換;
-
按照上面的規律執行,直到遍歷完成。
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (var i = 0; i < arr.length; i++) {
const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)
(2)倒序遍歷
倒序遍歷和上面實現思路類似,代碼如下:
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let length = arr.length,
randomIndex,
temp;
while (length) {
randomIndex = Math.floor(Math.random() * length--);
temp = arr[length];
arr[length] = arr[randomIndex];
arr[randomIndex] = temp;
}
console.log(arr)
這篇文章到這裏就結束了
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/jg6D457yshioumy7Y4sk6Q