[JS高程] Typed Array 定型數組

0. 前言

​ 關於Typed Array, MDN 上有一段內容值得先參看閱讀一下,有一個基本的認識。

0.1 什麼是定型數組 (typed arrays) ?

​ 什麼是定型數組? 用一句話概括即:定型數組,是一種對內存緩衝區中的原生二進制數據有着讀寫機制(能力)的一種類數組對象。 (後面會介紹,實際是一種ArrayBuffer 視圖)

JavaScript typed arrays are array-like objects that provide a mechanism for reading and writing raw binary data in memory buffers.

​ 我們之前常用的普通數組,其長度是能夠動態的變動的,且其元素類型不受限制。 JavaScript 憑着強大的引擎所以這些數組操作能夠被快速處理。
​ 但是,現代 web 應用能力增強,也不斷的增加了各種特性以滿足需求。例如音視頻, 使用WebSockets訪問原始數據等等。 以及WebGL的日益強大,要求JavaScript需要更強的數據處理能力。而現有的普通數據不便於處理大量的二進制數據,中間有大量的性能損耗。
​ 因此, 我們需要JavaScript 代碼能夠快速且簡單的操作原始二進制數據, 這就是 定型數組(typed arrays)誕生的契機。
一個定型數組中的每一個元素都是一種原始二進制值, 這些值可以自 8-bit 整型 到 64-bit 浮點數。

值得注意的是, 不要將普通數組和定型數組混淆, 儘管我們後面會介紹它的很多方法,普通數組似乎都是支持的。 但是學習時,最好將它完全獨立起來。 如果你嘗試使用Array 構造函數提供的Array.isArray() 靜態方法去判斷一個定型數組, 將會返回 false

1. Buffers 和 Views

​ 爲了讓定型數組最大程度的靈活和高效, JavaScript 定型數組將其實現分成了兩個部分: buffers 和 views.
一個buffer 就是一個代表了一個數據塊的對象(chunk of data),它由ArrayBuffer 對象實現;它沒有所謂的格式,也沒有提供訪問其內容的機制
​ 所以你需要使用 view (通常都譯作”圖“/"視圖") 訪問 一個buffer 中的內存(the memory contained in a buffer)。 一個 視圖 提供了一個上下文,即數據類型(data type), 起始點偏移(starting offest), 以及元素的數量。

1.1 ArrayBuffer

ArrayBuffer 通常是一種用於表示通用,定長的二進制數據緩存的數據類型 ,下面這張圖表示了常見的ArrayBuffer:

Typed arrays in an ArrayBuffer

在內存中分配一個16個字節大小的ArrayBuffer, 可以用不同bit長度的元素填充。

  • Uint8Array: uint8array 類型數組代表一個8位無符號整數數組。 (U 即 unsigned)
  • Uint16Array: 16位無符號整數數組;
  • Uint32Array: 32位無符號整數數組;
  • Float64Array: 64 位浮點數組;

有無符號:區別在於值的表示範圍不同,例如Int8Array的 取值範圍是:-128 ~ 127, 但是Uint8Array 的取值範圍是 :0 ~ 255 , 實際範圍大小是一樣的, 只是取值不同。

取值範圍的計算:如UInt16Array 即元素長度爲16個bit位,所能表示的最大值即16個bit 全置1, 二進制計算結果就是 十進制的 65535 即2^16 - 1 , 最小值即全置0, 十進制換算也是0, 所以無符號16bit所表示的值範圍就是0~65535。 而Int16Array 是帶符號的, 因此最bit位爲符號位,表示正負, 剩下15位用於表示數值, 所以最大值即15位全置1, 即 32767, 至於最小值則是高位置負,其餘位全1,即 -32727, 但是要 減1, 所以有符號16bit 表示的值範圍就是 -32728 ~ 32727。至於爲什麼這裏不做探究,其他的更多位都是同理。

再如 Uint32Array 即32個bit位,由於無符號,其所能表示的二進制最大值即32 個 1, 4,294,967,295,所以表示範圍爲 0 ~ 4,294,967,295。 更多的值範圍可參看這裏 link

1.1.1 創建ArrayBuffer實例

const buf = new ArrayBuffer(16); // 在內存中分配16字節

1.1.2 讀取ArrayBuffer的 字節長度

alert(buf.byteLength); // 16

⚠️注意:

  • ArrayBuffer 一經創建,就不能再去動態調整大小
  • 要讀取或者寫入ArrayBuffer, 就必須通過視圖, 視圖有不同的類型,但引用的都是ArrayBuffer中存儲的二進制數據。

1.2 DataView

​ 這是第一種允許你讀寫ArrayBuffer 的視圖類型 —— DataView。
​ 該視圖專爲文件I/O 和 網絡 I/O 設計, 其 API 支持對緩衝數據的高度控制,但是相比於其他類型的視圖性能也差一些。 DataView 對緩衝內容沒有任何預設,也不能迭代。

​ 必須在對已有的ArrayBuffer 讀取或者寫入時才能創建DataView 實例。 這個實例可以使用全部或者部分ArrayBuffer, 且維護着對該緩衝實例的引用,以及視圖在緩衝中開始的位置。

1.2.1 創建DataView 實例

​ DataView 的構造函數接收三個參數, 分別是: 目標ArrayBuffer 實例、 可選的字節偏移(byteOffset)、 可選的字節長度(byteLength)

const buf = new ArrayBuffer(16);
const firstHalfDataView = new DataView(buf, 0, 8);
alert(firstHalfDateView.byteLength); // 8

訪問DataView實例屬性

  • byteOffset : 訪問視圖的緩衝起點,上例中firstHalfDataView.byteOffset 返回 0 ;
  • byteLength : 訪問視圖的字節長度,上例中firstHaldDataView.byteLength 返回 8;
  • buffer訪問視圖所訪問的目標buffer實例, 上例執行firstHaldDataView.buffer === buf 將會返回 true

如果僅提供一個可選參數,將會被視作字節偏移,即緩衝起點,然後使用全部剩餘Arraybuffer

const buf = new ArrayBuffer(16);
const secondHalfDataView = new DataView(buf, 8); // 將從第9個字節開始 到 剩餘全部字節
alert(secondHalfDataView.byteLength); // 8

如果不提供可選參數,將默認使用整個 ArrayBuffer

const buf = new ArrayBuffer(16);
const fullDataView = new DataView(buf);
alert(fullDataView.byteLength); //16

1.2.2 通過DataView 訪問 buffer

要通過DataView 讀取緩衝,還需要幾個組件:

  1. 首先是要讀或者寫的字節偏移量。 可以看成DataView 中的某種 “地址”
  2. DataView 應該使用ElementType 來實現 JavaScript 的 Number 類型到緩衝內二進制格式的轉換。
  3. 最後是內存中值的字節序。 默認爲大端字節序。
1.2.2.1 ElementType

DataView 對存儲在緩衝內的數據類型沒有預設。 它暴露的API 強制開發者在讀寫時指定一個 ElementTypt。 ES6 支持 8 種不同的 ElementType

type Int8 Uint8 Int16 Uint16 Int32 Uint32 Float32 Float64
byte 1 1 2 2 4 4 4 8

DateView 爲上表中的每種類型都暴露了getset 方法, 這些方法使用byteOffset (字節偏移量)定位要讀取或者寫入值的位置。 類型都是可以互換使用的。

// 在內存中分配兩個字節並聲明一個DataView
const buf = new ArrayBuffer(2);
const view = new DataView(buf);

// 說明整個緩衝確實所有二進制位都是0
// 檢查第一個和第二個字符
alert(view.getInt8(0));// 0
alert(view.getInt8(1));// 0
// 檢查整個緩衝
alert(view.getInt16(0)); // 0

// 將整個緩衝都設置爲1
// 255 的二進制表示是 0xFF
view.setUint8(1,0xFF);

//現在,緩衝裏都是1了
// 如果把它當成二補數的有符號整數,則應該是 -1
alert(view.getInt16(0)); // -1
1.2.2.2 字節序

0x1234567的大端字節序和小端字節序的寫法如下圖。

img

01 是高位(最高有效位), 67 是低位。

關於字節序,可以看 阮一峯的文章 《理解字節序》

計算機電路先處理低位字節,效率比較高,因爲計算都是從低位開始的。所以,計算機的內部處理都是小端字節序。

但是,人類還是習慣讀寫大端字節序。所以,除了計算機的內部處理,其他的場合幾乎都是大端字節序,比如網絡傳輸和文件儲存。

DataView 的所有API 方法都以大端字節序作爲默認值,但接收一個可選的布爾值參數,設置爲true 即可啓用小端字節序。 (略)

基本上用不着,等用的着的時候再看下,不用深究。

1.2.2.3 邊界情形

DataView 完成讀寫操作的前提必須有充足的緩衝區, 否則就會拋出 RangeError :

const buf = new ArrayBuffer(6);
consr view = new DataView(buf);

// 嘗試讀取部分超出緩衝範圍的值
view.getInt32(4);
// 嘗試讀取超出緩衝範圍的值
view.getInt32(8);
// 嘗試寫入超出緩衝範圍的值
view.setInt32(4,123);

1.3 定型數組

定型數組是另一種形式的ArrayBuffer視圖。 概念上於DataView 接近,但是定型數組的區別在於,它特定於一種ElementType且遵循系統原生的字節序。 相應地,定型數組提供了適用面更廣的API 和 更高的性能。

​ 設計定型數組的目的,就是提高與 WebGL 等原生庫交換二進制數據的效率。 由於定型數組的二進制表示對操作系統而言是一種容易使用的格式,JavaScript 引擎可以重度優化算術運算、按位運算和其他對定型數組的常見操作,因此使用它們速度極快。

​ 創建定型數組的方式包括讀取已有的緩衝、使用自由緩衝、填充可迭代結構,以及填充基於任意類型的定型數組。 另外,通過<ElementType>.from()<ElementType.of()也可以創建定型數組:

1.3.1 定型數組創建

創建一個 12 字節的緩衝

const buf = new ArrayBuffer(12);
// 創建一個引用該緩衝的 Int32Array
const ints = new Int32Array(buf);

// 這個定型數組知道自己的每個元素需要4個字節 // 32bit / 8bit = 4byte
// 因此長度爲 3 // new Int32Array()指的是每個元素位爲32bit,也就是4 字節, 一個12字節的緩衝也就佔了3個元素位,即長度爲3
alert(ints.length); // 3

創建一個長度爲6的Int32Array

const ints2 = new Int32Array(6);
// 每個數值使用4字節, 因此ArrayBuffer 是24字節 , Intl
alert(ints2.length); // 6

// 類似 DataView, 定型數組也有一個指向關鍵緩衝的引用
alert(ints2.buffer.byteLength); //24

創建一個包含[2,3,6,8]的Int32Array

const ints3 = new Int32Array([2,3,6,8]);
alert(ints.length);		//4
alert(ints3.buffer.byteLength);	//16  //每個元素位32bit,4個元素 就是16字節
alert(ints3[2]);	//6

通過複製ints3的值創建一個Int16Array

const ints4 = new Int16Array(ints3);

// 這個新類型數組會分配自己的緩衝
// 對應索引的每個值會相應地轉換爲新格式
alert(ints4.length);	//4
alert(ints4.buffer.byteLength);	//8 每一個元素位是16bit,即2字節(byte), 四個元素即8字節
alert(ints5[2]);	//6

基於普通數組類創建一個Int16Array

const ints5 = Int16Array.from([3, 5, 7, 9]);
alert(ints5.length);	// 4
alert(ints5.buffer.byteLength); //8
alert(ints5[2]);	//7

基於傳入的參數創建一個Float32Array

const floats = Float32Array.of(3.14, 2.718, 1.618);
alert(floats.length);		//3
alert(floats.buffer.byteLength); //12
alert(floats[2]);	//1.6180000305175781

1.3.2 BYTES_PER_ELEMENT屬性

定型數組的構造函數和實例都有一個 BYTES_PER_ELEMENT屬性, 返回該類型數組中的每個元素大小:

alert(Int16Array.BYTES_PER_ELEMENT);//2
alert(Int32Array.BYTES_PER_ELEMENT);//4
const ints = new Int32Array(1);
const float = new Float64Array(2);
alert(ints.BYTES_PER_ELEMENT);//4
alert(floats.BYTES_PER_ELEMENT);//8

如果定型數組沒有用任何值初始化,則其關聯的緩衝會以0填充:

const ints = new Int32Array(4);
alert(init[0]); //0
alert(init[1]); //0
alert(init[2]); //0
alert(init[3]); //0

1.3.3 定型數組行爲

從很多方面看,定型數組與普通數組都很相似。 定型數組支持如下操作符、方法和屬性:

  • []
  • copyWithin()
  • entries()
  • every()
  • fill()
  • filter()
  • find()
  • findIndex()
  • forEach()
  • indexOf()
  • join()
  • keys()
  • lastIndexOf()
  • length
  • map()
  • reduce()
  • reduceRight()
  • reverse()
  • slice()
  • some()
  • sort()
  • toLocalString()
  • toString()
  • values()

⚠️ 其中,返回新數組的方法也會返回包含同樣元素類型(element type)的新定型數組:

const ints = new Int16Array([1,2,3]);
consr doubleints = ints.map(x=> 2*x);
alert(doubleints instanceof Int16Array); // true

1.3.4 定型數組的迭代

定型數組有一個Symbol.iterator 符號屬性,因此可以通過 for...of 循環和擴展操作符來操作:

const ints = new Int16Array([1,2,3]);
for (const int of ints){
    alert(int);
}
// 1
// 2
// 3
alert(Math.max(...ints)) ; //3

1.3.5 普通數組 合併|複製|修改 方法 不適用於定型數組

定型數組同樣使用數組緩衝來存儲數據, 而數據緩衝無法調整大小。 因此,下列方法不適用於定型數組:

  • concat()
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

不過定型數組也提供了兩個新的方法, 可以快速的向內或者向外複製數據:

1.3.6 定型數組方法

1.3.6.1 定型數組的複製方法 set()subarray()
  • set()
  • subarray()

set() 從提供的數組或者定型數組中把值複製到當前定型數組中指定的索引位置:

// 創建長度爲 8的int16 數組
const container = new Int16Array(8);
// 把定型數組賦值爲前4個值
// 偏移量默認爲索引0 
container.set(Int8Array.of(1,2,3,4));
console.log(container);// [1,2,3,4,0,0,0,0]

// 把普通數組賦值爲後四個值
// 偏移量4 表示從索引4 開始插入
container.set([5,6,7,8], 4);
console.log(container); // [1,2,3,4,5,6,7,8]

// 溢出會拋出錯誤
container.set([5,6,7,8], 7);
// RangeError

subarray() 執行與set() 相反的操作, 它會基於從原始定型數組中複製的值返回一個新定型數組。

複製值時的開始索引和結束索引是可選的:

const source = Int16Array.of(2,4,6,8);

// 把整個數組賦值爲一個同類型的新數組
const fullCopy = source.subarray();
console.log(fullCopy); //[2,4,6,8]

// 從索引2 開始複製數組 (包含索引2位置的值)
const halfCopy = souce.subarray(2);
console.log(halfCopy); // [6,8]

// 從索引1 開始複製到索引 3 (包含開始索引, 不包含結束索引)
const partialCopy = source.subarray(1, 3);
console.log(partialCopy); // [4,6]
1.3.6.2 定型數組拼接能力

定型數組沒有原生的拼接能力,但是使用定型數組API提供的很多工具可以手動構建 :

// 第一個參數是應該返回的數組類型
// 其餘參數是應該拼接在一起的定型數組

function typedArrayConcat(typedArraysConstructor, ...typedArrays) {
    // 計算所有數組中包含的元素總數
    const numElements = typedArrays.reduce((x,y) => (x.length || x) + y.length);
    
    // 按照提供的類型創建一個數組, 爲所有元素留出空間
    const resultArray = new typedArrayConstructor(numElements);
    
    // 依次轉移數組
    let currentOffset = 0;
    typedAArrays.map(x => {
        resultArray.set(x, currentOffset);
        currentOffset += x.length;
    });
    
    return resultArray;
}

const concatArray = typedArrayConcat (IntArray,
                                      Int8Array.of(1,2,3),
                                      Int16Array.of(4,5,6),
                                      Float32Array.of(7,8,9));
console.log(concatArray); //[1,2,3,4,5,6,7,8,9]
console.log(concatArray instanceof Int32Array); // true

1.3.7 下溢和上溢

定型數組中,值的下溢或者上溢不會影響到其他索引,但是仍然需要考慮數組的元素應該是什麼類型。

定型數組對於可以存儲的每個索引只接收一個相關位,而不用考慮它們對實際數值的影響。 以下代碼演示瞭如何處理下溢和上溢:

// 長度爲2 的有符號整數數組
// 每個索引保存一個二補數形式的有符號整數(範圍是 -1 * 2^7 ~ (2^7 - 1)
const ints = new Int8Array(2);

// 長度爲2 的無符號整數數組
// 每個索引保存一個無符號整數 (0~255) // (2^7 - 1)
const unsignedInts = new Uint8Array(2);

// 上溢的位不會影響相鄰索引
// 索引只取最低有效位上的 8 位
unsignedInts[1] = 256; //0x100
console.log(unsignedInts);//[0, 0]
unsignedInts[1] = 511; // 0x1FF
console.log(unsignedInts);// [0,255]

// 下溢的位會被轉換爲其無符號的等價值 
// 0xFF 是以二補數形式表示的-1(截取到 8 位), 
// 但 255 是一個無符號整數 
unsignedInts[1] = -1        // 0xFF (truncated to 8 bits) 
console.log(unsignedInts);  // [0, 255] 
 
// 上溢自動變成二補數形式 
// 0x80 是無符號整數的 128,是二補數形式的-128 
ints[1] = 128;        // 0x80 
console.log(ints);    // [0, -128] 
 
// 下溢自動變成二補數形式 
// 0xFF 是無符號整數的 255,是二補數形式的-1 
ints[1] = 255;        // 0xFF 
console.log(ints);    // [0, -1]

除了 8 種元素類型, 還有一種 “夾板” 數組類型 : Uint8ClampedArray, 不允許任何方向溢出。 超出最大值255的值會被向下舍入爲255, 而小於最小值的 0 會被向上舍入爲0。

const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256]);
console.log(clampedInts); // [0, 0, 255, 255]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章