JavaScript 讀寫二進位制資料

lucefer發表於2018-09-08

型別化陣列的出現

型別化陣列是 HTML5 中引入的API,它能夠讓開發者使用 JavaScript 直接操作二進位制資料。在型別化陣列出現之前,我們是無法直接通過 JavaScript 操作二進位制資料,通常都是操作 JavaScript 中的資料型別,由執行時轉化成二進位制。這就多了一個轉化的過程,儘管 JavaScript 對資料型別做了很多優化以提高效率,但相比直接操作二進位制來說,仍然有效率上的差異。於是型別化陣列就順勢推出了。

用途

那麼,型別化陣列的應用場景都有哪些呢?

  • canvas 影像處理。
  • WebGL 與顯示卡通訊。
  • 檔案操作
  • Ajax響應

如何使用

那麼,既然型別化陣列這麼重要,那還等什麼,趕緊來掌握它們吧。

既然我們要直接操作二進位制資料,二進位制資料又是存放在一段連續的記憶體區域中,所以我們首先要有這麼一段記憶體區域。

我們可以建立一個記憶體區域:

let buffer = new ArrayBuffer()
複製程式碼

ArrayBuffer 是一個建構函式,允許我們例項化陣列緩衝區,陣列緩衝區可以理解為是一段連續的記憶體區域。 由於我們建構函式傳入的引數是空,所以生成的 buffer 指向的記憶體長度是 0 位元組,沒有意義。

嗯,那我們就建立一個有意義的記憶體區域。

buffer = new ArrayBuffer(8)
複製程式碼

我們給ArrayBuffer 傳入引數 8,意思是讓瀏覽器幫我們建立一段 8 個位元組長度的記憶體區域。

我們看下這段記憶體區域的長度是否是 8 個位元組

console.log(buffer.byteLength);
複製程式碼

輸出是 8, 看來瀏覽器沒有欺騙我們。

JavaScript 讀寫二進位制資料

我們猜想這 8 個位元組裡面的值應該都是 0 ,因為我們並沒有給 buffer 賦值。讓我們確認一下吧,先看第一個位元組:

console.log(buffer[0])
複製程式碼

輸出: undefined。

咦?怎麼是 undefined 呢?

哦,原來 buffer[0] 的意思是檢視 buffer 這個物件 的屬性為 0 的值,因為 buffer 沒有 0 這個屬性,所以是 undefined。

好吧,看來我們檢視 buffer 內容的姿勢不對。

那該如何檢視 buffer 內容呢?

陣列檢視

璫璫璫璫,八大金剛閃亮登場~

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

這八大金剛有什麼神通呢?我們無法直接讀寫 buffer 資料,而這八種資料型別充當了讀寫 buffer 內容的橋樑。

JavaScript 讀寫二進位制資料

還是上面的 buffer,我們想檢視一下 buffer 內容。首先建立一個讀寫該 buffer 的橋樑:

let int8Array = new Int8Array(buffer);
複製程式碼

我們建立了一個讀寫 buffer 的橋樑,用 8 位有符號整數來讀寫 buffer。那現在我們看看 buffer 第一位的內容是什麼吧?

console.log(int8Array[0]);
複製程式碼

輸出:0,看來初始化的時候,buffer 的各個位元組儲存的值預設都是 0 了。

JavaScript 讀寫二進位制資料

我們看看如何使用 Int8Array 給 buffer 賦值:

int8Array[0] = 30;
int8Array[1] = 41;

int8Array[2] = 52;
int8Array[3] = 63;

int8Array[4] = 74;
int8Array[5] = 85;

int8Array[6] = 86;
int8Array[7] = 97;
複製程式碼

很簡單,因為 Int8Array 是一個位元組的長度,和 buffer 的單位一致,所以我們可以通過索引的形式對 buffer 指定位置進行賦值操作。

JavaScript 讀寫二進位制資料

很簡單吧?就是這麼簡單。

另外七大金剛和 Int8Array 的用法一樣,但是有所不同,我們看下他們之間的區別。

這次我們使用 Int16Array,仍然是剛才的 buffer,我們建立一個新的橋樑,這座橋樑仍然通往 buffer 。

let int16Array = new Int16Array(buffer);
複製程式碼

大家試想一下 int16Array[0] 是什麼內容? 我們輸出一下:

console.log(int16Array[0])
複製程式碼

咦,結果怎麼是 10526?

不太理解了。好吧,我們分析下 10526 怎麼得來的。

我們看下 buffer 中的二進位制資料。

由於 Int16Array 佔兩個位元組,所以我們在用它讀寫資料的時候,一個索引所代表的資料等於 buffer 中兩個位元組。

JavaScript 讀寫二進位制資料

我們可以看到 int16Array[0] 裡面的二進位制資料是由30的二進位制和41的二進位制資料拼接而成:00011110(30) 00101001(41)。

我們按照 30、41的順序計算一下二進位制對應的十進位制數。

parseInt(1111000101001, 2) //輸出 7721
複製程式碼

算出來的值是 7721,這和我們輸出的不一致呀?

這就涉及到位元組順序的概念。在我們的個人筆記本上一般都是小端位元組序。小端位元組序體現在我們這個示例中即是 41、30的二進位制順序,我們剛才的計算順序有問題,那按照 41、30 的二進位制順序計算一下

parseInt(10100100011110, 2) //輸出 10526
複製程式碼

可以看到輸出結果是 10526,和我們直接使用 int16Array[0] 得出的結果一致。

上面這個例子,告訴我們在換資料結構解析 buffer 的時候,資料會變得不容易理解,我們一定要謹慎處理。

屬性和方法

型別化陣列例項化的物件包含一些很有用的屬性和方法:

length

length屬性返回型別化陣列的資料成員個數。

byteLength

返回型別化陣列的位元組長度。注意與length的區別。通長 byteLength = length * 每個資料佔用位元組數

byteOffset

返回該型別化陣列的資料從所處 buffer 中的哪個位元組開始。

buffer

型別化陣列對應的 buffer。

set

複製陣列,將某段記憶體中的資料完整地複製到另一段記憶體。

let a = new Uint8Array(12);
a[0] = 31;
a[1] = 32;
let b = new Uint8Array(12);
b.set(a);
複製程式碼

上面這段程式碼的意思是將 a 這段buffer中的內容,完整地拷貝到 b 這段 buffer 中,這種方式比按索引賦值要快速地多。

當然,set 支援從某個索引開始複製資料


let a = new Uint8Array(12);
a[0] = 31;
a[1] = 32;
let b = new Uint8Array(10);
b.set(a, 2);
複製程式碼

上面這段程式碼意思是從b的第三個索引位置開始複製 a 中的資料。

subarray

subarray的意思是對一個型別化陣列,取其子陣列的內容,返回一個新的型別化陣列。

let a = new Uint8Array(8);
a[2] = 1;
let b = a.subarray(2,3);
console.log(b.length);
console.log(b.byteLength);
複製程式碼

subarray 的第一個引數,代表從源陣列的第幾個索引開始擷取,第二個引數代表擷取到第幾個索引。

混合檢視

有一點需要注意,我們的型別陣列初始化的時候,可以指定 buffer的某一段,這就意味著,我們可以對一段 buffer 記憶體區域指定多個型別數,我們稱之為 混合檢視

let buffer = new Buffer(8);
let idArray = new Int8Array(buffer, 0,2);
let nameArray = new Int8Array(buffer, 2, 4);
let ageArray = new Int8Array(buffer, 6, 2);
複製程式碼

我們用一段記憶體區域表示一個人的 id、name、age。這種結構類似於 C 語言中的 struct 。

JavaScript 讀寫二進位制資料

我們將一段 8 個位元組的記憶體分成三個部分:

  • 位元組 0 ~ 位元組 1 代表 id。
  • 位元組 2 ~ 位元組 5 代表 username。
  • 位元組 6 ~ 位元組 7 代表 age。

DataView

JavaScript 還引入了另外種檢視DataView,也能達到操作 buffer 的目的,但相比之下,DataView 操作粒度更細一些,而且還能夠設定位元組序為大端還是小端。

DataView 的建構函式:

DataView(ArrayBuffer物件 buffer, 從 buffer 的第幾個位元組開始讀取, 讀取的長度);
複製程式碼

舉個例子來說:

let buffer = new ArrayBuffer(10);
let view = new DataView(buffer);

複製程式碼

如何讀取?

我們建立好了檢視 view, 那該如何讀取呢?

  • getInt8(index, order):從第 index 個位元組讀取一個 8 位整數。
  • getUint8(index, order):從第 index 個位元組開始讀取一個無符號的 8 位整數。
  • getInt16(index, order):從第 index 個位元組開始讀取 2 個位元組,返回一個 16 位整數。
  • getUint16(index, order):從第 index 個位元組開始讀取 2 個位元組,返回一個無符號的 16 位整數。
  • getInt32(index, order):從第 index 個位元組開始讀取 4 個位元組,返回一個32位的整數。
  • getUint32(index, order):從第 index 個位元組開始讀取 4 個位元組,返回一個無符號的 32 位整數。
  • getFloat32(index, order):從第 index 個位元組開始讀取 4 個位元組,返回一個 32 位 浮點數。
  • getFloat64(index, order):從第 index 個位元組開始讀取 8 個位元組,返回一個 64 位的浮點數。

JavaScript 提供了 8 種讀取方式,功能很簡單,也很容易理解,這裡就不一一做示例了,大家可以自己試一下。

剛剛我們也說了,DataView 也支援設定位元組序,在上面 8 中讀取方式中,第一個位元組是索引,第二個位元組允許我們設定位元組序,true 代表 小端位元組序讀取,false 代表大端位元組序讀取,預設為 false。

如何寫入?

DataView 不僅能支援細粒度的讀取操作,也支援細粒度的寫入操作:

  • setInt8(index, value, order):從第 index 個位元組開始,寫入 1 個位元組的值為 value 的 8 位整數。
  • setUint8(index, value, order):從第 index 個位元組開始,寫入 1 個位元組的值為 value 的無符號 8 位整數。
  • setInt16(index, value, order):從第 index 個位元組開始,寫入 2 個位元組的值為 value 的 16 位整數。
  • setUint16(index, value, order):從第 index 個位元組開始,寫入 2 個位元組的值為 value 的無符號的 16 位整數。
  • setInt32(index, value, order):從第 index 個位元組開始,寫入 4 個位元組的值為 value 的 32 位整數。
  • setUint32(index, value, order):從第 index 個位元組開始,寫入 4 個位元組的值為 value 的無符號 32 位整數。
  • setFloat32(index, value, order):從第 index 個位元組開始,寫入 4 個位元組的值為 value 的 32 位浮點數。
  • setFloat64(index, value, order):從第 index 個位元組開始,寫入 8 個位元組的值為 value 的 64 位浮點數。

order 的意思仍然是設定寫入時的位元組序, true 為小端位元組序,false 為大端位元組序,預設為 false。

用法也很簡單,大家可以練習一下。

至此,關於 JavaScript 操作二進位制的方式就介紹完了,大家以後碰到需要直接操作記憶體的場景時,不妨用用這兩種方式。

未來的你會感謝今天努力的你。

相關文章