天天看點

JS中的二進制操作簡介

JS中的二進制操作簡介

寫這篇部落格的起源是小胡子哥的一篇文章《你所不知道的javascript數組你所不知道的javascript數組》。

因為随着xhr2和現代浏覽器的普及,在浏覽器當中處理二進制不再向過去那樣無所适從,随着canvas/webgl等新技術逐漸開始進入大衆視野,也會用到一些位元組數組或者16位、8位整數等東西。在node.js剛剛釋出的4.0版本中,buffer的底層使用了更符合js标準的uint8array來實作,浏覽器和node.js再次向相同的目标靠近了一點點,是以對于js中處理二進制,我就打算寫這篇文章作一個入門性質的流水賬,友善一些對二進制處理不了解的同學快速入門,雖然在前端領域用到的不多,不過也可以作為茶餘飯後的休閑談資。

二進制資料在js程式裡的表達

現今世界上幾乎所有的計算機體系結構都是以位元組(byte)為二進制資料的基本機關(注:不是說最小機關),是以二進制常常以位元組數組的形式存在于程式當中。例如在c#裡面,就用byte[],标準c裡面沒有byte類型,但可以通過typedef把byte定義為unsigned

char的别名,效果是一樣的。

js設計之初似乎根本沒想過要處理二進制的東西,加上對類型的極度弱化,對于位元組的概念可以說是非常非常的模糊。如果要表達位元組數組,那麼似乎隻能用一個普通數組來表示。

html5體系引入了一大堆新的東西,比如xhr2,是可以上傳或下載下傳二進制内容的,與之配套的東西就是js裡的arraybuffer和typed array了。

arraybuffer是一個固定長度的位元組序列,你可以通過new

arraybuffer(length)來得到一片空間,或者用下文将會介紹的方法從xhr2等途徑擷取。由于内部實作與數組不一樣,arraybuffer通常都是連續記憶體(注意,這隻是經驗之談,并不是規範也不是文檔所明确的),是以對于高密度的通路操作而言它比js中的array速度會快很多(但并不要用它來簡單地代替array)。如果用chrome的profile工具檢視heap

snapshot,會發現arraybuffer會被單獨列為一類,也許它的記憶體配置設定和布局與array以及其他js對象有一些差别吧。

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個位元組,我們分别用它來構造uint8array, uint16array, uint32array,則可以得到

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是通過file和blob一套相關接口。

通過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,并提供了name, lastmodifieddate等基礎中繼資料,但是依然是一個深度封裝,不能直接擷取到它的二進制。

blob是binary 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的buffer, bytelength,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對記憶體對齊有要求,它不能在非對齊的位置建立,同理,uint32array和int32array則要求偏移量是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,

blob, file, filereader, formdata等多種方式來擷取或消費arraybuffer。

另外羅嗦一句,浏覽器還提供了一系列所謂的“binary

string”,就是一些看起來像亂碼一樣的字元串,然後又提供了atob/btoa這種方式來對base64和“binary

string”進行互相轉換,甚至filereader還提供了readasbinarystring方法(已經廢棄了,善哉)。這個binary

string真是誰用誰遭殃,别問我為什麼知道……

作者:佚名

來源:51cto

繼續閱讀