1. 程式人生 > 實用技巧 >JavaScript ArrayBuffer 二進位制陣列(一)

JavaScript ArrayBuffer 二進位制陣列(一)

一、 ArrayBuffer

ArrayBuffer物件、TypedArray檢視和DataView檢視是 JavaScript 操作二進位制資料的一個介面。這些物件早就存在,屬於獨立的規格(2011 年 2 月釋出),ES6 將它們納入了 ECMAScript 規格,並且增加了新的方法。它們都是以陣列的語法處理二進位制資料,所以統稱為二進位制陣列。

二進位制陣列由三類物件組成。

(1)ArrayBuffer物件:代表記憶體之中的一段二進位制資料,可以通過“檢視”進行操作。“檢視”部署了陣列介面,這意味著,可以用陣列的方法操作記憶體。

(2)TypedArray

檢視:共包括 9 種類型的檢視,比如Uint8Array(無符號 8 位整數)陣列檢視,Int16Array(16 位整數)陣列檢視,Float32Array(32 位浮點數)陣列檢視等等。

(3)DataView檢視:可以自定義複合格式的檢視,比如第一個位元組是 Uint8(無符號 8 位整數)、第二、三個位元組是 Int16(16 位整數)、第四個位元組開始是 Float32(32 位浮點數)等等,此外還可以自定義位元組序。

簡單說,ArrayBuffer物件代表原始的二進位制資料,TypedArray檢視用來讀寫簡單型別的二進位制資料,DataView檢視用來讀寫複雜型別的二進位制資料。

TypedArray檢視支援的資料型別一共有 9 種(DataView檢視支援除Uint8C以外的其他 8 種)。

資料型別位元組長度含義對應的 C 語言型別
Int8 1 8 位帶符號整數 signed char
Uint8 1 8 位不帶符號整數 unsigned char
Uint8C 1 8 位不帶符號整數(自動過濾溢位) unsigned char
Int16 2 16 位帶符號整數 short
Uint16 2 16 位不帶符號整數 unsigned short
Int32 4 32 位帶符號整數 int
Uint32 4 32 位不帶符號的整數 unsigned int
Float32 4 32 位浮點數 float
Float64 8 64 位浮點數 double

注意,二進位制陣列並不是真正的陣列,而是類似陣列的物件。

很多瀏覽器操作的 API,用到了二進位制陣列操作二進位制資料,下面是其中的幾個。

ArrayBuffer 物件概述

ArrayBuffer物件代表儲存二進位制資料的一段記憶體,它不能直接讀寫,只能通過檢視(TypedArray檢視和DataView檢視)來讀寫,檢視的作用是以指定格式解讀二進位制資料。

ArrayBuffer也是一個建構函式,可以分配一段可以存放資料的連續記憶體區域。

const buf = new ArrayBuffer(32);

上面程式碼生成了一段 32 位元組的記憶體區域,每個位元組的值預設都是 0。可以看到,ArrayBuffer建構函式的引數是所需要的記憶體大小(單位位元組)。

為了讀寫這段內容,需要為它指定檢視。DataView檢視的建立,需要提供ArrayBuffer物件例項作為引數。

const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0) // 0

上面程式碼對一段 32 位元組的記憶體,建立DataView檢視,然後以不帶符號的 8 位整數格式,從頭讀取 8 位二進位制資料,結果得到 0,因為原始記憶體的ArrayBuffer物件,預設所有位都是 0。

另一種TypedArray檢視,與DataView檢視的一個區別是,它不是一個建構函式,而是一組建構函式,代表不同的資料格式。

const buffer = new ArrayBuffer(12);

const x1 = new Int32Array(buffer);
x1[0] = 1;
const x2 = new Uint8Array(buffer);
x2[0]  = 2;

x1[0] // 2

上面程式碼對同一段記憶體,分別建立兩種檢視:32 位帶符號整數(Int32Array建構函式)和 8 位不帶符號整數(Uint8Array建構函式)。由於兩個檢視對應的是同一段記憶體,一個檢視修改底層記憶體,會影響到另一個檢視。

TypedArray檢視的建構函式,除了接受ArrayBuffer例項作為引數,還可以接受普通陣列作為引數,直接分配記憶體生成底層的ArrayBuffer例項,並同時完成對這段記憶體的賦值。

const typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3

typedArray[0] = 5;
typedArray // [5, 1, 2]

上面程式碼使用TypedArray檢視的Uint8Array建構函式,新建一個不帶符號的 8 位整數檢視。可以看到,Uint8Array直接使用普通陣列作為引數,對底層記憶體的賦值同時完成。

ArrayBuffer.prototype.byteLength :返回所分配的記憶體區域的位元組長度

ArrayBuffer.prototype.slice() :允許將記憶體區域的一部分,拷貝生成一個新的ArrayBuffer物件。

ArrayBuffer.isView()

ArrayBuffer有一個靜態方法isView,返回一個布林值,表示引數是否為ArrayBuffer的檢視例項。這個方法大致相當於判斷引數,是否為TypedArray例項或DataView例項。

const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false

const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true

二、TypedArray 檢視

ArrayBuffer物件作為記憶體區域,可以存放多種型別的資料。同一段記憶體,不同資料有不同的解讀方式,這就叫做“檢視”(view)。ArrayBuffer有兩種檢視,一種是TypedArray檢視,另一種是DataView檢視。前者的陣列成員都是同一個資料型別,後者的陣列成員可以是不同的資料型別。

目前,TypedArray檢視一共包括 9 種類型,每一種檢視都是一種建構函式。

  • Int8Array:8 位有符號整數,長度 1 個位元組。
  • Uint8Array:8 位無符號整數,長度 1 個位元組。
  • Uint8ClampedArray:8 位無符號整數,長度 1 個位元組,溢位處理不同。
  • Int16Array:16 位有符號整數,長度 2 個位元組。
  • Uint16Array:16 位無符號整數,長度 2 個位元組。
  • Int32Array:32 位有符號整數,長度 4 個位元組。
  • Uint32Array:32 位無符號整數,長度 4 個位元組。
  • Float32Array:32 位浮點數,長度 4 個位元組。
  • Float64Array:64 位浮點數,長度 8 個位元組。

這 9 個建構函式生成的陣列,統稱為TypedArray檢視。它們很像普通陣列,都有length屬性,都能用方括號運算子([])獲取單個元素,所有陣列的方法,在它們上面都能使用。普通陣列與 TypedArray 陣列的差異主要在以下方面。

  • TypedArray 陣列的所有成員,都是同一種類型。
  • TypedArray 陣列的成員是連續的,不會有空位。
  • TypedArray 陣列成員的預設值為 0。比如,new Array(10)返回一個普通陣列,裡面沒有任何成員,只是 10 個空位;new Uint8Array(10)返回一個 TypedArray 陣列,裡面 10 個成員都是 0。
  • TypedArray 陣列只是一層檢視,本身不儲存資料,它的資料都儲存在底層的ArrayBuffer物件之中,要獲取底層物件必須使用buffer屬性。
// 建立一個8位元組的ArrayBuffer
const b = new ArrayBuffer(8);

// 建立一個指向b的Int32檢視,開始於位元組0,直到緩衝區的末尾
const v1 = new Int32Array(b);

// 建立一個指向b的Uint8檢視,開始於位元組2,直到緩衝區的末尾
const v2 = new Uint8Array(b, 2);

// 建立一個指向b的Int16檢視,開始於位元組2,長度為2
const v3 = new Int16Array(b, 2, 2);
  • TypedArray.prototype.copyWithin(target, start[, end = this.length])
  • TypedArray.prototype.entries()
  • TypedArray.prototype.every(callbackfn, thisArg?)
  • TypedArray.prototype.fill(value, start=0, end=this.length)
  • TypedArray.prototype.filter(callbackfn, thisArg?)
  • TypedArray.prototype.find(predicate, thisArg?)
  • TypedArray.prototype.findIndex(predicate, thisArg?)
  • TypedArray.prototype.forEach(callbackfn, thisArg?)
  • TypedArray.prototype.indexOf(searchElement, fromIndex=0)
  • TypedArray.prototype.join(separator)
  • TypedArray.prototype.keys()
  • TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
  • TypedArray.prototype.map(callbackfn, thisArg?)
  • TypedArray.prototype.reduce(callbackfn, initialValue?)
  • TypedArray.prototype.reduceRight(callbackfn, initialValue?)
  • TypedArray.prototype.reverse()
  • TypedArray.prototype.slice(start=0, end=this.length)
  • TypedArray.prototype.some(callbackfn, thisArg?)
  • TypedArray.prototype.sort(comparefn)
  • TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
  • TypedArray.prototype.toString()
  • TypedArray.prototype.values()

BYTES_PER_ELEMENT 屬性

每一種檢視的建構函式,都有一個BYTES_PER_ELEMENT屬性,表示這種資料型別佔據的位元組數。

Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Uint8ClampedArray.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8

三、DataView 檢視

如果一段資料包括多種型別(比如伺服器傳來的 HTTP 資料),這時除了建立ArrayBuffer物件的複合檢視以外,還可以通過DataView檢視進行操作。

DataView檢視提供更多操作選項,而且支援設定位元組序。本來,在設計目的上,ArrayBuffer物件的各種TypedArray檢視,是用來向網絡卡、音效卡之類的本機裝置傳送資料,所以使用本機的位元組序就可以了;而DataView檢視的設計目的,是用來處理網路裝置傳來的資料,所以大端位元組序或小端位元組序是可以自行設定的。

DataView檢視本身也是建構函式,接受一個ArrayBuffer物件作為引數,生成檢視。

new DataView(ArrayBuffer buffer [, 位元組起始位置 [, 長度]]);

下面是一個例子。

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

DataView例項有以下屬性,含義與TypedArray例項的同名方法相同。

  • DataView.prototype.buffer:返回對應的 ArrayBuffer 物件
  • DataView.prototype.byteLength:返回佔據的記憶體位元組長度
  • DataView.prototype.byteOffset:返回當前檢視從對應的 ArrayBuffer 物件的哪個位元組開始

DataView例項提供 8 個方法讀取記憶體。

  • getInt8:讀取 1 個位元組,返回一個 8 位整數。
  • getUint8:讀取 1 個位元組,返回一個無符號的 8 位整數。
  • getInt16:讀取 2 個位元組,返回一個 16 位整數。
  • getUint16:讀取 2 個位元組,返回一個無符號的 16 位整數。
  • getInt32:讀取 4 個位元組,返回一個 32 位整數。
  • getUint32:讀取 4 個位元組,返回一個無符號的 32 位整數。
  • getFloat32:讀取 4 個位元組,返回一個 32 位浮點數。
  • getFloat64:讀取 8 個位元組,返回一個 64 位浮點數。

這一系列get方法的引數都是一個位元組序號(不能是負數,否則會報錯),表示從哪個位元組開始讀取。

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

// 從第1個位元組讀取一個8位無符號整數
const v1 = dv.getUint8(0);

// 從第2個位元組讀取一個16位無符號整數
const v2 = dv.getUint16(1);

// 從第4個位元組讀取一個16位無符號整數
const v3 = dv.getUint16(3);

上面程式碼讀取了ArrayBuffer物件的前 5 個位元組,其中有一個 8 位整數和兩個十六位整數。

如果一次讀取兩個或兩個以上位元組,就必須明確資料的儲存方式,到底是小端位元組序還是大端位元組序。預設情況下,DataViewget方法使用大端位元組序解讀資料,如果需要使用小端位元組序解讀,必須在get方法的第二個引數指定true

// 小端位元組序
const v1 = dv.getUint16(1, true);

// 大端位元組序
const v2 = dv.getUint16(3, false);

// 大端位元組序
const v3 = dv.getUint16(3);

DataView 檢視提供 8 個方法寫入記憶體。

  • setInt8:寫入 1 個位元組的 8 位整數。
  • setUint8:寫入 1 個位元組的 8 位無符號整數。
  • setInt16:寫入 2 個位元組的 16 位整數。
  • setUint16:寫入 2 個位元組的 16 位無符號整數。
  • setInt32:寫入 4 個位元組的 32 位整數。
  • setUint32:寫入 4 個位元組的 32 位無符號整數。
  • setFloat32:寫入 4 個位元組的 32 位浮點數。
  • setFloat64:寫入 8 個位元組的 64 位浮點數。

這一系列set方法,接受兩個引數,第一個引數是位元組序號,表示從哪個位元組開始寫入,第二個引數為寫入的資料。對於那些寫入兩個或兩個以上位元組的方法,需要指定第三個引數,false或者undefined表示使用大端位元組序寫入,true表示使用小端位元組序寫入。

// 在第1個位元組,以大端位元組序寫入值為25的32位整數
dv.setInt32(0, 25, false);

// 在第5個位元組,以大端位元組序寫入值為25的32位整數
dv.setInt32(4, 25);

// 在第9個位元組,以小端位元組序寫入值為2.5的32位浮點數
dv.setFloat32(8, 2.5, true);

如果不確定正在使用的計算機的位元組序,可以採用下面的判斷方式。

const littleEndian = (function() {
  const buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true);
  return new Int16Array(buffer)[0] === 256;
})();

如果返回true,就是小端位元組序;如果返回false,就是大端位元組序。

更多詳情參考:https://wangdoc.com/es6/arraybuffer.html

JS DataURL 整理(二) DataURL 和圖片

JS DataURL 整理(一)

JavaScript 與 ECMAScript 的關係