JS經常使用數組方法總結筆記

數組(Array)和對象(Object)幾乎是不少程序語言中最經常使用的類型。在ECMAScript中,數組的長度能夠動態變化,數組中的數據能夠是不一樣類型,相比其餘語言更加靈活。另外,ECMAScript數組原生支持不少實用的方法,給數據的保存和處理帶來很大的方便。javascript

因爲數組是引用類型,須要注意方法的可變性,簡單理解就是「是否會改變原數組」。這對於函數式編程尤爲重要,由於可變方法可能會產生咱們調用它的目的以外的反作用,致使一些不可預知的結果,更容易形成bug且給bug的定位增長了難度。java

這裏把數組的經常使用方法總結一下。因爲是我的總結,若是有差錯的地方還望你們及時指出。編程

tips:數組

  1. 示例代碼能夠直接打開瀏覽器console進行運行和實驗;
  2. 爲閱讀方便,方法介紹時會用如sort(?compareFn)表示函數名和參數列表,參數前有?表明是可選參數。
  3. 查看瀏覽器中實現的全部數組方法,能夠直接在console中執行console.dir(Array)console.log("%O", Array), 能看到Array上的靜態方法和其prototype上的方法列表;

靜態方法

相似於ES6中在class類中定義的static的方法,不會被實例繼承,只能經過類自己來訪問:瀏覽器

Array.isArray

這個方法用於檢驗傳入值是不是數組,與instanceof相比,具備「跨iframe」的優勢;由於一個瀏覽器中的多個window是不共享全局對象的,因此經過全局變量直接訪問的Array也不必定指向同一個Array構造函數;而當執行value instanceof Array時,這裏的Array不必定是建立value時所在全局對象下的Array, 可能會返回錯誤的信息。app

// 插入一個新iframe
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var a = new window.frames[0].Array(2); // 在iframe中建立一個長度爲2的數組

// 兩個window的Array是不一樣的對象(構造函數)
a instanceof window.frames[0].Array // true
a instanceof window.Array // false

window.frames[0].Array.isArray(a); // true
window.Array.isArray(a) // true

// 由於不一樣window下的全局屬性是不一樣的引用
window.frames[0].Array === window.Array // false
複製代碼

Array.from, Array.of

這兩個方法均可以用來建立新的數組實例函數式編程

  • Array.from(arrayLike, ?mapfn, ?thisArg)接收3個參數,第一個是類數組或可迭代對象,後兩個是可選的回調函數mapFnmap的回調函數)和thisArg(設置this),至關於把可迭代對象轉爲數組後對其執行map(mapFn, thisArg); 可迭代對象中若是有「空元素」(empty), 會處理爲undefined返回;
Array.from(new Array(3)); // [undefined, undefined, undefined]
Array.from("abcdefg", (s, i) => s + i); // ["a0", "b1", "c2", "d3", "e4", "f5", "g6"]
複製代碼
  • Array.of(...arg)接收1個或多個參數,會把參數列表按順序做爲新數組的元素,並返回該新數組;
Array.of("a", 1, "b", 2); // ["a", 1, "b", 2]
複製代碼

不可變(immutable)方法

不可變方法不會影響原數組,對返回的數組自己進行的操做也和原數組無關;但須要區別的一種狀況是,因爲元素複製時都是淺複製,新數組中引用類型值的元素與原數組元素引用的是同一個對象,修改會相互影響,同時影響其餘引用該對象的全部變量。函數

map, filter, forEach

這三個方法屬於ECMAScript定義的迭代方法,能夠對數組中每一個元素執行必定操做後返回必定的結果;它們均可以接收兩個參數(callbackfn, ?thisArg),第一個是要在每一個元素上執行的回調函數,第二個參數是可選的,即運行該函數的做用域對象(指定this值);須要注意的是若是使用箭頭函數做爲回調函數,this值是建立時綁定的,不會被第二個參數影響。post

// 第二個參數對this值的影響
[1, 2, 3].map(function (n) {
    return this.name;
}, {name: "Anne"})
// ["Anne", "Anne", "Anne"]

// 箭頭函數回調, 建立時綁定了當前所在執行環境的this--> Window做爲固定的this的值
[1, 2, 3].map(n => this, {name: "Anne"}) // [Window, Window, Window]
複製代碼

傳入的回調函數會接收到三個參數(item, index, array),即當前元素、當前索引和源數組;通常使用最多的是前兩個。ui

  • map對每一個元素執行該回調後,將回調函數返回值組成新的數組返回,用於對元素成員轉換或取值;
[1,2,3,4,5].map((n, i) => n + i); // [1, 3, 5, 7, 9]
複製代碼
  • filter是將執行回調函數後返回的是truthy值的元素保留組成新數組返回,一般用於數組的過濾;
[1, 2, 3, 4, 5].filter((n, i) => n % 2 === 0); // [2, 4]
複製代碼
  • forEach則只是對每一個元素執行回調函數的操做,沒有返回值。
let a = [1, 2, 3, 4, 5];
let b = [];
a.forEach((n, i) => b[i] = n + i) // (返回)undefined
a // (未改變)[1, 2, 3, 4, 5]
b // [1, 3, 5, 7, 9]
複製代碼

some, every

這兩個方法也屬於迭代方法,一樣接收一個回調函數參數和一個可選的做用域對象參數,回調函數會接收(item, index, array)做爲參數並須要返回布爾類型值,做爲每一個元素是否符合條件的判斷依據;

與上面方法不一樣的是它們返回的是布爾值;從字面能夠看出,some表明的是「只要有符合條件的元素就返回true」而every則是「全部元素都符合條件才返回true」.因此:

[1, 2, 3, 4, 5].some((n) => n % 2 === 0) // true
[1, 2, 3, 4, 5].every((n) => n % 2 === 0) // false
複製代碼

它們不必定會遍歷全部的元素,當some遇到符合條件的元素或every遇到不符合條件的元素它們就會中止遍歷直接返回結果,由於後面的遍歷再也不必要;

// 輸出true以前 執行了3次
[1, 2, 3, 4, 5].some(n => {
    console.count("some");
    return n === 3;
});

// 輸出false以前 執行了1次
[1, 2, 3, 4, 5].every(n => {
    console.count("every");
    return n === 3;
});

複製代碼

reduce, reduceRight

我以爲reduce函數值得是數組方法中被運用最多的方法之一(還有mapfilter)。初學JavaScript時我對它認知較淺,只有在遇到相似書中所舉的「數組求和」問題時纔會想到它。但如今認識到它其實比我想象的能作更多事(本質仍是同樣的),我寫在另外一篇總結裏(擴展一下使用reduce的思路)。

reducereduceRight是ES5中增長的數組歸併方法。reduce會從第一項到最後一項遍歷數組全部元素,構建一個最終返回的值(取決於回調函數);reduceRightreduce同樣,只是遍歷方向相反,從最後一項開始到第一項進行歸併操做。

它們接收兩個參數(callbackfn, ?initialValue),第一個是在每一項上調用的回調函數,第二個是可選參數,用於設置初始值;例如書中的例子:

[1, 2, 3, 4, 5].reduce((prev, cur) => prev + cur); // (數組元素的和)15
複製代碼

在每一項上調用的回調函數能夠接收到四個參數,即(accumulator, currentValue, currentIndex, sourceArray);

  • accumulator: 可理解爲累積器,每次執行回調函數後的返回值,傳入下一項中做爲此參數;在reduce初始值(第二個參數)沒有設定時,執行時會默認把數組中第一個元素做爲這個參數直接在第二個元素上執行;若是傳入了初始值,則先在第一個元素上執行,初始值做爲回調的該參數。
  • currentValue: 當前元素
  • currentIndex: 當前索引
  • sourceArray: 源數組

能夠驗證,沒有設定初始值時,執行回調函數的次數比元素個數少一個;而設定初始值時執行次數與元素個數相同。由於有初始值時遍歷從第一個元素開始。

[1, 2, 3, 4, 5].reduce((sum, cur) => {
    console.count("reduce-no-initail");
    return sum + cur;
});
// 輸出結果 15 前,"reduce-no-initail"打印了4次

[1, 2, 3, 4, 5].reduce((sum, cur) => {
    console.count("reduce-initail");
    return sum + cur;
}, 0);
// 輸出結果 15 前,"reduce-initail"打印了5次
複製代碼

concat, slice

這兩個方法不傳入參數時都會簡單淺複製已知數組並返回這個副本,全部也經常使用於複製數組或類數組/可迭代對象(經過Array.prototype.concat.call(someObj)[].concat.call(someObj)的方式).

  • concat(...items)用於對數組副本的拼接和合並,接收0或多個參數,不傳入參數時會返回將原數組淺複製後的新數組;傳入1個或多個參數時,會在淺複製一份原數組的基礎上,將每一個參數(若是參數是數組則按序取出其中的元素,不然直接取該參數)做爲元素按順序拼接在其後;至關於直接將參數合併後執行了一次flat()再與原函數合併。
[[0], 1].concat(2, [3, 4], [[5, 6], 7]); // [[0], 1, 2, 3, 4, [5, 6], 7]
複製代碼
  • slice(?start, ?end)用於返回數組的某一部分的副本,接收2個可選參數,表明起始索引和結束索引(左閉右開), 不傳參數的狀況與concat類似,返回將原數組淺複製的新數組;傳入一個參數則默認從該參數位置到數組末尾; 傳入的負值參數會取絕對值後從後向前數,例如-1會被解釋爲倒數第一個元素的位置(其餘數組方法對錶明索引的負數參數的處理都與此相同)。
[1,2,3,4].slice(2) // [3, 4]
[1,2,3,4].slice(2, -1) // [3]
複製代碼

find, findIndex, indexOf, lastIndexOf, includes

這幾個方法的類似之處都是執行對數組的查找操做;不一樣之處在於:

  • find(predicate, ?thisArg)findIndex(predicate, ?thisArg)接收一個回調函數做爲查找標準,該函數接收每一個迭代元素的(item, index, array)參數,一旦執行後返回值爲truthy則視爲找到該元素,find將會返回該元素(或其引用)而findIndex返回該元素的索引,並中止查找;它們還能夠接收第二個可選參數用於綁定this所指的對象;
[
    {name: "a", val: 1},
    {name: "b", val: 2},
    {name: "c", val: 1}
].find(item => item.val === 1)
// {name: "a", val: 1}

function getVal2(o) {
    return o.val === this.val;
}

[
    {name: "a", val: 1},
    {name: "b", val: 2},
].find(getVal2, {name: "d", val: 2})
// {name: "b", val: 2}

複製代碼
  • indexOf(searchElement, ?fromIndex)lastIndexOf(searchElement, ?fromIndex)接收的第一個參數是一個要查找的元素,並在迭代數組元素時使用===來判斷是不是查找的元素,若是是則返回該元素的索引,若是最後都沒有找到則返回-1; lastIndexOfindexOf同樣,只是是從數組末尾開始向前查找;它們能夠接收第二個參數,用於設定從哪一個位置開始查找;
[NaN, +0, -0].indexOf(NaN) // -1
[NaN, -0].indexOf(0) // 1
複製代碼
  • includes(searchElement, ?fromIndex)的參數與indexOf類似,也是一個要查找的值,和一個可選的「起始位置」;但不一樣的是這個方法在對比元素時使用sameValueZero的判斷方式,即NaNNaN視爲相等,其餘與===判斷相同。
[NaN, -0].includes(NaN) // true
[NaN, -0].includes(0) // true

複製代碼

關於===和'sameValueZero'相等性判斷

  • ===不進行類型轉換,直接對比值,若是是引用類型值則對比其是否指向同一個對象;NaN不等於自身,0+0-0互相相等;

  • 'sameValueZero'判斷時除了對NaN處理爲其與自身相等,其餘均與===同樣;

  • 另外ES6中新增的Object.is(),則在sameValueZero的基礎上,增長了對0的符號的限制,用它來判斷時0等於+0, 但它們不等於-0

flat, flatMap

flat(?depth)用於鋪平數組,能夠接收一個參數設定要鋪平的深度(層數), 默認爲1,若是傳入的深度值比數組自己的深度大,則與傳入數組的最大深度效果相同, 數組被徹底鋪平成爲一維數組。

let a = [1, [2, [3, [4, 5], 6], 7], [8], 9];
a.flat() // [1, 2, [3, [4, 5], 6], 7, 8, 9]
a.flat(2) // [1, 2, 3, [4, 5], 6, 7, 8, 9]
a.flat(10) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
a.flat(Infinity) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
複製代碼

flatMap(callback, ?thisArg)則是map方法與flat的方法的結合體,接收一個回調函數和一個可選的this參數,做爲第一步map的參數對每一個元素執行並返回新的值,而後對構建的新數組進行展平1層。它與咱們本身調用map方法後再調用flat方法效果相同,但效率可能更高一點點。

比較常見的狀況如從對象數組中取出某些值爲數組的屬性值,而後但願變成一個一維數組方便執行其餘操做,就能夠用這個方法;

let b = [
    {memberIds: [1,2,3]},
    {memberIds: [4,5,6]},
    {memberIds: [7,8,9,10]}
];
// 獲取b中全部memberId組成一個一維數組
b.flatMap(obj => obj.memberIds) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 和下面的方法結果同樣
b.map(obj => obj.memberIds).flat() // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
複製代碼

可變(mutable)方法

這裏可變方法是指會改變數組自己的方法。並非說可變方法不能用,它們有時候可能很是有用。若是開發人員本身清楚使用它們的目的和結果並只在須要的時候使用,有利於提升代碼的可維護性和健壯性。

fill, copyWithin, splice

這三個方法的類似性不是很高,但使用目的有必定的類似,即將數組中某些元素改變爲咱們但願的值,甚至插入/刪除一些值。

  • fill(value, ?start, ?end)是對已有的數組填充,它的做用範圍僅限於當前數組長度以內,不能改變數組長度。它接收三個參數,第一個是須要填充的值,第二個和第三個是可選的位置參數,設定填充的起始索引(含)和結束索引(不含),若是不傳則分別默認爲0和數組的length。對負數位置參數的處理與上面提到的相同;若是超出了數組自己,則轉爲與它們最近的有效值(0 或 array.length)。執行結束返回改變後的數組。例如:
[1,2,3,4].fill(5) // [5,5,5,5]
[1,2,3,4].fill(5, 2, 4) // [1,2,5,5]
[1,2,3,4].fill(5, -2, 4) // [1,2,5,5]
[1,2,3,4].fill(5, -3, 4) // [1,5,5,5]
[1,2,3,4].fill(5, -5, 4) // [5,5,5,5]
複製代碼
  • copyWithin(target, start, ?end)在數組內部複製一部分值到另外一部分,也不能改變數組長度。它接收第一個參數是要放置複製元素的目標位置索引,第二個參數是要複製的部分的起始索引(含),結束索引(不含)即是可選的第三個參數,若是沒有傳入則默認爲函數長度,即從起始一直複製到末尾。複製的部分將從目標位置開始填充,覆蓋對應位置原有的元素。執行結束後返回改變後的數組。
[1,2,3,4].copyWithin(2, 0, 2) // [1,2,1,2]
[1,2,3,4].copyWithin(2, 0, 4) // [1,2,1,2]
[1,2,3,4].copyWithin(2, 4, 4) // [1,2,3,4] (沒有複製到元素,也不會對原來的數組有影響)
複製代碼
  • splice(start, ?deleteCount, ...items)幾乎能夠算這些方法中最強大的方法了。它能夠對數組任意位置執行插入、刪除、替換操做,也能夠改變數組長度。就像對數組作手術,而具體會作什麼樣的手術(執行什麼操做)則徹底由參數決定。它能夠接收1個或多個參數,第一個設置起始位置,第二個爲可選的「刪除數量」,設置從起始位置(含)開始刪除多少個元素,若是沒有傳入則默認將起始位置及其以後的元素所有刪除。若是設置爲0則不刪除元素,並將其後的參數列表按順序都從起始位置開始插入數組中。最後返回的是被刪除的元素組成的數組。
let a = [1,2,3,4,5,6];
a.splice(4) // [5, 6] 刪除了5,6

a // [1,2,3,4] 數組a自己長度改變

a.splice(2, 0, 7, 8, 9, 0); // [] 沒有刪除元素
a // [1,2,7,8,9,0,3,4] // 7,8,9,0被做爲插入元素從索引2開始插入,數組原來的元素被放到插入元素列表的後面
複製代碼

push, shift, pop, unshift

這幾個方法都用於對數組的頭部或尾部進行插入和刪除。push(末尾推入)和pop(末尾刪除)操做在數組末尾,像使用棧同樣使用數組,遵循「後進先出」的規則;shift(頭部刪除)和unshift(頭部推入)做用於數組頭部,結合使用shiftpushunshiftpop能夠從正向和反向模擬隊列行爲,像使用隊列同樣使用數組,遵循「先進先出」的規則。

  • push(...items)方法能夠接收任意多個參數,把它們按序依次添加到數組末尾,返回改變後的數組的長度;
  • pop()方法不接收參數,每次執行都會從數組中刪除掉最後一項,並返回這個元素;
  • unshift(...items)方法與push的方向相反,把接收到的任意多個參數放在數組頭部,返回改變後的數組長度;
  • shift()方法也不接收參數,每次執行都會從數組頭部刪除掉第一項並返回這個元素;

這幾個方法也能夠應用在類數組對象上,或者有length屬性和數值字符串屬性的對象,它們會根據length屬性肯定數組的末尾位置並訪問對應位置,而與對象實際存在的元素個數或其餘屬性無關。

var a = [];
a.push(1,2,3) // 3 (添加元素後的數組長度)
a // [1,2,3]
a.pop() // 3 (刪除的尾部元素)
a // [1,2]
a.unshift(5,6,7,8); // 6 (添加元素後的數組長度)
a // [5,6,7,8,1,2]
a.shift() // 5 (刪除的頭部元素)
a // [6,7,8,1,2]
複製代碼

reverse, sort

這兩個是數組的重排序方法;

  • reverse()直接按元素的位置進行反序操做,並返回改變後的數組;
[4,14,3,23].reverse() // [23,3,14,4]
[4,14,3,23].sort() // [14,23,3,4]
複製代碼
  • sort(?compareFn)方法則是默認根據對比元素的字符串表示的前後順序升序排列--即便每一個元素都是數值,也會先把它們轉換爲字符串,而後按照字符串的對比規則(對比它們的UTF-16字符編碼值)肯定排序關係。若是數組中有undefined,則它們不參與排序並被放置在最後。

通常狀況下咱們更多須要的是對一組數值或擁有數值類型屬性的對象進行排序,直接調用sort是沒法知足的,須要本身傳入一個「比較函數」,接收兩個值(a, b)做爲參數並返回一個數值,若是返回負數則a排在b以前,若是返回正數則相反,若是返回是0, 則通常將這兩個值保持原來的前後順序一塊兒與其餘值按序排列。ECMAScript中沒有保證對比時返回0的兩個值必定保持前後順序,因此並不是全部的瀏覽器都能保證作到這一點(引用)。

[4,14,3,23].sort() // [14,23,3,4] 按數值轉換爲字符串後的字符編碼排序而非數值自己
[4,14,3,23].sort((a, b) => a - b) // [3,4,14,23] 按數值的大小進行升序排列

let a = ["x", "u", "m", "a"];
[undefined, ...a, "undefined"].sort() // ["a", "m", "u", "undefined", "x", undefined]

複製代碼

可變方法的不可變替代

使用不可變方法複製數組

例如slice, concat, map, filter等,根據不一樣須要選擇代替;

// concat 代替 push / unshift
let a = [1,2,3];
a.push(4);
a // [1,2,3,4];

let b = a.concat(5);
b // [1,2,3,4,5]
a // [1,2,3,4]

// slice 代替 pop / shift
b.pop();
b // [1,2,3,4]
let c = b.slice(0, -1);
c // [1,2,3]
b // [1,2,3,4]
複製代碼

使用擴展操做符複製數組

擴展運算符能夠方便地對數組進行復制或部分複製,不會改變原數組;

let a = [1,2,3,4];
let b = [..a, 5];
b // [1,2,3,4,5]
a // [1,2,3,4]
複製代碼

但須要注意的是,不管是不可變方法仍是擴展運算符,數組的複製都是淺複製,對於引用類型的元素複製的是其引用,而非整個對象。

注意:不可變方法隱蔽下的可變操做

還有一點值得注意,雖然不可變方法自己不會改變原數組,可是由於數組自己是引用類型的值,若是在回調函數中引用數組自己並對其元素進行改變操做或從新賦值,仍是會「隱蔽地」修改原數組。這種作法應該儘可能避免,由於在後續維護時可能會給別人帶來沒必要要的困擾(不知在哪裏莫名其妙值就被改變了)。例如:

let c = [9, 8, 7, 6, 5];
c.map((n, i) => c[i] = i);
// 此時c已變成[0, 1, 2, 3, 4]
複製代碼

當回調函數很是長的時候這種問題更難定位,其餘引用c值的變量頗有可能也同時被影響。因此每一個函數最好都目的明確只作一件事,把確實須要改變原引用值的操做放在一個專門的函數中操做,而不是散佈在任何看起來不會發生這種改變的地方。

參考

相關文章
相關標籤/搜索