JavaScript ArrayBuffer淺析


時隔一年半,再次來到博客園。回首剛接觸前端時所寫的兩篇隨筆,無法直視啊~

----------------------------------------------------------------------------♠

 

簡介:

  ArrayBuffer又稱類型化數組

  javascript數組(Array)長什么樣子,相信大家都清楚,那么我說說差別應該就可以了解這究竟是個什么了!

  1. 數組里面可以放數字、字符串、布爾值以及對象和數組等,ArrayBuffer放0和1組成的二進制數據
  2. 數組放在堆中,ArrayBuffer則把數據放在棧中(所以取數據時后者快)
  3. 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里),我總結了如下幾種情況會得到的結果:

  1. 如果在A類型中設置了超過A類型范圍的值,則將該值二進制后,取得對應范圍類型的結果作為最終值;
  2. 設置某個字節的值為String字符串,則該值為0;
  3. 設置字節的值為boolean值,則true為1,false為0;
  4. 如果在整型中設置了浮點型,則將浮點型取整(沒有四舍五入)后二進制轉化再取對應范圍的值;

其中第一點和第四點在設置最終值的時候都跟字節序有關,而為了解決這個問題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


參考鏈接

TypedArray(MDN)

Javascript TypedArray 解惑:Uint8Array 與 Uint8ClampedArray 的區別

后記:

  雖然兩三年前別人就寫過這個東西了,但是我還是寫了一遍,以前寫了兩篇就放棄了,因為看到很多大神的文章,覺得自己實在太菜,連寫的資格都沒有。這一年半的時間都在看別人的,如今才恍然,我不過是要記錄自己的編程之路,通過寫作才能對問題研究得相對透徹,又不是要出教程,勇敢的告訴大家,鄙人才疏學淺,請各位看官多多指正!o(∩_∩)o 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM