JavaScript ArrayBuffer淺析
簡介:
ArrayBuffer又稱型別化陣列。
javascript陣列(Array)長什麼樣子,相信大家都清楚,那麼我說說差別應該就可以瞭解這究竟是個什麼了!
- 數組裡面可以放數字、字串、布林值以及物件和陣列等,ArrayBuffer放0和1組成的二進位制資料
- 陣列放在堆中,ArrayBuffer則把資料放在棧中(所以取資料時後者快)
- ArrayBuffer初始化後固定大小,陣列則可以自由增減。(準確的說,檢視才應該跟陣列來比較這個特點)
建構函式:
// new ArrayBuffer(Bytelength); var arraybuffer = new ArrayBuffer(8); //類方法ArrayBuffer.isView() 判斷某物件是否為 檢視(這是什麼?往下看) var int8a = new Int8Array(arraybuffer); ArrayBuffer.isView(int8a) //return true //類屬性ArrayBuffer.length 預設值1,暫未發現用處 ArrayBuffer.length //return 1 //返回的物件具有byteLength屬性 值為引數Bytelength arraybuffer.byteLength //return 8
如上所訴:例項化一個物件的時候,僅需要傳入一個引數,即位元組數。
位元組(Byte):儲存空間的基本計量單位。一個位元組等於8位(bit),每一位用0或1表示。
如下為兩個位元組(16個格子):
1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 |
檢視:
ArrayBuffer物件並沒有提供任何讀寫記憶體的方法,而是允許在其上方建立“檢視”,從而插入與讀取記憶體中的資料。如上:我們在記憶體中分配了16個格子也就是兩個位元組,如果我們要劃分出A檢視與B檢視來瓜分這16個格子的話,程式碼是這樣的:
var arraybuffer = new ArrayBuffer(8); var aView = new Int8Array(arraybuffer,0,1); var bView = new Int8Array(arraybuffer,1,1); aView[0] = 1; //二進位制00000001 bView[0] = 2; //二進位制00000010
格子變成這樣了:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
前8位表示數字1,後8位表示數字2
檢視型別
檢視型別 | 資料型別 | 佔用位數 | 佔用位元組 | 有無符號 |
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 | \ |
納尼?連最常用的字串都沒有?悄悄告訴你,字串本身也就用二進位制儲存的,後面細說。
佔用位數就相當於佔用了多少“格子”,等同於佔用位元組數,可以通過訪問檢視型別的靜態屬性:BYTES_PER_ELEMENT來獲取這個值,如:
Int8Array.BYTES_PER_ELEMENT // 1 Uint16Array.BYTES_PER_ELEMENT // 2 Int32Array.BYTES_PER_ELEMENT // 4 Float32Array.BYTES_PER_ELEMENT // 4 Float64Array.BYTES_PER_ELEMENT // 8
有無符號則表示該類資料型別是否包含負數,如:Int8Array代表8位有符號整數,其範圍為 -128~127,而Uint8Array代表8位無符號整數,範圍是 0~255。
檢視建構函式
(一)
var view = new Int16Array([1,653,700,-90,88]);
如上:直接傳入一定特定範圍內的陣列
(二)
var view = new Uint8Array(8); view[0] = 10; view[1] = 58; view[2] = 156; . . . view[7] = 255;
如上:傳入一個數組長度值,佔用的位元組數 = 長度 X 該型別的BYTES_PER_ELEMENT
(三)
//new Int8Array(arraybuffer,start,length); //引數 //arraybuffer為ArrayBuffer的例項 必填 //start表示從第幾個位元組開始 可選(預設從0開始) //length表示資料個數 可選(預設到分配的記憶體末尾) var arraybuffer = new ArrayBuffer(32); var aView = new Int16Array(arraybuffer,0,4); //佔用0-7 var bView = new Float32Array(arraybuffer,8,5); //佔用8-27 var cView = new Uint8Array(arraybuffer,28,8) //僅剩4個,報錯Invalid typed array length
如上:首先分配了32位元組的空間,A檢視使用Int16Array型別從0開始4個數據,每個資料佔2個位元組,所以A檢視一共佔用了8(0-7)個位元組,後面的以此類推,最後留給C檢視的空間僅有4位元組,然而傳入的length為8,所以就超出了所分配記憶體的範圍而報錯。
萬一在分配檢視空間的時候,兩個試圖空間重疊了會發生什麼呢?舉個例子:
var arraybuffer = new ArrayBuffer(4); var aView = new Int8Array(arraybuffer); //從0開始到記憶體末尾 var bView = new Int8Array(arraybuffer,2); //從2開始到末尾 aView[0] = 1; aView[1] = 2; aView[2] = 3; aView[3] = 4; bView[0] = 9; bView[1] = 8; console.log(aView[2] ); //return 9 console.log(aView[3] ); //return 8
兩個相互重疊的檢視所佔據的記憶體空間,存在其中的值以最後一次寫進去的為主。
假如我們寫進去的資料型別不一樣又會發生什麼呢?↓
var arraybuffer = new ArrayBuffer(4); var aView = new Int8Array(arraybuffer); //從0開始到記憶體末尾 var bView = new Int16Array(arraybuffer,2); //從2開始到末尾 aView[0] = 1; aView[1] = 2; aView[2] = 3; aView[3] = 4; bView[0] = 500; bView[1] = 8; console.log(aView[2] ); //return -12 console.log(aView[3] ); //return 1
我們的B檢視從第二個位元組開始,剛好能放一個16位的資料,然而我們在下面又寫
bView[1] = 8;
並沒有報錯。說明在例項化檢視時超出記憶體空間不允許,而對記憶體讀寫時超出則沒有問題。不過bView[1]並沒有值,返回undefined。
接下來我們看看為什麼返回-12與1呢?
500的二進位制值為(16位表示):00000001 11110100
1的二進位制值為(8位表示): 00000001
-12的二進位制值表示(8位表示): 11110100
負數二進位制轉化法(展開):
//先取負數的絕對值 |-12| = 12 //12的二進位制8位為: 00001100 //對上一部的二進位制取反,即1換成0,0換成1 11110011 //最後補碼,即對該值加 1 11110100
原來如此,把500的16位分成兩個8位就是1和-12。但是為什麼-12在前面的呢?
這就要提到位元組序這個東西了,詳細內容點選連結看百科,這裡簡單說一下就是:500這個數字CPU-A認為我應該存為500,CPU-B認為我應該存005,他們各有各的理由,不巧的是個人計算機就是將數字倒著存的,所以放在第三和第四位元組裡面的東西分別是 11110100 00000001
通過實驗(在chrome44裡),我總結了如下幾種情況會得到的結果:
- 如果在A型別中設定了超過A類型範圍的值,則將該值二進位制後,取得對應範圍型別的結果作為最終值;
- 設定某個位元組的值為String字串,則該值為0;
- 設定位元組的值為boolean值,則true為1,false為0;
- 如果在整型中設定了浮點型,則將浮點型取整(沒有四捨五入)後二進位制轉化再取對應範圍的值;
其中第一點和第四點在設定最終值的時候都跟位元組序有關,而為了解決這個問題javascript引入了可以設定位元組序的新型別DataView,詳細情況後面再說。
檢視的方法與屬性
var arraybuffer = new ArrayBuffer(8); var view = new Int8Array(arraybuffer); view.buffer //return arraybuffer readonly view.byteLength //return 8 readonly view.byteOffset //return 0 readonly view.length //return 0 readonly view.entries() //return Array Iterator object 包含鍵值對 view.keys() //return Array Iterator object 只包含鍵 view.set([1,2,3],3) //return [0,0,0,1,2,3,0,0] view.subarray(1,4) //return [0,0,1] 根據上面set後的值 從位置1開始到4但不包括第4位
如上:前四個屬性都是隻讀的:
buffer 返回ArrayBuffer的引用
byteLength 返回位元組長度
byteOffset 返回檢視在該ArrayBuffer中佔用記憶體區域的起點位置
length 返回檢視資料的個數
set() 第一個引數為已有的檢視或者陣列,第二個引數代表從第幾個位元組開始設定值
subarray 返回一個新的檢視,如果第二個引數省略,則取剩餘的全部
entries和keys兩個方法目前僅在chrome和FireFox上面支援,返回一個數組迭代物件,你可以通過該物件的next()方法依次取得相應的值,或者使用for...of迴圈進行迭代。
在寫這篇隨便的時候,我查看了 Mozilla開發者網路 實際上這幾種檢視型別的原型TypedArray還有很多方法,諸如join、indexOf、forEach、map等,但可惜其他瀏覽器並不支援,或許將來會有所改善。
DataView檢視
為了解決各種硬體裝置、資料傳輸等對預設位元組序的設定不一而導致解碼時候會發生的混亂問題,javascript提供了DataView型別的檢視來讓開發者在對記憶體進行讀寫時手動設定位元組序的型別。
(一)DataView建構函式
//new DataView(arraybuffer,byteOffset [, byteLength]) var arraybuffer = new ArrayBuffer(8); var dv1 = new DataView(arraybuffer); //0-7 var dv2 = new DataView(arraybuffer,2); //2-7 var dv3 = new DataView(arraybuffer,3,2); //3-4
(二)DataView例項化後的物件所具有的功能
Read | Write |
getInt8() | setInt8() |
getUint8() | setUint8() |
getInt16() | setInt16() |
getUint16() | setUint16() |
getInt32() | setInt32() |
getUint32() | setUint32() |
getFloat32() | setFloat32() |
getFloat64() | setFloat64() |
以上這些方法均遵循如下的語法
//讀取資料 var num = dataview.getUint32(byteOffset [, littleEndian]); //寫入資料 dataview.setUint32(byteOffset,value [, littleEndian]); //引數 //byteOffset 表示從記憶體的哪個位元組開始 //value 該對應位元組將被設定的值 //littleEndian 位元組序,true為小端位元組序,false或者不填為大端位元組序
值得注意的是,在DataView檢視中,讀寫超出其例項化時的範圍的值時,都會發生錯誤,這跟之前的固定型別的檢視不一樣,在使用時更加謹慎。
你可以通過如下的方式來判斷運行當前javascript的機器使用哪一種位元組序
var littleEndian = (function() { var buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true); return new Int16Array(buffer)[0] === 256; })(); console.log(littleEndian); // true ---->littleEndian //false ---->BigEndian
ArrayBuffer與字串
javascript的字串使用UTF-16編碼的方式,所以我們可以這樣來做:
function Uint162Str(arraybuffer){ return String.fromCharCode.apply(null,new Uint16Array(arraybuffer)); } function Str2Uint16(str){ //假設字串”abc“ length=3,使用16位,則每一個字母佔據2位元組,總位元組為length乘以2 var arraybuffer =new ArrayBuffer(str.length*2); var view = new Uint16Array(arraybuffer); for(var i=0,l=str.length;i<l;i++){ view[i] = str.charCodeAt(i); } return view; }
在實際開發中,我們可能會遇到從伺服器端拿來的二進位制資料的字串使用的是UTF-8編碼的,這時我們就需要先將UTF-8的二進位制編碼還原成為unicode對應的二進位制,目前在有意義的unicode範圍內,已經可以剛好用兩個位元組來容納這個二進位制值了,相當於UTF-8三個位元組來表示的字元,當然也包括了我們最關心的中文字元。然而關於unicode的那些事也比較繁瑣,就不在此討論了,你可以參考這個:Decode UTF-8 with Javascript