JS 中的二進制數據處理

圖片

作者:大勾

部門:業務技術 / 前端

前言

  在現有的計算機中,二進制常常以字節數組的形式存在於程序當中。例如在 C# 裏面,就用 byte[],標準 C 裏面沒有 byte 類型,但可以通過 typedef 把 byte 定義爲 unsigned char 的別名,效果是一樣的。JS 設計之初似乎就沒想過要處理二進制,對於字節的概念可以說是非常非常的模糊。如果要表達字節數組,那麼似乎只能用一個普通數組來表示。

  然而隨着業務需求的逐漸發展,出現了 WebGL 這樣的技術。所謂 WebGL,就是指瀏覽器與顯卡之間的通信接口。爲了滿足 JavaScript 與顯卡之間大量的、實時的數據交換,它們之間的數據通信必須是二進制的,而不能是傳統的文本格式。類型化數組(Typed Array)就是在這種背景下誕生的。而類型化數組是建立在 ArrayBuffer 對象的基礎上的。下面介紹一下 Arraybuffer。

一、Arraybuffer

================

1.1 基本概念

  ArrayBuffer 對象是 ES6 才納入正式 ECMAScript 規範,是 JavaScript 操作二進制數據的一個接口。ArrayBuffer 對象是以數組的語法處理二進制數據,也稱二進制數組。它不能直接讀寫,只能通過視圖(TypedArray 視圖和 DataView 視圖)來讀寫。

 ❝ArrayBuffer 簡單說是一片內存,但是你不能直接用它。這就好比你在 C 裏面,malloc 一片內存出來,你也會把它轉換成 unsigned_int32 或者 int16 這些你需要的實際類型的數組 / 指針來用。這就是 JS 裏的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是給 ArrayBuffer 提供了一個 “View”,MDN 上的原話叫做 “Multiple views on the same data”,對它們進行下標讀寫,最終都會反應到它所建立在的 ArrayBuffer 之上。❝

1.2 基本操作

「語法」

new ArrayBuffer(length)

「示例」

const buffer = new ArrayBuffer(32);
buffer.byteLength; // 32
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
const buffer2 = buffer.slice(0, 1);

上面代碼表示實例對象 buffer 佔用 32 個字節。

它有實例屬性 byteLength ,表示當前實例佔用的內存字節長度。

它擁有一個靜態方法 isView(),這個方法可以用來判斷是否爲 TypedArray 實例或 DataView 實例。

它擁有實例方法 slice(),用來複制一部分內存,使用方式同數組的 slice 方法。

除了 slice 方法,ArrayBuffer 對象不提供任何直接讀寫內存的方法,只允許在其上方建立視圖,然後通過視圖讀寫。

二、視圖

2.1 TypedArray

     TypedArray 一共包含九種類型,每一種都是一個構造函數。(DataView 視圖不支持 Uint8ClampedArray,其他都支持)

ichBeT

每一種視圖都有一個 BYTES_PER_ELEMENT 常數,表示這種數據類型佔據的字節數。

Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.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

  這 9 個構造函數生成的數組,統稱爲 TypedArray 視圖。它們很像普通數組,都有 length 屬性,普通數組的操作方法和屬性,對 TypedArray 數組完全適用。 普通數組與 TypedArray 數組的差異主要在以下方面:

TypedArray 和 Array 之間也可以互相轉換

const typedArray = new Uint8Array([1, 2, 3, 4]);
const normalArray = Array.apply([], typedArray);

「建立 TypedArray 視圖」

// 創建一個8字節的ArrayBuffer
const a = new ArrayBuffer(8);

// 創建一個指向a的Int32視圖,開始於字節0,直到緩衝區的末尾
const a1 = new Int32Array(a);

// 創建一個指向a的Uint8視圖,開始於字節4,直到緩衝區的末尾
const a2 = new Uint8Array(a, 4);

// 創建一個指向a的Int16視圖,開始於字節4,長度爲2
const a3 = new Int16Array(a, 4, 2);

上面代碼在一段長度爲 8 個字節的內存(a)之上,生成了三個視圖:a1、a2 和 a3。

視圖的構造函數可以接受三個參數:

  建立了視圖以後,就可以進行各種操作了。這裏需要明確的是,視圖其實就是普通數組,語法完全沒有什麼不同,只不過它直接針對內存進行操作,而且每個成員都有確定的數據類型。所以,視圖就被叫做 “類型化數組”。

「TypedArray 視圖操作」

const buffer = new ArrayBuffer(8);

const int16View = new Int16Array(buffer);

for (let i = 0; i < int16View.length; i++) {
  int16View[i] = i * 2;
}
console.log(int16View) // [0, 2, 4, 6]

上面代碼生成一個 8 字節的 ArrayBuffer 對象,然後在它的基礎上,建立了一個 16 位整數的視圖。由於每個字節佔據 8 位,那麼 16 位就佔據了 2 個字節(1 個字節等於 8 位),所以一共可以寫入 4 個整數,依次爲 0,2,4,6。

如果在這段數據上接着建立一個 8 位整數的視圖,則可以讀出完全不一樣的結果。

const int8View = new Int8Array(buffer);

for (let i = 0; i < int8View.length; i++) {
  int8View[i] = i;
}

console.log(int8View) // [0, 0, 2, 0, 4, 0, 6, 0]

首先整個 ArrayBuffer 對象會被分成 8 段。然後,由於 x86 體系的計算機都採用小端字節序(具體概念理解請自主查詢),相對重要的字節排在後面的內存地址,相對不重要字節排在前面的內存地址,所以就得到了上面的結果。還可以看到下面這個例子

const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 10;
v1[1] = 3;
v1[2] = 11;
v1[3] = 8;

console.log(v1) // [10, 3, 11, 8]

const uInt16View = new Uint16Array(buffer); // [0xa, 0x3, 0xb, 0x8]

console.log(uInt16View) // 計算機採用小端字節序 [0x030a, 0x080b] => [778, 2059]

如果一段數據是大端字節序(大端字節序主要用於數據傳輸),TypedArray 數組將無法正確解析,因爲它只能處理小端字節序!爲了解決這個問題,JavaScript 引入 DataView 對象,可以設定字節序。

2.2 DataView

     DataView 視圖是一個可以從二進制 ArrayBuffer 對象中讀寫多種數值類型的底層接口,使用它時,不用考慮不同平臺的字節序問題。

❝ 字節順序,又稱端序或尾序(英語:Endianness),在計算機科學領域中,指存儲器中或在數字通信鏈路中,組成多字節的字的字節的排列順序。
字節的排列方式有兩個通用規則。例如,一個多位的整數,按照存儲地址從低到高排序的字節中,如果該整數的最低有效字節(類似於最低有效位)在最高有效字節的前面,則稱小端序;反之則稱大端序。在網絡應用中,字節序是一個必須被考慮的因素,因爲不同機器類型可能採用不同標準的字節序,所以均按照網絡標準轉化。
例如假設上述變量 x 類型爲 int,位於地址 0x100 處,它的值爲 0x01234567,地址範圍爲 0x100~0x103 字節,其內部排列順序依賴於機器的類型。大端法從首位開始將是:0x100: 01, 0x101: 23,..。而小端法將是:0x100: 67, 0x101: 45,..。❝

「語法」

new DataView(buffer [, byteOffset [, byteLength]])

相關的參數說明如下:

「示例」

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer, 0);

view.setInt8(1, 68);
view.getInt8(1); // 68

  如果一次操作(get 或者 set)兩個或兩個以上字節,就必須明確數據的存儲方式,到底是小端字節序還是大端字節序。DataView 的操作方法默認使用大端字節序解讀數據,如果需要使用小端字節序解讀,必須在操作方法中指定參數爲 true(get 方法的第二個參數和 set 方法的第三個參數)。

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
// 1個字節,默認大端字節序
const v1 = dv.getUint8(0);
// 小端字節序
const v1 = dv.getUint16(1, true);
// 大端字節序
const v2 = dv.getUint16(3, false);
// 在第5個字節,以小端字節序寫入值爲11的32位整數
dv.setInt32(4, 11, true);

  對於直接處理 ArrayBuffer 對象的業務場景不是特別多,特別是寫頁面比較多的同學。筆者深刻認識並運用的場景,主要是在處理比較複雜且數據量比較大的點雲數據,前端接收到的點雲數據已經是原始採集數據轉換過的二進制數據,前端需要對二進制數據進行解析,運用的解析方法就是上述提到的各種方法。下面介紹一下業務場景中比較常見到的一種二進制表示類型——Blob。

三、Blob

3.1 基本介紹

  Blob 對象比較常用於文件上傳文件讀寫操作等。在對文件讀寫的時候,我們更多的時候只是操作 File 對象,而 File 繼承了所有 Blob 的屬性。所以在我們看來,File 對象可以看作一種特殊的 Blob 對象。

  而 Blob 對象與 ArrayBuffer 的區別在於,Blob 對象用於操作二進制文件, ArrayBuffer 用於直接操作內存,所以他們有如下圖的關係:

「語法」

const blob = new Blob(array [, options]);

相關的參數說明如下:

「示例」

const array = ['<h1>Hello World!</h1>'];
const blob = new Blob(array, {type : 'text/html'});

「屬性和方法」

由上圖可以看到,Blob 對象擁有 size 和 type 兩個屬性,以及多種自有方法。比較常用的方法 slice、arrayBuffer 等;slice 方法主要用來拷貝原來的數據,返回的也是一個 Blob 實例,這個方法可以用來做切片上傳。arrayBuffer 方法返回一個 Promise 對象,包含 blob 中的數據,並在 ArrayBuffer 中以二進制數據的形式呈現。

const blob = new Blob([]);
blob.slice(0, 1);
blob.arrayBuffer().then(buffer => /* 處理 ArrayBuffer 數據的代碼……*/);

3.2 運用場景

通過 window.URL.createObjectURL 方法可以把一個 blob 轉化爲一個 Blob URL,並且用做文件下載或者圖片顯示的鏈接。

Blob URL 所實現的下載或者顯示等功能,僅僅可以在單個瀏覽器內部進行。而不能在服務器上進行存儲,亦或者說它沒有在服務器端存儲的意義。

下面是一個 Blob 的例子,可以看到它很短

blob:d3958f5c-0777-0845-9dcf-2cb28783acaf

和冗長的 Base64 格式的 Data URL 相比,Blob URL 的長度顯然不能夠存儲足夠的信息,這也就意味着它只是類似於一個瀏覽器內部的 “引用 “。從這個角度看,Blob URL 是一個瀏覽器自行制定的一個僞協議。

「文件下載」  「圖片顯示」  「切片上傳」  「本地文件讀取」 

四、參考資料

《瞭解 ES6 TypedArray 和 DataView》
《聊聊 JS 的二進制家族:Blob、ArrayBuffer 和 Buffer》
《ECMAScript 6 入門》

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/kgc966FWJFG7fE6eh5kfow