前面的話
數組是一種基礎的JS對象,隨著時間推進,JS中的其他部分一直在演進,而直到ES5標準才為數組對象引入一些新方法來簡化使用。ES6標準繼續改進數組,添加了很多新功能。本文將詳細介紹ES6數組擴展
靜態方法
在ES6以前,創建數組的方式主要有兩種,一種是調用Array構造函數,另一種是用數組字面量語法,這兩種方法均需列舉數組中的元素,功能非常受限。如果想將一個類數組對象(具有數值型索引和length屬性的對象)轉換為數組,可選的方法也十分有限,經常需要編寫額外的代碼。為了進一步簡化JS數組的創建過程,ES6新增了Array.of()和Array.from()兩個方法
【Array.of()】
ES6之所以向JS添加新的創建方法,是要幫助開發者們規避通過Array構造函數創建數組時的怪異行為
let items = new Array(2); console.log(items.length); // 2 console.log(items[0]); // undefined console.log(items[1]); // undefined items = new Array("2"); console.log(items.length); // 1 console.log(items[0]); // "2" items = new Array(1, 2); console.log(items.length); // 2 console.log(items[0]); // 1 console.log(items[1]); // 2 items = new Array(3, "2"); console.log(items.length); // 2 console.log(items[0]); // 3 console.log(items[1]); // "2"
如果給Array構造函數傳入一個數值型的值,那么數組的length屬性會被設為該值。如果傳入多個值,此時無論這些值是不是數值型的,都會變為數組的元素。這個特性令人感到困惑,不可能總是注意傳入數據的類型,所以存在一定的風險
ES6通過引入Array.of()方法來解決這個問題。Array.of()與Array構造函數的工作機制類似,只是不存在單一數值型參數值的特例,無論有多少參數,無論參數是什么類型的,Array.of()方法總會創建一個包含所有參數的數組
let items = Array.of(1, 2); console.log(items.length); // 2 console.log(items[0]); // 1 console.log(items[1]); // 2 items = Array.of(2); console.log(items.length); // 1 console.log(items[0]); // 2 items = Array.of("2"); console.log(items.length); // 1 console.log(items[0]); // "2"
要用Array.of()方法創建數組,只需傳入希望在數組中包含的值。第一個示例創建了一個包含兩個數字的數組;第二個數組包含一個數宇;最后一個數組包含一個字符串。這與數組字面量的使用方法很相似,在大多數時候,可以用數組字面量來創建原生數組,但如果需要給一個函數傳入Array的構造函數,則可能更希望傳入Array.of()來確保行為一致
function createArray(arrayCreator, value) { return arrayCreator(value); } let items = createArray(Array.of, value);
在這段代碼中心createArray()函數接受兩個參數,一個是數組創造者函數,另一個是要插入數組的值。可以傳入Array.of()作為createArray()方法的第一個參數來創建新數組,如果不能保證傳入的值一定不是數字,那么直接傳入Array會非常危險
[注意]Array.of()方法不通過Symbol.species屬性確定返回值的類型,它使用當前構造函數(也就是of()方法中的this值)來確定正確的返回數據的類型
【Array.from()】
JS不支持直接將非數組對象轉換為真實數組,arguments就是一種類數組對象,如果要把它當作數組使用則必須先轉換該對象的類型。在ES5中,可能需要編寫如下函數來把類數組對象轉換為數組
function makeArray(arrayLike) { var result = []; for (var i = 0, len = arrayLike.length; i < len; i++) { result.push(arrayLike[i]); } return result; } function doSomething() { var args = makeArray(arguments); // 使用 args }
這種方法先是手動創建一個result數組,再將arguments對象里的每一個元素復制到新數組中。盡管這種方法有效,但需要編寫很多代碼才能完成如此簡單的操作。最終,開發者們發現了一種只需編寫極少代碼的新方法,調用數組原生的slice()方法可以將非數組對象轉換為數組
function makeArray(arrayLike) { return Array.prototype.slice.call(arrayLike); } function doSomething() { var args = makeArray(arguments); // 使用 args }
這段代碼的功能等價于之前的示例,將slice()方法執行時的this值設置為類數組對象,而slice()對象只需數值型索引和length屬性就能夠正確運行,所以任何類數組對象都能被轉換為數組
盡管這項技術不需要編寫很多代碼,但是我們調用Array.prototype.slice.call(arrayLike)時不能直覺地想到這是在將arrayLike轉換成一個數組。所幸,ES6添加了一個語義清晰、語法簡潔的新方法Array.from()來將對象轉化為數組
Array.from()方法可以接受可迭代對象或類數組對象作為第一個參數,最終返回一個數組
function doSomething() { var args = Array.from(arguments); // 使用 args }
Array.from()方法調用會基于arguments對象中的元素創建一個新數組,args是Array的一個實例,包含arguments對象中同位置的相同值
[注意]Array.from()方法也是通過this來確定返回數組的類型的
映射轉換
如果想要進一步轉化數組,可以提供一個映射函數作為Array.from()的第二個參數,這個函數用來將類數組對象中的每一個值轉換成其他形式,最后將這些結果儲存在結果數組的相應索引中
function translate() { return Array.from(arguments, (value) => value + 1); } let numbers = translate(1, 2, 3); console.log(numbers); // 2,3,4
在這段代碼中,為Array.from()方法傳入映射函數(value)=>value+1,數組中的每個元素在儲存前都會被加1。如果用映射函數處理對象,也可以給Array.from()方法傳入第三個參數來表示映射函數的this值
let helper = { diff: 1, add(value) { return value + this.diff; } }; function translate() { return Array.from(arguments, helper.add, helper); } let numbers = translate(1, 2, 3); console.log(numbers); // 2,3,4
此示例傳入helper.add()作為轉換用的映射函數,由于該方法使用了this.diff屬性,因此需要為Array.from()方法提供第三個參數來指定this的值,從而無須通過調用bind()方法或其他方式來指定this的值了
用Array.from()轉換可迭代對象
Array.from()方法可以處理類數組對象和可迭代對象,也就是說該方法能夠將所有含有Symbol.iterator屬性的對象轉換為數組
let numbers = { *[Symbol.iterator]() { yield 1; yield 2; yield 3; } }; let numbers2 = Array.from(numbers, (value) => value + 1); console.log(numbers2); // 2,3,4
由于numbers是一個可迭代對象,因此可以直接將它傳入Array.from()來轉換成數組。此處的映射函數將每一個數字加1,所以結果數組最終包含的值為2、3和4
[注意]如果一個對象既是類數組又是可迭代的,那么Array.from()方法會根據迭代器來決定轉換哪個值
實例方法
ES6延續了ES5的一貫風格,也為數組添加了幾個新的方法:includes()方法返回一個布爾值,表示數組是否包含給定的值;find()方法和findIndex()方法可以協助開發者在數組中查找任意值;fill()方法和copyWithin()方法的靈感則來自于定型數組的使用過程,定型數組也是ES6中的新特性,是一種只包含數字的數組
【includes()】
Array.prototype.includes
方法返回一個布爾值,表示某個數組是否包含給定的值,與字符串的includes
方法類似。ES2016 引入了該方法
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true
該方法的第二個參數表示搜索的起始位置,默認為0
。如果第二個參數為負數,則表示倒數的位置,如果這時它大于數組長度(比如第二個參數為-4
,但數組長度為3
),則會重置為從0
開始
[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true
沒有該方法之前,我們通常使用數組的indexOf
方法,檢查是否包含某個值
if (arr.indexOf(el) !== -1) { // ... }
indexOf
方法有兩個缺點,一是不夠語義化,它的含義是找到參數值的第一個出現位置,所以要去比較是否不等于-1
,表達起來不夠直觀。二是,它內部使用嚴格相等運算符(===
)進行判斷,這會導致對NaN
的誤判
[NaN].indexOf(NaN)// -1
includes
使用的是不一樣的判斷算法,就沒有這個問題
[NaN].includes(NaN)// true
下面代碼用來檢查當前環境是否支持該方法,如果不支持,部署一個簡易的替代版本
const contains = (() => Array.prototype.includes ? (arr, value) => arr.includes(value) : (arr, value) => arr.some(el => el === value) )(); contains(['foo', 'bar'], 'baz'); // => false
另外,Map 和 Set 數據結構有一個has
方法,需要注意與includes
區分
1、Map 結構的has
方法,是用來查找鍵名的,比如Map.prototype.has(key)
、WeakMap.prototype.has(key)
、Reflect.has(target, propertyKey)
2、Set 結構的has
方法,是用來查找值的,比如Set.prototype.has(value)
、WeakSet.prototype.has(value)
【find()和findIndex()】
由于沒有內建的數組搜索方法,因此ES5正式添加了indexOf()和lastIndexOf()兩個方法,可以用它們在數組中查找特定的值。雖然這是一個巨大的進步,但這兩種方法仍有局限之處,即每次只能查找一個值,如果想在系列數字中查找第一個偶數,則必須自己編寫代碼來實現。于是ES6引入了find()方法和findIndex()方法來解決這個問題
find()方法和findIndex()方法都接受兩個參數:一個是回調函數;另一個是可選參數,用于指定回調函數中this的值。執行回調函數時,傳入的參數分別為數組中的某個元素、該元素在數組中的索引和數組本身,與傳入map()和forEach()方法的參數相同。如果給定的值滿足定義的標準,回調函數應返回true。一旦回調函數返回true,find()方法和findIndex()方法都會立即停止搜索數組剩余的部分
二者間唯一的區別是,find()方法返回查找到的值,findIndex()方法返回查找到的值的索引
let numbers = [25, 30, 35, 40, 45]; console.log(numbers.find(n => n > 33)); // 35 console.log(numbers.findIndex(n => n > 33)); // 2
這段代碼通過調用find()方法和findIndex()方法來定位numbers數組中第一個比33大的值,調用find()方法返回的是35,而調用findIndex()方法返回的是35在numbeps數組中的位置2
如果要在數組中根據某個條件查找匹配的元素,那么find()方法和findIndex()方法可以很好地完成任務;如果只想查找與某個值匹配的元素,則indexOf()方法和lastIndexOf()方法是更好的選擇
【fill()】
fill()方法可以用指定的值填充一至多個數組元素。當傳入一個值時,fill()方法會用這個值重寫數組中的所有值
let numbers = [1, 2, 3, 4]; numbers.fill(1); console.log(numbers.toString()); // 1,1,1,1
在此示例中,調用numbers.fill(1)方法后numbers中所有的值會變成1,如果只想改變數組某一部分的值,可以傳入開始索引和不包含結束索引(不包含結束索引當前值)這兩個可選參數
let numbers = [1, 2, 3, 4]; numbers.fill(1, 2); console.log(numbers.toString()); // 1,2,1,1 numbers.fill(0, 1, 3); console.log(numbers.toString()); // 1,0,0,1
在numbers.fill(1,2)調用中,參數2表示從索引2開始填充元素,由于未傳入第三個參數作為不包含結束索引,因此使用numbers.length作為不包含結束索引,因而numbers數組的最后兩個元素被填充為1。操作numbers.fill(0,1,3)會將數組中位于索引1和2的元素填充為0。調用fill()時若傳入第二個和第三個參數則可以只填充數組中的部分元素
[注意]如果開始索引或結束索引為負值,那么這些值會與數組的length屬性相加來作為最終位置。例如,如果開始位置為-1,那么索引的值實際為array.length-1,array為調用fill()方法的數組
【copyWithin()】
copyWithin()方法與fill()方法相似,其也可以同時改變數組中的多個元素。fill()方法是將數組元素賦值為一個指定的值,而copyWithin()方法則是從數組中復制元素的值。調用copyWithin()方法時需要傳入兩個參數:一個是該方法開始填充值的索引位置,另一個是開始復制值的索引位置
比如復制數組前兩個元素的值到后兩個元素
let numbers = [1, 2, 3, 4]; // 從索引 2 的位置開始粘貼 // 從數組索引 0 的位置開始復制數據 numbers.copyWithin(2, 0); console.log(numbers.toString()); // 1,2,1,2
這段代碼從numbers的索引2開始粘貼值,所以索引2和3將被重寫。給CopyWithin()傳入第二個參數0表示,從索引0開始復制值并持續到沒有更多可復制的值
默認情況下,copyWithin()會一直復制直到數組末尾的值,但是可以提供可選的第三個參數來限制被重寫元素的數量。第三個參數是不包含結束索引,用于指定停止復制值的位置
let numbers = [1, 2, 3, 4]; // 從索引 2 的位置開始粘貼 // 從數組索引 0 的位置開始復制數據 // 在遇到索引 1 時停止復制 numbers.copyWithin(2, 0, 1); console.log(numbers.toString()); // 1,2,1,4
在這個示例中,由于可選的結束索引被設置為了1,因此只有位于索引0的值被復制了,數組中的最后一個元素保持不變
[注意]正如fill()方法一樣,copyWithin()方法的所有參數都接受負數值,并且會自動與數組長度相加來作為最終使用的索引
fill()和copyWithin()這兩個方法起源于定型數組,為了保持數組方法的一致性才添加到常規數組中的。如果使用定型數組來操作數字的比特,這些方法將大顯身手
文章列表