Array、Set、Map 與 ArrayBuffer
August 9, 2022來看看幾個 API,以及 ES6 以後,內建的群集 API,善加利用可以省不少功夫。
Array
陣列是經常使用的資料結構,ES6 以後 Array
上新增了一些 API 值得介紹一下。首先是替換 Array
建構式用的 Array.of
:
> Array.of(1);
[ 1 ]
> Array.of(1, 2);
[ 1, 2 ]
> Array.of(1, 2, 3);
[ 1, 2, 3 ]
> Array.of(1, 2, 3, undefined, 4);
[ 1, 2, 3, undefined, 4 ]
>
在只有一個引數的時候,不會像 Array(1)
或 new Array(1)
有別於其他引數長度時的行為,如果中間有元素從缺,使用 Array.of
必須明確指定從缺的值是什麼(null
?undefined
?),如果你寫 [1, 2, 3, ,4 ]
會如何呢?索引 3 處會是空項目(empty item),不是 undefined
,在〈數字為特性的陣列〉談過,空項目並不是 undefined
。
除此之外,大概就是在某些場合需要指定回呼函式,可以將 Array.of
傳入(指定 Array
?認真的嗎?),因為你不能將 []
當成函式傳入。
在〈數字為特性的陣列〉談過,在 JavaScript 的世界中,會有很多機會遇到許多長得像陣列(Array-like)的物件,而有時候,需要將這些物件轉成陣例,使之具有真正陣列該有的行為,這不單只是將 forEach
之類的方法設給該物件,還有許多瑣碎的事要做。
這時 ES6 的 Array.from
可以省許多功夫:
> let o = {
... '0' : 100,
... '1' : 200,
... '2' : 300,
... length : 3
... };
undefined
> let arr = Array.from(o);
undefined
> arr.forEach(elem => console.log(elem));
100
200
300
undefined
> arr instanceof Array;
true
>
Array.from
遇到有空項目,或轉換過程會發生空項目的情況,會明確使用 undefined
:
> Array.from([1,,2]);
[ 1, undefined, 2 ]
> Array.from({length : 4});
[ undefined, undefined, undefined, undefined ]
>
除此之外,Array.from
可以接受一個回呼函式,在轉換過程可以進行 map 的動作:
> Array.from([1, , 3], elem => elem ? elem * 2 : 0);
[ 2, 0, 6 ]
>
如果要用某個值填滿陣列,那麼 Array.fill
很方便:
> Array(10).fill(0);
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
> [1, 2, 3, 4, 5].fill(0, 2, 4);
[ 1, 2, 0, 0, 5 ]
>
Array.fill
第一個參數接受要填充的值,之後選擇性地接受 start
與 end
,用來指定填充的索引起始與結束。
Array
的 find
方法其實是 findFirst 的概念,可以找到第一個符合條件的數,找不到就傳回 undefined
,findIndex
則是找到第一個符合條件的元素索引位置,找不到傳回 -1:
> [10, 20, -30, 40].find(n => n < 0);
-30
> [10, 20, -30, 40].findIndex(n => n < 0);
2
>
find
和 findIndex
的回呼函式,其實可以接受三個引數,分別是元素值、索引與原陣列參考,上面的範例只使用了元素值的部份。
Set
ES6 以後有了 Set
API,它內部的元素不重複:
> let set = new Set([1, 2, 3, 4, 5, 1, 2, 3]);
undefined
> set;
Set { 1, 2, 3, 4, 5 }
> set.add(6);
Set { 1, 2, 3, 4, 5, 6 }
> set.has(3);
true
> set.forEach(elem => console.log(elem));
1
2
3
4
5
6
undefined
> for(let elem of set) {
... console.log(elem);
... }
1
2
3
4
5
6
undefined
> [...set];
[ 1, 2, 3, 4, 5, 6 ]
> let [a, b, c] = set;
undefined
> a;
1
> b;
2
> c;
3
>
Set
本身是可迭代的,因此可以使用 for/of
,當然,也可以適用 ...
來 Spread 元素,或者解構語法。
Set
是無序的,因此沒有直接可取值的 get 之類的方法,除了上面方法示範之外,Set
還有 delete
可用來刪除元素,clear
可用來清空元素,size
可用來查看 Set
中的元素數量。
那麼問題來了,Set
中判定元素是否重複的依據是什麼?如果是基本型態,顯然就是值是不是相同,這沒有問題,那麼物件呢?
> let o1 = {x: 10};
undefined
> let o2 = {x: 10};
undefined
> let o3 = o2;
undefined
> let set = new Set([o1, o2, o3]);
undefined
> set;
Set { { x: 10 }, { x: 10 } }
>
在上面的例子中,最後的 set
有兩個物件,顯然地,並不是判定物件的特性實際上是否相等,那麼有 equals、hashCode 之類的協定,可以定義物件實質的內含值是否相同嗎?沒有!對於物件,基本上就是相當於 ===
比較。
因為 JavaScript 的物件特性很容易變更,如果你瞭解其他語言中 equals、hashCode 之類的協定,也應該知道,一個狀態可變的物件,在實作 equals、hashCode 之類的協定時會有許多坑,因此就目前來說,Set
是特意這麼做的(並不是忽略了),這會造成一些不便,如果你真的需要能依 equals、hashCode 之類的協定來判定物件相等性,那必須自行實作或者是尋求既有的程式庫。
對於 Set
來說,NaN
是可以判定相等的:
> let set = new Set([NaN, NaN, 0, -0]);
undefined
> set;
Set { NaN, 0 }
>
之後談到 ECMAScript 6 以後相等性判定時,會看到像 Object.is
會將 0 與 -0 視為不相等,然而,對於 0、-0,Set
認定是相等的,具體來說,Set
是採用所謂的 SameValueZero 演算來判定相等性,詳情會在下一篇文件中說明。
在談到 Set
時,通常會一併談到 WeakSet
,簡單來說,垃圾收集時不會考慮物件是否被 WeakSet
包含著,只要物件沒有其他名稱參考著,就會回收物件,如果 WeakSet
中本來有該物件,會自動消失,這可以用來避免必須使用 Set
管理物件,而後忘了從 Set
中清除而發生記憶體洩漏的問題。
WeakSet
中的元素只能是物件,不能是 number
、boolean
、string
、symbol
等,也不能是 null
,由於物件可能被垃圾回收,因此它不能被迭代(也不能使用 forEach
)、不能使用 size
、clear
方法,只能使用 add
、has
、delete
方法。
Map
接著來談談 ES6 以後的 Map
,雖然 JavaScript 中的物件,本身就是鍵與值的集合體,然而,鍵的部份基本上就是字串,ES6 以後多了個 Symbol
可以做為特性,除此之外,就算使用 []
指定物件作為鍵,它會取得字串描述作為特性:
> let o = {x: 10};
undefined
> let map = {
... [o] : 'foo'
... };
undefined
> for(let p in map) {
... console.log(p);
... }
[object Object]
undefined
>
Map
的鍵可以是物件:
> let o = {x: 10};
undefined
> let map = new Map();
undefined
> map.set(o, 'foo');
Map { { x: 10 } => 'foo' }
> map.set({y : 10}, 'foo2');
Map { { x: 10 } => 'foo', { y: 10 } => 'foo2' }
> for(let [key, value] of map) {
... console.log(key, value);
... }
{ x: 10 } 'foo'
{ y: 10 } 'foo2'
undefined
> map.get(o);
'foo'
> map.delete(o);
true
> map;
Map { { y: 10 } => 'foo2' }
>
Map
本身可迭代,使用 for...of
的話,迭代出來的元素會是個包含鍵與值的物件,也可以使用 ...
來解構。除了以上示範的方法之外,可以使用 has
判定是否具有某個鍵,delete
刪除某鍵(與對應的值),使用 clear
清空 Map
,使用 keys
取得全部的鍵,使用 values
取得全部的值,使用 entries
取得鍵值對,使用 size
取得鍵值數量,也可以使用 forEach
等。
建構 Map
時,可以使用陣列,其中必須是 [[鍵, 值], [鍵, 值]]
的形式:
> let map = new Map([['k1', 'v1'], ['k2', 'v2']]);
undefined
> map;
Map { 'k1' => 'v1', 'k2' => 'v2' }
>
Map
中的鍵必須是唯一的,判定的方式是 SameValueZero。
在談到 Map
時,通常會一併談到 WeakMap
,簡單來說,垃圾收集時不會考慮物件是否被 WeakMap
作為鍵,只要物件沒有其他名稱參考著,就會回收物件,如果 WeakMap
中本來有該物件作為鍵,會自動消失,這可以用來避免必須使用 Map
管理物件,而後忘了從 Map
中清除而發生記憶體洩漏的問題。
WeakMap
中的鍵只能是物件,不能是 number
、boolean
、string
、symbol
等,也不能是 null
,由於鍵物件可能被垃圾回收,因此它不能被迭代(也不能使用 forEach
)、不能使用 size
、clear
方法,只能使用 get
、set
、has
、delete
方法。
ArrayBuffer
如果要儲存一組二進位資料,可以使用 ArrayBuffer
,它從 ES6 以後成為標準之一,建構 ArrayBuffer
實例時,必須指定位元組長度,例如:
let buf = new ArrayBuffer(32); // 32 個位元組
console.log(buf.byteLength); // 顯示 32
你沒辦法直接改變 ArrayBuffer
實例的內容,只能透過 byteLength
特性取得長度,slice
方法切割 ArrayBuffer
。
若要改變或取得 ArrayBuffer
內容,方式之一是透過 TypedArray
,有以下幾個子類型:
Int8Array
:8 位有號整數,長度一位元組Uint8Array
:8 位無號整數,長度一位元組Uint8ClampedArray
:8 位無號整數,長度一位元組,超出 [0, 255] 的值,會設定為 0 或 255。Int16Array
:16 位有號整數,長度二位元組。Uint16Array
:16 位無號整數,長度二位元組。Int32Array
:32 位有號整數,長度四位元組。Uint32Array
:32 位無號整數,長度四位元組。Float32Array
:32 位浮點數,長度四位元組。Float64Array
:64 位浮點數,長度八位元組。
不同的 TypedArray
子類型,檢視、操作 ArrayBuffer
的觀點(View)不同,例如同一個 ArrayBuffer
實例,在不同的 TypedArray
觀點下,每個元素的位元長度不同,因而透過 length
取得元素個數也就不同:
let buf = new ArrayBuffer(32);
let i8arr = new Int8Array(buf);
let i16arr = new Int16Array(buf);
let i32arr = new Int32Array(buf);
console.log(i8arr.length); // 顯示 32
console.log(i16arr.length); // 顯示 16
console.log(i32arr.length); // 顯示 8
buf
有 32 個位元組,也就是 256 個位元,就 Int8Array
來說,8 個位元為一個元素,因此會有 32 個元素,就 Int16Array
來說,16 位元為一個元素,因而會有 16 個元素,同樣地,對 Int32Array
來說,會有 8 個元素。
在設定元素時,對每個元素的觀點,也視不同的 TypedArray
子類型而不同,例如,透過 Uint8Array
來寫入與讀出:
let buf = new ArrayBuffer(5);
let ui8arr = new Uint8Array(buf);
ui8arr[0] = 72; // H
ui8arr[1] = 101; // e
ui8arr[2] = 108; // l
ui8arr[3] = 108; // l
ui8arr[4] = 111; // 0
// 顯示 Hello
console.log(String.fromCharCode.apply(null, ui8arr));
在建構 TypedArray
子類型時,也可以使用類陣列物件,例如:
let ui8arr = new Uint8Array([72, 101, 108, 108, 111]);
// 顯示 Hello
console.log(String.fromCharCode.apply(null, ui8arr));
在建構 TypedArray
之後,操作方法和特性,與 Array
幾乎是完全相同的,可參考 TypedArray API 文件。
TypedArray
的子類型,對待每個元素的觀點是一致的,如果資料中包含了不同類型的元素,例如,同時包含了 8 位整數與 16 位整數,可以使用 DataView
來處理:
let buf = new ArrayBuffer(3);
let dataView = new DataView(buf);
dataView.setInt16(0, 72);
dataView.setInt8(2, 105);
console.log(dataView.getInt16(0));
console.log(dataView.getInt8(2));
由於資料長度不固定,在呼叫 setXXX
、getXXX
時,必須指定位元組偏移量。