js基礎之探祕Array的原型方法

如果現在需要用js生成[0, 2, 4, 6, 8, …, 100]這樣一個數組,你第一時間想到的會是下面的寫法嗎?

var arr = new Array(51);
//使用臨時變量保存數組長度,可以有效避免每次循環都計算數組長度
var len = arr.length;  
for(var i = 0; i < len ; i++){
  arr[i] = i * 2;
}

或者你會想到這樣寫更好?

var arr = Array.apply(null, Array(51)).map((item, index) => {
  return index * 2;
})

再或者這樣寫?

var arr = new Array(51).fill(undefined).map((item, index) => {
  return index * 2;
})

很顯然後面兩種方式看起來更優雅。

不過有這樣一組數據(僅用於比較速度快慢,實際數值與電腦性能有關):當上述數組長度爲5萬時,三者的用時分別爲5毫秒、4毫秒、5毫秒(經過多次測試,第二種方式總是比其他兩種略快一點),三者幾乎不存在性能差距;而當這個長度增加到50萬,三者的用時分別爲:10毫秒、Max stack size(堆棧溢出)和21毫秒。當長度達到500萬時,三者用時分別來到了34毫秒、Max stack size和148毫秒。

儘管後面兩種方式所用到的數組原型方法都是用C++實現的,但是for循環的表現卻比我們預期的要好得多(其實只要沒有使用for in循環,差距都可以忽略不計)。

不過作爲前端開發者,我們應該儘量避免使用for循環實現上述功能。當數組長度較小時,後面兩種方式均可,當長度可能較大時最好還是使用第三種方式,避免因堆棧溢出而導致程序崩潰。原因是,一方面,for循環的語義不夠明確,如果這是一個複雜的數組操作,往往會看得別人雲裏霧裏。另一方面,使用原生的數組方法往往比我們自己實現更加安全可靠,它很少會產生一些怪異的行爲(堆棧溢出不算是怪異行爲,這裏所說的怪異行爲指程序看似正常執行,但結果卻與預期不符)。

很多時候,如果你的程序中出現了上面這一小段優雅的代碼,閱讀代碼的人馬上就會對你刮目相看。但是話說回來,掌握數組的原生方法本就應是每個前端程序員的“第一課”,下面就讓我們一起來探祕數組的靜態方法(不帶prototype的就是靜態方法,它需要使用Array構造函數直接調用,比如下面的Array.from)及原型方法(你可以在數組實例上調用它們)。

1. Array.from( arrayLike[, mapFn[, thisArg]])

MDN描述 - 從一個類似數組或可迭代對象創建一個新的,淺拷貝的數組實例。

它最多接收三個參數,第一個是需要轉化的類數組對象或可迭代對象;第二個是一個map函數,用於在轉化後對新生成的數組的每個元素執行操作,可選;第三個是執行第二個map函數時的this對象,即引擎會在該對象上調用傳入的第二個函數,可選。

比如:

function func(){
  var args = Array.from(arguments, function(item, index){
	return JSON.stringify(item);
  }, window);  //將arguments轉化爲數組,並將每個參數字符串化
}

函數內部的arguments就是一個類數組對象,它用於接收所有傳入函數的參數,有length屬性,且是可迭代的(iterable。也就是具有Iterator迭代器,該迭代器的next方法可以按序遍歷出該對象的每一個屬性)的。

但是它不是真正的數組,你無法在該對象上直接調用數組的原型方法。爲了像真正的數組一樣操作這個對象,我們使用Array.from將其轉化爲真正的數組([].slice.call(arguments)也是同樣的作用,不過from方法看上去更加直接和優雅)。這樣你就可以更簡單地操作這個參數對象了。比如:

  ...
  var temp = args.slice(1); //提取從第二個到最後的所有參數

與之類似的對象還有DOM中的childNodes屬性,嚴格來講,它是NodeList類型的對象。但它具有length屬性,並且可迭代,因此我們通常像處理數組一樣來處理它。如果希望把它變成真正的數組,就可以使用上面的Array.from方法。而DOM在定義childNodes屬性時之所以沒有設置爲數組,是因爲NodeList具備隨DOM結構變化而動態變化的能力,數組則不具備這種能力。

總之,像arguments和childNodes這樣具有length屬性,並且內部實現了迭代器Iterator的對象,都可以使用Array.from來轉化爲真正的數組。不過注意,這個轉化只是個淺拷貝,也就是說,如果原對象的某個屬性是對象,那麼創建出來的數組和原屬性引用的是同一個對象,因此對這類屬性的操作會對兩者同時造成影響。爲了避免這種影響,你可以簡單實用下面的語法來進行一次深拷貝:

JSON.parse(JSON.stringify(arr));

把得到的數組經過上述轉化,就可以創建一個與原對象無關的新數組,從而實現了深拷貝。

2. Array.isArray(obj)

MDN描述 - 用於確定傳遞的值是否是一個 Array。

用於判斷當前對象是不是數組,使用方法很簡單,將需要判斷的對象傳入該方法,如果它是數組,將返回true,否則將返回false。

Array.isArray的檢測原理類似於instanceof。但是在涉及iframes時,它的優先級應高於instanceof,因爲instanceof不能跨iframe檢測數組,比如下面的例子:

//在頁面中創建一個iframe
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
//拿到iframe內的Array構造函數的引用,
//它與當前環境的Array並不是同一個構造函數
xArray = window.frames[window.frames.length-1].Array;

//使用iframe內的Array構造函數構造了一個數組
var arr = xArray(1,2,3);  //[1,2,3]

arr instanceof Array;//false,iframe內的Array與當前環境
					 //的Array不是同一個,檢測失敗
Array.isArray(arr); //true,isArray成功檢測到它是一個數組

涉及跨iframe檢測數組時要注意這一點。

3. Array.of(element0[, element1[, …[, elementN]]])

MDN描述 - 創建一個具有可變數量參數的新數組實例,而不考慮參數的數量或類型。

用過js的同學應該知道,js的數組存在一個似乎不太合常理的行爲。如果你在構造函數中傳入超過一個參數,那麼他們將成爲數組的元素:

var arr = new Array(10, 20);  //[10, 20];

但是如果只傳入一個整數,引擎卻會爲你創建一個該長度的空數組:

var arr = new Array(10); //[],長度爲10,每個元素都是empty

如果你希望用這種方式創建僅有一個元素爲10的數組,那麼你得到的結果可能不會符合預期。但由於歷史原因,糾正這種行爲也不太可能。

因此es規範推出了Array.of()來提供更規範的實現。傳入Array.of()的參數都將作爲數組元素創建新數組。現在你可以像下面這樣創建[10]:

var arr = Array.of(10);

它的職責看起來更單一,因此你不需要擔心出現什麼怪異行爲。

4. Array.prototype.concat()

MDN描述 - 用於合併兩個或多個數組。此方法不會更改現有數組,而是返回一個新數組。

語法:var new_array = old_array.concat(value1[, value2[, …[, valueN]]])

它會將多個數組的元素拷貝出來,構成一個數組。如下:

var arr1 = [1,2];
var arr2 = [3];

var arr3 = arr1.concat(arr2); //[1,2,3]

concat不會影響原數組,而是返回一個新數組。並且所執行的是淺拷貝,也就是說,如果數組的某個元素是對象,那麼返回的新數組將持有與原數組相同的引用,修改其中一個的值,都會立即影響到另一個數組中該元素的值。

如果傳入concat的某個參數不是數組,那麼它將作爲單個元素被添加到最終返回的數組,如:

傳入的對象{carter:24}被視爲了[{carter:24}]進行拼接
var arr = [1,2].concat([3], {carter: 24}); //[1,2,3,{carter: 24}]

5. Array.prototype.copyWithin()

MDN描述 - 淺複製數組的一部分到同一數組中的另一個位置,並返回它,不會改變原數組的長度。

語法:arr.copyWithin(target[, start[, end]])

它首先提取從start到end-1位置的數組元素(類似於slice的行爲),然後從target位置開始進行替換,最後返回修改後的數組。比如我希望把數組的最後兩個元素複製並替換到數組的開頭位置,那麼target就是0(因爲需要替換開頭的元素),start是arr.length - 2,表示從索引爲arr.length - 2的位置開始複製,end爲arr.length,表示複製的結束位置爲arr.length - 1,它是數組的最後一個元素。

var arr = [1,2,3,4,5];
arr.copyWithin(0, arr.length - 2, arr.length);

arr //=>[4, 5, 3, 4, 5];

這裏的start和end爲可選參數,start默認爲0,end默認爲arr.length。

爲什麼end表示複製到數組的end - 1的位置呢?這是因爲數組的下標是從0開始的,也就是說數組的第一個元素是arr[0]。如果數組的長度爲10,那麼它的最後一個元素是arr[9]。所以如果end表示的是結束位置的索引值,那麼你每次想複製到最後一個元素,都必須傳入end的值爲arr.length - 1,這是很令人討厭的行爲。於是工作組規定,複製到end的前一個位置就會結束,現在你可以優雅地將arr.length作爲end傳入函數。類似的還有slice這樣的函數。

6. Array.prototype.entries()

MDN描述 - 返回一個新的Array Iterator對象,該對象包含數組中每個索引的鍵/值對。

語法:arr.entries()

該原型方法返回一個迭代器對象,通過該對象,你可以按順序迭代數組的每個元素。比如:

var arr = ["a", "b", "c"];
var iterator = arr.entries();
console.log(iterator);

/*Array Iterator {}
         __proto__:Array Iterator
         next:ƒ next()
         Symbol(Symbol.toStringTag):"Array Iterator"
         __proto__:Object
*/

對於數組arr,調用entries方法得到了一個Iterator對象(也稱爲遍歷器),它最主要的作用是向外暴露了next方法,如果嘗試調用next方法,你會得到下面一個對象:

iterator.next() //{value: [0, "a"], done: false}

value是由鍵值對構成的數組,由於當前被遍歷的是一個數組,因此這裏的鍵就是元素的索引,而值就是元素的值。done表示當前遍歷是否結束,爲true則表示遍歷結束。因此你可以像下面一樣遍歷數組:

for(var i = 0; i < arr.length + 1; i++){
  var tem = iter.next(); // 每次迭代時更新next
  console.log(tem);
  
  if(!tem.done){
    ...   //對該元素執行一些操作
  }
}

可以看到在控制檯將輸出這樣一組對象:

{value: [0, "a"], done: false}
{value: [1, "b"], done: false}
{value: [2, "c"], done: false}
{value: undefined, done: true}

之所以for循環的結束條件需要寫爲i < arr.length + 1,是因爲Iterator在輸出最後一個元素後並不知道遍歷已經結束了,只有多檢查一次才知道所有的屬性都輸出完畢了。

entries方法使得遍歷數組元素變得非常靈活和安全。假如你對數組元素的操作無法在一個for循環內完成(如存在異步操作或跨模塊問題),那麼你就必須藉助一個變量來記錄當前你處理到了第幾個元素,這樣使得你的遍歷操作內聚性非常差,並因此會導致一個安全問題 – 你無法保證你用於記錄索引的這個變量不會被無意或惡意地修改。而通過entries方法得到遍歷器,你可以在任何位置調用next方法獲取下一個要操作的元素,遍歷器自身可以記錄當前處理到第幾個元素。

Iterator遍歷器是es6中非常重要的一個對象,如果你希望自定義的js對象可以使用for … of語句進行遍歷,那麼你只需要手動爲該對象實現一個遍歷器屬性即可。此外,實現了遍歷器的對象可以使用上面講到的Array.from轉化爲數組,一些複雜的處理將變得非常方便。遍歷器不是本文的重點,感興趣的可以自行查閱資料瞭解。

7. Array.prototype.every()

MDN描述 - 測試一個數組內的所有元素是否都能通過某個指定函數的測試。它返回一個布爾值。

語法:arr.every(callback[, thisArg])

這是一個較爲常見的函數,用於檢查當前數組的每個元素是否都能滿足某個條件。如果將元素傳入callback,返回了true,則表示通過測試,否則表示未通過測試。如果數組的每個元素都通過測試,則every方法返回true,否則返回false。如:

[1,2,3,4,5,6,7].every(function(item, index, array){
  return item < 10;
})   //true

[1,2,3,14,5,6,57].every(function(item, index, array){
  return item < 10;
})   //false

上述回調函數用於檢查是否每個數組元素都小於10,第一個數組通過了測試,而第二個沒有通過。

回調函數最多接收三個參數,分別是當前的數組元素(上述item)、該元素的索引值(index)和數組本身(array)。由於這裏的參數只是形參,因此它們的名字可以隨意起,其實際值由引擎執行該函數時傳入。

8. Array.prototype.fill()

MDN描述 - 用一個固定值填充一個數組中從起始索引到終止索引內的全部元素。不包括終止索引。

語法:arr.fill(value[, start[, end]])

從字面意思理解,就是使用傳入的value值填充數組。填充區間爲start ~ end - 1。這裏end的含義與copyWithin類似,它表示填充到索引爲end的前一個位置,因此你可以直接傳入arr.length指代填充到最後一個元素。

注意,使用new Array()創建出的空數組與使用fill(null)或fill(undefined)填充數組得到的結果是不一樣的。如:

var a = new Array(10);//[empty * 10]
console.log(a[0]); //undefined

a.fill(null); [null, null, ... null];
console.log(a[0]); //null
a.fill(undefined); [undefined, undefined, ... undefined]
console.log(a[0]); //undefined

填充爲null的數組自然與不填充是不一樣的,因爲null本身是個對象,有自己的內存地址。但是填充爲undefined和不進行填充有差別嗎?

當然是有的。還記得我們最開始的例子嗎?我們使用:

var arr = new Array(51).fill(undefined).map((item, index) => {
  return index * 2;
})  //[0, 2, 4, ..., 100]

而不是:

var arr = new Array(51).map((item, index) => {
  return index * 2;
})   //[empty × 51]

使用fill填充最初的數組後,我們得到了預期的結果。但是如果不使用fill進行填充,我們得到的仍然是長度爲51的空數組。這是因爲對於空元素,js引擎不會爲其執行map回調函數,但是對於值爲undefined的元素,js引擎會執行該回調函數。當某個元素爲empty時,js引擎會告訴你它的值爲undefined,但這只是js引擎的默認行爲,你必須清楚的是,數組元素爲empty和undefined對引擎來說並不完全等價。

9. Array.prototype.filter()

MDN描述 - 創建一個新數組, 其包含通過所提供函數實現的測試的所有元素。

語法:var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

同樣非常常見的一個方法,從字面意思理解,就是過濾數組。它只會留下能夠通過回調函數測試的那些元素。如:

//如果用不到index和array,這裏也可以不寫
[1,72,3,4,15,6,7,38,9].filter((item, index, array) => {
  return item > index * 2;
})   => [72, 15, 38]

上述代碼返回比自身索引值兩倍還大的數組元素,最終得到了由72,15和38構成的一個數組。注意,該方法不會修改原數組,而是創建一個新數組。

一個經常犯錯的點是,即使數組中必定只有一個元素滿足條件,得到的結果也是一個數組,因此獲取該元素時請取結果數組的第一個元素。

10. Array.prototype.find()

MDN描述 - 返回數組中滿足提供的測試函數的第一個元素的值。否則返回 undefined。

語法:arr.find(callback[, thisArg])

類似於filter的作用,但它只返回符合條件的第一個數組元素,得到的結果是符合條件的第一個元素,而不是一個數組。

如果你清楚知道數組中必定只有一個滿足條件的元素,或者你只關心第一個滿足條件的元素,那麼你可以使用find方法。它的用法與filter類似:

[1,72,3,4,15,6,7,38,9].find((item, index, array) => {
  return item > index * 2;
})   => 72

11. Array.prototype.findIndex()

MDN描述 - 返回數組中滿足提供的測試函數的第一個元素的索引。否則返回-1。

語法: arr.findIndex(callback[, thisArg])

這是find的姊妹方法,用法幾乎完全一致,只是它返回的是第一個符合條件的元素的索引值。這裏不再詳述。

12. Array.prototype.flat()

MDN描述 - 按照一個可指定的深度遞歸遍歷數組,並將所有元素與遍歷到的子數組中的元素合併爲一個新數組返回。

語法:var newArray = arr.flat([depth])

這是數組的扁平化方法,用於數組降維。接收的參數爲需要降低的維數。如:

[[1,2], [3], 4].flat(1); //[1,2,3,4]

[[[1],2], [3], 4].flat(1); //[[1],2,3,4]

[[[2],2], [3], 4].flat(2); //[1,2,3,4]

傳入的depth爲1時,可以把二維數組降爲一維數組,或把三維數組降爲二維數組等。傳入的depth爲2時,可以把三維數組降爲一維數組,以此類推。不傳depth時,默認值爲1。如果當前數組的維數小於depth,那麼降到一維後就直接返回。

如果你希望無論傳入多少維的數組,都壓縮成一維數組,那麼你可以將depth設置爲Infinity(即flat(Infinity)),它在js中表示無窮大,是一個常量。

值得注意的是,flat方法會去除數組中的空元素。比如:

[1,2,3, ,5].flat() //[1,2,3,5]

var arr = new Array(10).flat() //[]

13. Array.prototype.flatMap()

MDN描述 - 首先使用映射函數映射每個元素,然後將結果壓縮成一個新數組。

語法:var new_array = arr.flatMap(function callback(currentValue[, index[,array]]) {
// 返回新數組的元素
}[, thisArg])

這個函數就是map和flat方法的組合,只是flat的depth只能爲1。關於map方法後面會介紹到。它進行的操作就是,首先調用傳入的函數(也就是用於映射的map函數)得到結果數組,然後對這個數組執行flat(1)進行降維。

下面的兩種寫法幾乎完全等價:

[1,2].flatMap((item, index) => {
  return [index, item];
})   //[0, 1, 1, 2]

[1,2].map((item, index) => {
  return [index, item];
}).flat(1)   //[0, 1, 1, 2]

14. Array.prototype.forEach()

MDN描述 - 對數組的每個元素執行一次提供的函數。

語法:arr.forEach(callback[, thisArg]);

該方法沒有返回值,也不會修改原數組,它的行爲只是對數組的每個元素執行一次傳入的回調函數。比如你希望按序輸出數組的每個元素,使用forEach方法可以這麼寫:

[1,2,3,4,5].forEach(item => {
  console.log(item)
})  //依次輸出1 2 3 4 5

15. Array.prototype.includes()

MDN描述 - 用來判斷一個數組是否包含一個指定的值,根據情況,如果包含則返回 true,否則返回false。

語法: arr.includes(valueToFind[, fromIndex])

第一個參數爲需要檢查的數組元素,第二個爲查找起點,表示從某個元素之後開始查找,該參數可選。如果找到了該元素,則返回true,否則返回false。

注意,該方法的比對規則與===類似,是地址比對,這也就意味着你無法在數組中查找對象元素:

[{}].includes({});  //false,因爲{} !== {},兩者在內存中有不同的地址

該方法的用途也很簡單,就是檢查某個數組中是否存在某個元素。

16. Array.prototype.indexOf()

MDN描述 - 返回在數組中可以找到一個給定元素的第一個索引,如果不存在,則返回-1。

語法:arr.indexOf(searchElement[, fromIndex])

這可能是最常用的數組原型方法之一了。它最多接收兩個參數,需要查找的元素和查找起點,返回該元素在數組中的索引值。如果沒有查找到該元素,則返回-1。因此我們經常看到下面的代碼來檢測某個元素是否存在於數組中:

if(arr.indexOf(target) > -1){
  ...
}

如果你只是希望檢測target是否存在與arr中,那麼你應該使用arr.includes(target),而不是indexOf,因爲在這裏,前者的語義更爲明確。

17. Array.prototype.join()

MDN描述 - 將一個數組(或一個類數組對象)的所有元素連接成一個字符串並返回這個字符串。如果數組只有一個項目,那麼將返回該項目而不使用分隔符。

語法:arr.join([separator])

也是一個常用的數組原型方法。它使用傳入的字符來將數組連接爲字符串,這種操作常見於在頁面上顯示數組。join的默認參數是英文中的逗號“,”,如:

["小明","小紅"].join();  //“小明,小紅”

["小明","小紅"].join("-");  //“小明-小紅”

在中文環境下,我們通常傳入中文的逗號“,”作爲分隔符。與英文的逗號不同的是,中文的逗號與一個漢字是等寬的,而英文逗號只有半個漢字的寬度。

數組的join方法與字符串的split方法是一對互操作,比如上面的字符串使用split方法可以再轉化成數組:

"小明-小紅".split("-"); //["小明","小紅"]

這組互操作在數據顯示中非常常用。

18. Array.prototype.keys()

MDN描述 - 返回一個包含數組中每個索引鍵的Array Iterator對象。

語法:arr.keys()

該方法與entries方法類似,但是它返回的迭代器只能迭代出數組每個元素的索引值,相對於Object原型上的keys方法,該方法的價值似乎比較小,因爲數組元素的索引值不太需要專門的遍歷器來遍歷,我們更關心的是數組的元素。我們知道,js中數組就是對象,那何不用對象的keys方法來遍歷數組呢?這是因爲對象的keys方法無法遍歷出空元素的鍵,比如:

var arr = ["a", , "c"];

var sparseKeys = Object.keys(arr);
var denseKeys = [...arr.keys()];

console.log(sparseKeys); // ['0', '2']
console.log(denseKeys);  // [0, 1, 2]

該方法更多是爲了數組對象的完整性而設計的,比較少用,這裏不再詳述。

19. Array.prototype.lastIndexOf()

MDN描述 - 返回指定元素(也即有效的 JavaScript 值或變量)在數組中的最後一個的索引,如果不存在則返回 -1。從數組的後面向前查找,從 fromIndex 處開始。

語法:arr.lastIndexOf(searchElement[, fromIndex])

indexOf的孿生方法,與indexOf不同的是,該方法是從後往前查找,找到第一個匹配的元素之後就返回它的索引值。如果傳入了fromIndex,將從該位置向前查找。

20. Array.prototype.map()

MDN描述 - 創建一個新數組,其結果是該數組中的每個元素都調用一個提供的函數後返回的結果。

語法:var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])

如果你經常使用js,那麼這個原型方法可能是你最常用的原型方法之一了。比如我們最開始生成那個數組的後面兩個方法的優雅之處就在於用到了map函數。它的作用是映射數組元素。比如我們有下面一個數組:

var arr = [1,2,3]

我們希望把它映射爲:[2,4,6],於是你可以這樣寫:

var arr2 = arr.map((item, index) => {
  return item * 2;
})

注意,map函數不會改變原數組,因此你必須使用一個變量接收操作結果。如果原數組已經沒用了,你也可以賦值到原數組。

上面代碼的含義是,將原數組映射到另一個結果數組,由傳入map的函數定義映射規則。這裏所給的映射規則爲:結果數組的元素是原數組元素的2倍。注意,映射關係是建立在兩個數組相同索引的元素上的。也就是:

(item => item * 2)
arr[0] = 1   =>  arr[2] = 2
arr[1] = 2   =>  arr[1] = 4
arr[2] = 3   =>  arr[2] = 6

arr => [1,2,3]
arr2 => [2,4,6]

js引擎每次會按序把一個原數組元素傳入回調函數,得到結果數組中對應位置的元素。當原數組元素遍歷完,映射過程就結束了。

21. Array.prototype.push/pop/unshift/shift()

這是四個數組的原型方法,分別是從尾部添加和刪除一個元素,從頭部刪除和添加一個元素。

如果學過C語言的話應該知道,C語言中存在棧和隊列的概念。棧只允許從頂部(也就是尾部)增刪元素,而隊列只允許在尾部增加元素,在頭部刪除元素(就像排隊一樣,先入先出)。js中沒有直接定義棧和隊列這樣的數據結構,而是使用數組來模擬兩者的行爲。

push和pop用於模擬棧的行爲,push爲從數組尾部插入元素,pop爲從數組尾部刪除一個元素。前者沒有返回值,但會改變原數組,後者也會改變原數組,並且返回被刪除的元素。

shift和push可以模擬隊列的行爲,push與棧的行爲一致,而shift會從數組頭部刪除一個元素(出隊操作),並返回這個元素。爲了方便,js還擴充了unshift方法,它是shift的逆操作,即從數組頭部插入一個元素。

有了這四個方法,你可以像操作C語言的棧和隊列一樣操作js的數組。

22. Array.prototype.reduce()

reduce: 減少,縮小。

MDN描述 - 對數組中的每個元素執行一個由您提供的reducer函數(升序執行),將其結果彙總爲單個返回值。

語法:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

reduce本身可以接受兩個參數:reducer函數和初始值initialValue,前者是定義如果彙總數組元素,後者定義初始值,如果不傳初始值,數組的第一個元素將作爲初始值使用,並且reduce將跳過第一個元素,從第二個元素開始執行reducer函數。

reducer 函數接收4個參數:

  1. Accumulator (acc) (累計器)
  2. Current Value (cur) (當前值)
  3. Current Index (idx) (當前索引)
  4. Source Array (src) (源數組)

reducer 函數的返回值會被分配給累計器,該返回值在數組的每個迭代中被記住,並最後成爲最終的單個結果值。

這是一個強大但經常被忽略的數組原型方法,開發者們傾向於使用複雜的for循環和一些變量來實現reduce本可以實現的功能。reduce的作用是依次處理數組中的每一項,把每次的處理結果傳遞給下次繼續處理,直到數組處理完畢,得到一個最終結果(也就是彙總)。一個非常典型的用法是數組求和:

var arr = [1,2,3,4,5,6,7];
arr.reduce((total, currentVal, index, src) => {
  return total + currentVal;
})    //輸出28,即原數組每項的和

上面的代碼中,由於沒有傳入初始值,js引擎以第一項1作爲初始值,然後從第二項2開始進行求和。對2求和時,把之前的求和結果1作爲total參數傳入,得到第一步的彙總值3。然後引擎繼續對第三個元素進行彙總,並把之前的彙總結果3作爲total參數傳入,繼續求和,得到彙總值6。以此類推,直到彙總到數組的最後一個元素7,返回最終的彙總結果28。

如果傳入了初始值,那麼js引擎將從第一個元素開始彙總,並以初始值作爲累加結果傳入回調函數。

這裏取名reduce的本意,就是將一個數組從某個長度縮減到單個的彙總結果。React的核心庫redux中有一個reducer的概念,也正是借鑑的這種思想。

23. Array.prototype.reduceRight()

上述方法的孿生方法,用於從後向前彙總數組元素,所傳參數與上述方法一致,這裏不再詳述。

24. Array.prototype.reverse()

MDN描述 - 將數組中元素的位置顛倒,並返回該數組。數組的第一個元素會變成最後一個,數組的最後一個元素變成第一個。該方法會改變原數組。

語法: arr.reverse()

用於顛倒數組元素,比如:

[1,2,3,4].reverse()  => [4,3,2,1]

它不接受任何參數,返回顛倒後的結果。該方法會改變原數組,因此你不必使用變量來接收操作結果。

25. Array.prototype.slice()

MDN描述 - 返回一個新的數組對象,這一對象是一個由 begin 和 end 決定的原數組的淺拷貝(包括 begin,不包括end)。原始數組不會被改變。

語法:arr.slice([begin[, end]])

數組的切片方法。你可以使用該方法從數組中提取若干元素,且不會影響到原數組。它接收兩個參數,start和end,表示從start位置開始,一直取到end - 1的位置,end默認爲數組長度,即截取到最後一個元素。比如:

var a = [1,2,3,4,5];

var b = a.slice(2);  //[3,4,5]
var c = a.slice(2, 4);  //[3,4]
a   //[1,2,3,4]

除了常見的數組操作,slice還長被用於將類數組對象轉化爲數組,即前文提到的Array.slice.call(arguments)。

26. Array.prototype.some()

MDN描述 - 測試數組中是不是至少有1個元素通過了被提供的函數測試。它返回的是一個Boolean類型的值。

語法:arr.some(callback(element[, index[, array]])[, thisArg])

該方法與every對應,every用於檢測是否所有的元素都能通過回調函數測試,some用於檢測是否存在可以通過測試的數組元素。這裏只要有一個元素可以使回調函數返回true,則some方法立即返回true,如果全部測試失敗,則some方法返回false。

具體用法請參考every方法。

27. Array.prototype.sort()

MDN描述 - 用原地算法對數組的元素進行排序,並返回數組。默認排序順序是在將元素轉換爲字符串,然後比較它們的UTF-16代碼單元值序列時構建的。

語法:arr.sort([compareFunction])

它可以對數組元素進行排序,接收一個排序函數作爲參數。js引擎每次將兩個數組元素作爲參數傳入排序函數,根據返回的數值的正負判斷哪個元素排在前面。如果返回負數,則傳入的前一個元素應該排在前面,否則後一個應該排在前面。如:

[1,8,4,6,2,9].sort((num1, num2) => {
  return num1 - num2;
})   //[1,2,4,6,8,9]

[1,8,4,6,2,9].sort((num1, num2) => {
  return num2 - num1;
})   //[9,8,6,4,2,1]

sort方法的排序規則相當靈活,如果感興趣可以參考之前的JavaScript的函數式特性一文。

28. Array.prototype.splice()

MDN描述 - 通過刪除或替換現有元素或者原地添加新的元素來修改數組,並以數組形式返回被修改的內容。此方法會改變原數組。

語法:array.splice(start[, deleteCount[, item1[, item2[, …]]]])

增刪數組元素的一個非常靈活的方法。它可以在任意位置刪除若干個元素,或者插入若干個元素。它接收的第一個參數是進行操作的索引位置,js引擎將從該位置刪除或新增元素。第二個參數是要刪除的元素數量,如果你不需要刪除元素,這個值傳入0即可。後面的參數都是需要插入到該位置的元素。注意,如果你沒有執行刪除就想該位置插入元素,那麼該位置原來的元素將被排在所插入的所有元素的後面。如:

 //[1,5,6],從index爲1的位置刪除三個元素
[1,2,3,4,5,6].splice(1, 3);

//[1,7,8,2,3,4,5,6],從index爲1的位置插入7和8兩個元素
[1,2,3,4,5,6].splice(1, 0, 7, 8); 

//[1,9,3,4,5,6]刪除index爲1的元素,並添加元素9,也就是替換該元素
[1,2,3,4,5,6].splice(1, 1, 9);

29. Array.prototype.toLocaleString/toString()

MDN描述 - 返回一個字符串表示數組中的元素。

語法:arr.toLocaleString([locales[,options]]);

將數組轉化爲字符串,並且基於特定語言環境。比如當前設置語言環境爲中文,那麼拼接字符串時將使用中文的逗號隔開,如果某個元素是日期,也會使用中文格式來描述。與此對應的是Array.prototype.toString(),它以國際標準語言環境(英文)將數組轉化爲字符串,此時數組元素將以英文的逗號隔開,日期類型的元素也是英文格式的。

關於語言環境代碼可以自行查閱資料,比較常見的語言環境如英文爲:“en”,中文爲“zh”等。

30. Array.prototype.values()

MDN描述 - 返回一個新的 Array Iterator 對象,該對象包含數組每個索引的值。

語法:arr.values()

該方法與entries和keys是類似的。只是entries返回的遍歷器用於遍歷鍵值對,keys返回的用於遍歷鍵,那麼values返回的遍歷器就是用於遍歷元素值。相對於keys方法,values方法在數組遍歷中價值更高。比如你可以像下面一樣遍歷數組元素:

const array1 = ['a', 'b', 'c'];
const iterator = array1.values();

for (const value of iterator) {
  console.log(value); // 依次輸出"a" "b" "c"
}

同樣,你可以使用next方法依次遍歷出數組的每一個元素。它也包含一個done屬性,用來標記當前遍歷是否結束。

總結

這裏彙總了目前正在使用的幾乎所有的數組原型方法,一些不推薦或者已經廢棄的方法,如Array.observe、Array.toSource並沒有介紹,我們也不必關心這些方法的用法。本文篇幅有限,只能介紹這些方法的簡單用法,如果感興趣,請參閱MDN開發者文章進行深入學習Array.prototype。熟練運用這些原型方法對學習js至關重要,需要日常工作中大量積累才能掌握。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章