[JavaScript]爲JS處理二進制數據提供可能性的WEB API

寫這篇博客的起源是在div.io上的一篇文章《你所不知道的JavaScript數組》by 小鬍子哥下的評論中的討論。javascript

由於隨着XHR2和現代瀏覽器的普及,在瀏覽器當中處理二進制再也不向過去那樣無所適從,隨着Canvas/WebGL等新技術逐漸開始進入大衆視 野,也會用到一些字節數組或者16位、8位整數等東西。在node.js剛剛發佈的4.0版本中,Buffer的底層使用了更符合JS標準的Uint8Array來實現,瀏覽器和node.js再次向相同的目標靠近了一點點,因此對於JS中處理二進制,我就打算寫這篇文章做一個入門性質的流水帳,方便一些對二進制處理不瞭解的同窗快速入門,雖然在前端領域用到的很少,不過也能夠做爲茶餘飯後的休閒談資。前端

二進制數據在JS程序裏的表達

現今世界上幾乎全部的計算機體系結構都是以字節(byte)爲二進制數據的基本單位(注:不是說最小單位),因此二進制經常以字節數組的形式存在於程序當中。例如在C#裏面,就用byte[],標準C裏面沒有byte類型,但能夠經過typedefbyte定義爲unsigned char的別名,效果是同樣的。java

JS設計之初彷佛根本沒想過要處理二進制的東西,加上對類型的極度弱化,對於字節的概念能夠說是很是很是的模糊。若是要表達字節數組,那麼彷佛只能用一個普通數組來表示。node

HTML5體系引入了一大堆新的東西,好比XHR2,是能夠上傳或下載二進制內容的,與之配套的東西就是JS裏的ArrayBufferTyped Array了。web

ArrayBuffer是一個固定長度的字節序列,你能夠經過new ArrayBuffer(length)來獲得一片空間,或者用下文將會介紹的方法從XHR2等途徑獲取。因爲內部實現與數組不同,ArrayBuffer通 常都是連續內存(注意,這只是經驗之談,並非規範也不是文檔所明確的),所以對於高密度的訪問操做而言它比JS中的Array速度會快不少(但並不要用 它來簡單地代替Array)。若是用Chrome的Profile工具查看Heap Snapshot,會發現ArrayBuffer會被單獨列爲一類,也許它的內存分配和佈局與Array以及其餘JS對象有一些差異吧。canvas

ArrayBuffer是不能直接被訪問的,所以須要藉助Typed Array。Typed Array是一組具體數據類型的Array-Like類型的統稱,包括數組

Int8Array             8位有符號整數,相似於C裏面的char
Uint8Array            8位無符號整數,相似於C裏面的unsigned char
Uint8ClampedArray     8位無符號整數,跟Uint8相似,但在溢出處理上不大同樣
Int16Array            後面這些類型就不羅嗦了
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array

 

Typed Array的背後是一個ArrayBuffer,也就是說,事實上的數據是存在ArrayBuffer裏面的,而Typed Array只是給你提供了一個某種類型的讀寫接口,用MDN的話說,叫作瀏覽器

Multiple views on the same data網絡

舉個栗子,若是咱們有一個ArrayBuffer名爲buffer(先不考慮怎麼構造這個測試數據),內容以下:工具

01 02 03 04 05 06 07 08

 也就是說它有8個字節,咱們分別用它來構造Uint8ArrayUint16ArrayUint32Array,則能夠獲得

var  u8 = new  Uint8Array(buffer); // length爲8
var u16 = new Uint16Array(buffer); // length爲4
var u32 = new Uint32Array(buffer); // length爲2

 它們的內容分別爲

[1, 2, 3, 4, 5, 6, 7, 8]
[513, 1027, 1541, 2055]
[67305985, 134678021]

 

這不難理解。

能夠看出,若是要手工構造上面的測試數據ArrayBuffer,用Uint8Array就會很方便(呃事實上這是我我的最經常使用的一種Typed Array)。

而若是用一樣的ArrayBuffer構建帶符號整數類型,則可能由於整數溢出而獲得不一樣的結果,上面的例子並無碰到,有興趣的話能夠本身試試。所以使用Typed Array也能夠用來作有符號數和無符號數的轉換。

若是你用過canvas的getImageData/putImageData的話,會發現它給你的就是一個Uint8ClampedArray,這東西訪問起來速度比JS的原生Array快不少,使得對canvas進行高速的像素操做成爲可能。

然而最最重要的一個概念仍是:Typed Array不直接存聽任何數據,全部對Typed Array進行讀寫的操做,最終都會落實到它背後所持有的ArrayBuffer的身上。ArrayBuffer纔是真正的raw bytes,而Typed Array只是一個操做窗口/操做視圖(View)。

獲取二進制數據

nodejs那邊先按住不表,這裏談談在網頁裏如何獲取二進制數據?常見的辦法有3種,1是經過XMLHttpRequest 2,2是經過FileBlob一套相關接口。

經過XMLHttpRequest 2

XHR2的接口跟XHR幾乎是同樣的,當制定xhr.responseType = 'arraybuffer'之後,在成功獲取數據的回調裏就能夠經過xhr.response來獲得請求結果的ArrayBuffer了,而後就能夠按照你的意願來構造各類Typed Array進行訪問。

responseType還能夠有blob取值,能夠用xhr.response得到Blob對象。

經過File和Blob

在HTML5中提供了對錶單的文件控件<input type="file" />更豐富的操做,能夠經過inputDOM對象的.files來獲取一個FileList,固然一般瀏覽器都只提供了單選的文件控件,因而這裏都只會有一個File對象。另外,經過拖拽、剪貼板等方式也能獲取到File或者Blob

File繼承了Blob,並提供了namelastModifiedDate等基礎元數據,可是依然是一個深度封裝,不能直接獲取到它的二進制。

BlobBinary large object的縮寫,它與ArrayBuffer的區別是除了raw bytes之外它還提供了mime type做爲元數據。但它依然是沒法直接被讀寫的。

這時候須要藉助FileReader的幫忙。FileReader提供了一組用來將Blob讀取爲更爲實用的類型的方法

readAsArrayBuffer()
readAsBinaryString()
readAsDataURL()
readAsText()

 例如

var file = get_file_some_how();
var fr = new FileReader();
fr.onload = function(e) {
    e.target.result; // 讀取的結果
};
fr.readAsDataUrl(file); // readAsArrayBuffer

 

能夠幹什麼呢?例如圖片上傳以前的本地預覽(甚至基於canvas的編輯)等等均可以實現了。

Blob的其餘構造方法多而雜,這裏就先不處處搬運文檔了。

消費二進制數據

何謂消費?最多見的方式也許就是經過XHR2直接把二進制數據以文件方式POST到服務端去。

這裏我比較推薦使用FormData來構造POST數據。由於在服務端收的時候會比較容易一些,具體有興趣能夠去找找別人的例子。

雖然直接提交ArrayBuffer也是能夠的,可是這種時候服務端收到的POST body會是一大團,用起來不方便。若是要使用FormData來提交ArrayBuffer,須要先將其構形成Blob

對Typed Array的構造留個心眼

當使用new xxxxxArray(arrayBuffer)這個重載進行構造的時候,它會默認基於此ArrayBuffer進行構造。但當使用new xxxxArray(another_typed_array)這個重載的時候,則是進行「拷貝構造」,這樣兩個Typed Array會指向不一樣的buffer,須要注意這是否符合預期。

若是須要基於同一個ArrayBuffer來構造Typed Array,可使用Typed Array的bufferbyteLength,byteOffset來獲取它背後的ArrayBuffer

Tips(坑)

對內存對齊留個心眼

當使用ArrayBuffer來構造Typed Array的時候,能夠指定byteOffset參數,例如

var buffer = get_array_buffer_some_how();
var i16 = new Int16Array(buffer, 10);

 上面的代碼就能以buffer向後偏移10字節處爲起點來構造Int16Array,可是若是將10設置爲一個奇數,會發現以下錯誤:

RangeError: start offset of Int16Array should be a multiple of 2

 

這是由於Typed Array對內存對齊有要求,它不能在非對齊的位置創建,同理,Uint32ArrayInt32Array則要求偏移量是4字節對齊的。

所以若是你但願在非對齊的位置進行讀寫,則須要藉助DataView的幫忙。

對字節序留個心眼

咱們平常中所寫的程序,幾乎都不須要關心字節序,所以這個問題沒那麼嚴重,知道本身的程序會有字節序問題的人,開發到這裏也確定會知道問題的存在,但這裏仍是稍微提一下。

按照MDN的說法,Typed Array只會使用當前平臺的字節序,例如咱們如今用的桌面電腦不論PC仍是Mac都是x86/x64的,也就是little-endian了。

使用DataView,不只能夠解決上面說到的內存對齊的問題,還能夠指定讀寫時的字節序,具體參數都在文檔裏面了,就不搬運了。

使用DataView配合Typed Array也能夠作到一個檢測當前平臺字節序的技巧:

function isLittleEndian() {
    var buf = new ArrayBuffer(2);
    var view = new DataView(buf);
    view.setInt16(0, 256, true); // 顯式以little endian寫入數據
    // 此時buf裏的內存佈局應該是 00 01
 
    var i16 = new Int16Array(buf);
    // 若是以little endian讀取,它就是256;以big endian讀取,則是1
    return (i16[0] === 256);
}

 

若是你編寫的程序須要垮體系結構例如x86/ARM/PPC等,則在交換文件和網絡包的時候須要謹慎處理字節序,固然一個辦法是在這些地方預先規範統一字節序以防後患。不過那些都是題外話了。

小姐小結

使用ArrayBuffer來存儲一段字節,使用Typed Array來構建一個具體數值類型的訪問窗口,使用DataView對非對齊或在意字節序的ArrayBuffer進行更精確的操做,使用XHR2, BlobFileFileReaderFormData等多種方式來獲取或消費ArrayBuffer

另外羅嗦一句,瀏覽器還提供了一系列所謂的「Binary String」,就是一些看起來像亂碼同樣的字符串,而後又提供了atob/btoa這種方式來對Base64和「Binary String」進行相互轉換,甚至FileReader還提供了readAsBinaryString方法(已經廢棄了,善哉)。這個Binary String真是誰用誰遭殃,別問我爲何知道……

http://web.jobbole.com/83701/

https://developer.mozilla.org/zh-CN/docs/Web/API/ByteString

https://developer.mozilla.org/en-US/docs/Web/API/DOMString/Binary

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays

相關文章
相關標籤/搜索