JavaScript 二進制數組(ArrayBuffer、Typed Arrays、DataView)

前言

二進制數組是JavaScript用來操做二進制數據的一個接口。有ArrayBuffer對象、TypedArray視圖和DataView視圖三個對象接口。它們能夠以數組下標的形式直接操做內存,能夠與操做系統的原生接口進行二進制通訊。
javascript

隨着Web應用程序變得愈來愈強大,尤爲一些新增長的功能例如:音頻視頻編輯,訪問WebSockets的原始數據等,很明顯有些時候若是使用JavaScript代碼能夠快速方便地經過類型化數組(Typed Arrays)來操做原始的二進制數據將會很是有幫助。注意:(不要把類型數組與正常數組混淆,在類型數組上調用Array.isArray(arr)會返回false,不是全部可用於正常數組的方法都能被類型數組所支持)前端

1、類型數組架構:緩衝和視圖

JavaScript 類型數組(Typed Arrays)將實現拆分爲緩衝視圖兩部分。它是一種處理二進制數據的特殊數組,像C語言那樣直接操縱字節,不過得先用ArrayBuffer對象建立數組緩衝區(Array Buffer),再映射到指定格式的視圖(view)以後,才能讀寫其中的數據。總共有兩類視圖,分別是特定類型的TypedArray和通用類型的DataView。在ES6引入類型化數組以後,大大提高了JavaScript數學運算的性能。
java

ArrayBuffer對象:表明內存之中的一段二進制數據,能夠經過「視圖」進行操做。「視圖」部署了數組接口,這意味着,能夠用數組的方法操做內存。web

TypedArray對象:用來生成內存的視圖,經過9個構造函數,能夠生成9種數據格式的視圖canvas

DataView對象:用來生成內存的視圖,能夠自定義格式和字節序數組

簡單說,ArrayBuffer對象表明原始的二進制數據,TypedArray對象表明肯定類型的二進制數據,DataView對象表明不肯定類型的二進制數據。它們支持的數據類型一共有9種(DataView對象支持除Unit8c之外的其餘8種)瀏覽器


2、ArrayBuffer(緩衝區)

雖然ArrayBuffer對象能夠開闢一片固定大小的內存區域(即數組緩衝區),但它不能直接讀寫所存儲的數據,須要藉助視圖(TypeArray和DataView)來讀寫。
服務器

【2.1】ArrayBuffer()微信

經過構造函數ArrayBuffer()能夠分配指定字節數量的緩衝區,以下代碼所示,分配了一段8個字節的內存區域,每一個字節的默認值都爲0。有一點要注意,緩衝區的容量在指定後,就不可再修改。網絡

let buffer = new ArrayBuffer(8);
複製代碼

【2.2】ArrayBuffer.prototype.byteLength

ArrayBuffer實例的byteLength屬性,返回所分配的內存區域的字節長度。

有時候咱們要分配的內存區域很大,有可能分配失敗(由於沒有那麼多的連續空餘內存),因此有必要利用檢查是否分配成功。

let buffer = new ArrayBuffer(8)
console.log(buffer.byteLength) // 8複製代碼

【2.3】ArrayBuffer.prototype.slice()

ArrayBuffer實例有一個slice方法,容許將內存區域的一部分,拷貝生成一個新的ArrayBuffer對象。

let buffer = new ArrayBuffer(8);
let newBuffer = buffer.slice(0, 3);

// 上面代碼拷貝buffer對象的前3個字節(從0開始,到第3個字節前面結束),生成一個新的ArrayBuffer對象。
// slice方法其實包含兩步,第一步是先分配一段新內存,第二步是將原來那個ArrayBuffer對象拷貝過去。複製代碼

【2.4】ArrayBuffer.isView()

ArrayBuffer有一個靜態方法isView,返回一個布爾值,表示參數是否爲ArrayBuffer的視圖實例。這個方法大體至關於判斷參數,是否爲TypedArray實例或DataView實例。

let buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false

let v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true複製代碼

3、TypedArray

ArrayBuffer對象做爲內存區域,能夠存放多種類型的數據。同一段內存,不一樣數據有不一樣的解讀方式,這就叫作「視圖」(view)。ArrayBuffer有兩種視圖,一種是TypedArray視圖,另外一種是DataView視圖,二者的區別主要是字節序,前者的數組成員都是同一個數據類型,後者的數組成員能夠是不一樣的數據類型。目前,TypedArray對象一共提供9種類型的視圖,每一種視圖都是一種構造函數。

【3.1】數據類型

  • 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個字節

這9個構造函數生成的對象,統稱爲TypedArray對象。它們很像正常數組,都有length屬性,都能用方括號運算符([])獲取單個元素,全部數組的方法,在類型化數組上面都能使用。

TypeArray和數組主要的區別是:

  • TypedArray數組的全部成員,都是同一種類型和格式。
  • TypedArray數組的成員是連續的,不會有空位。
  • Typed化數組成員的默認值爲0。好比, new Array(10)返回一個正常數組,裏面沒有任何成員,只是10個空位;new Unit8Array(10)返回一個類型化數組,裏面10個成員都是0。
  • TypedArray數組只是一層視圖,自己不儲存數據,它的數據都儲存在底層的ArrayBuffer對象之中,要獲取底層對象必須使用buffer屬性。
【3.2】TypedArray(buffer, byteOffset=0, length?)
  • 第一個參數(必需):視圖對應的底層ArrayBuffer對象,
  • 第二個參數(可選):視圖開始的字節序號,默認從0開始。
  • 第三個參數(可選):視圖包含的數據個數,默認直到本段內存區域結束。
// 建立一個8字節的ArrayBuffer
let b = new ArrayBuffer(8);

// 建立一個指向b的Int32視圖,開始於字節0,直到緩衝區的末尾
let v1 = new Int32Array(b);

// 建立一個指向b的Uint8視圖,開始於字節2,直到緩衝區的末尾
let v2 = new Uint8Array(b, 2);

// 建立一個指向b的Int16視圖,開始於字節2,長度爲2
let v3 = new Int16Array(b, 2, 2);


// v1[0]是一個32位整數,指向字節0~字節3;
// v2[0]是一個8位無符號整數,指向字節2;
// v3[0]是一個16位整數,指向字節2~字節3。
// v一、v2和v3是重疊的,只要任何一個視圖對內存有所修改,就會在另外兩個視圖上反應出來。複製代碼

【3.3】TypedArray(length)

視圖還能夠不經過ArrayBuffer對象,直接分配內存而生成。

let f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

// 上面代碼生成一個8個成員的Float64Array數組(共64字節)
// 而後依次對每一個成員賦值。這時,視圖構造函數的參數就是成員的個數
// 能夠看到,視圖數組的賦值操做與普通數組的操做毫無兩樣複製代碼

【3.4】TypedArray(typeArray)

類型化數組的構造函數,能夠接受另外一個視圖實例做爲參數。

let typedArray = new Int8Array(new Uint8Array(4));

// 上面代碼中,Int8Array構造函數接受一個Uint8Array實例做爲參數。
// 注意,此時生成的新數組,只是複製了參數數組的值,對應的底層內存是不同的。新數組會開闢一段新的內存儲存數據,不會在原數組的內存之上創建視圖。


let x = new Int8Array([1, 1]);
let y = new Int8Array(x);
x[0] // 1
y[0] // 1
x[0] = 2;
y[0] // 1

// 上面代碼中,數組y是以數組x爲模板而生成的,當x變更的時候,y並無變更。
//若是想基於同一段內存,構造不一樣的視圖,能夠採用下面的寫法。


var x = new Int8Array([1, 1]);
var y = new Int8Array(x.buffer);
x[0] // 1
y[0] // 1
x[0] = 2;
y[0] // 2複製代碼

【3.5】TypedArray(arrayLikeObject)

構造函數的參數也能夠是一個普通數組,而後直接生成TypedArray實例。

let typedArray = new Uint8Array([1, 2, 3, 4]);

// 上面代碼從一個普通的數組,生成一個8位無符號整數的TypedArray實例
// 注意,這時TypedArray視圖會從新開闢內存,不會在原數組的內存上創建視圖
// TypedArray數組也能夠轉換回普通數組,代碼以下

let normalArray = Array.prototype.slice.call(typedArray);複製代碼

【3.6】BYTES_PER_ELEMENT屬性

每一種視圖的構造函數,都有一個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

// 這個屬性在TypedArray實例上也能獲取,即有TypedArray.prototype.BYTES_PER_ELEMENT複製代碼

【3.7】ArrayBuffer與字符串的互相轉換

ArrayBuffer轉爲字符串,或者字符串轉爲ArrayBuffer,有一個前提,即字符串的編碼方法是肯定的。假定字符串採用UTF-16編碼(JavaScript的內部編碼方式),能夠本身編寫轉換函數。

// ArrayBuffer轉爲字符串,參數爲ArrayBuffer對象
function ab2str(buffer) {
  return String.fromCharCode.apply(null, new Uint16Array(buffer));
}

// 字符串轉爲ArrayBuffer對象,參數爲字符串
function str2ab(str) {
  let buf = new ArrayBuffer(str.length * 2); // 每一個字符佔用2個字節
  let bufView = new Uint16Array(buf);
  for (var i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}複製代碼

【3.8】溢出

不一樣的視圖類型,所能容納的數值範圍是肯定的。超出這個範圍,就會出現溢出。好比,8位視圖只能容納一個8位的二進制值,若是放入一個9位的值,就會溢出。TypedArray數組的溢出處理規則,簡單來講,就是拋棄溢出的位,而後按照視圖類型進行解釋。

let uint8 = new Uint8Array(1); // 1個字節 = 8位

uint8[0] = 256;
uint8[0] // 0

uint8[0] = -1;
uint8[0] // 255

// 上面代碼中,uint8是一個8位視圖,
// 而256的二進制形式是一個9位的值100000000,這時就會發生溢出。
// 根據規則,只會保留後8位,即00000000。uint8視圖的解釋規則是無符號的8位整數,因此00000000就是0。複製代碼

【3.9】TypedArray.prototype.buffer

TypedArray實例的buffer屬性,返回整段內存區域對應的ArrayBuffer對象。該屬性爲只讀屬性。

let a = new Float32Array(64);
let b = new Uint8Array(a.buffer);

// 上面代碼的a視圖對象和b視圖對象,對應同一個ArrayBuffer對象,即同一段內存。複製代碼

【3.10】TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

byteLength屬性返回TypedArray數組佔據的內存長度,單位爲字節。byteOffset屬性返回TypedArray數組從底層ArrayBuffer對象的哪一個字節開始。這兩個屬性都是隻讀屬性。

var b = new ArrayBuffer(8);

var v1 = new Int32Array(b);
var v2 = new Uint8Array(b, 2);
var v3 = new Int16Array(b, 2, 2);

v1.byteLength // 8
v2.byteLength // 6
v3.byteLength // 4

v1.byteOffset // 0
v2.byteOffset // 2
v3.byteOffset // 2複製代碼

【3.11】TypedArray.prototype.length​​​​​​​

length屬性表示TypedArray數組含有多少個成員。注意將byteLength屬性和length屬性區分,前者是字節長度,後者是成員長度。

let a = new Int16Array(8);

a.length // 8
a.byteLength // 16複製代碼

【3.12】TypedArray.prototype.set()

TypedArray數組的set方法用於複製數組(正常數組或TypedArray數組),也就是將一段內容徹底複製到另外一段內存。

var a = new Uint8Array(8);
var b = new Uint8Array(8);
b.set(a);

// 上面代碼複製a數組的內容到b數組,它是整段內存的複製,比一個個拷貝成員的那種複製快得多。
// set方法還能夠接受第二個參數,表示從b對象哪個成員開始複製a對象。

var a = new Uint16Array(8);
var b = new Uint16Array(10);
b.set(a, 2)

// 上面代碼的b數組比a數組多兩個成員,因此從b[2]開始複製。複製代碼

【3.13】TypedArray.prototype.subarray()​​​​​​​

subarray方法是對於TypedArray數組的一部分,再創建一個新的視圖。subarray方法的第一個參數是起始的成員序號,第二個參數是結束的成員序號(不含該成員),若是省略則包含剩餘的所有成員。

var a = new Uint16Array(8);
var b = a.subarray(2,3);

a.byteLength // 16
b.byteLength // 2

// 上面代碼的a.subarray(2,3),意味着b只包含a[2]一個成員,字節長度爲2。複製代碼

【3.14】TypedArray.prototype.slice()​​​​​​​

TypeArray實例的slice方法,能夠返回一個指定位置的新的TypedArray實例。slice方法的參數,表示原數組的具體位置,開始生成新數組。負值表示逆向的位置,即-1爲倒數第一個位置,-2表示倒數第二個位置,以此類推。

let ui8 = Uint8Array.of(0, 1, 2); // [0, 1, 2]
ui8.slice(-1) // Uint8Array [2]

// 上面代碼中,ui8是8位無符號整數數組視圖的一個實例。
// 它的slice方法能夠從當前視圖之中,返回一個新的視圖實例。複製代碼

4、DataView

若是一段數據包括多種類型(好比服務器傳來的HTTP數據),這時除了創建ArrayBuffer對象的複合視圖之外,還能夠經過DataView視圖進行操做。

DataView視圖提供更多操做選項,並且支持設定字節序。原本,在設計目的上,ArrayBuffer對象的各類TypedArray視圖,是用來向網卡、聲卡之類的本機設備傳送數據,因此使用本機的字節序就能夠了;而DataView視圖的設計目的,是用來處理網絡設備傳來的數據,因此大端字節序或小端字節序是能夠自行設定的。

【4.1】DataView(ArrayBuffer buffer [, 字節起始位置 [, 長度]])

DataView視圖自己也是構造函數,接受一個ArrayBuffer對象做爲參數,生成視圖。

let buffer = new ArrayBuffer(24);
let dv = new DataView(buffer);複製代碼

【4.2】DataView實例屬性

DataView實例有如下屬性,含義與TypedArray實例的同名方法相同。

DataView.prototype.buffer     // 返回對應的ArrayBuffer對象
DataView.prototype.byteLength // 返回佔據的內存字節長度
DataView.prototype.byteOffset // 返回當前視圖從對應的ArrayBuffer對象的哪一個字節開始複製代碼

【4.3】瞭解端序

端序又稱字節序(Endianness),表示多字節中的字節排列方式。小端序是指字節的最低有效位在最高有效位以前(大端序正好與之相反),例如數字10,若是用16位二進制表示,那麼它就變爲0000 0000 0000 1010,換算成16進制就是000A,用小端序存儲的話,該值會被表示成0A00。雖然大端序更符合人類的閱讀習慣,但英特爾處理器和多數瀏覽器採用的都是小端序。引入該參數後,能更靈活的處理不一樣存儲方式的數據。

【4.4】讀取內存

  • getInt8:讀取1個字節,返回一個8位整數。
  • getUint8:讀取1個字節,返回一個無符號的8位整數。
  • getInt16:讀取2個字節,返回一個16位整數。
  • getUint16:讀取2個字節,返回一個無符號的16位整數。
  • getInt32:讀取4個字節,返回一個32位整數。
  • getUint32:讀取4個字節,返回一個無符號的32位整數。
  • getFloat32:讀取4個字節,返回一個32位浮點數。
  • getFloat64:讀取8個字節,返回一個64位浮點數。

這一系列get方法的參數都是一個字節序號(不能是負數,不然會報錯),表示從哪一個字節開始讀取。

let buffer = new ArrayBuffer(24);
let dv = new DataView(buffer);

// 從第1個字節讀取一個8位無符號整數
let v1 = dv.getUint8(0);

// 從第2個字節讀取一個16位無符號整數
let v2 = dv.getUint16(1);

// 從第4個字節讀取一個16位無符號整數
let v3 = dv.getUint16(3);

// 上面代碼讀取了ArrayBuffer對象的前5個字節,其中有一個8位整數和兩個十六位整數。複製代碼

若是一次讀取兩個或兩個以上字節,就必須明確數據的存儲方式,究竟是小端字節序仍是大端字節序。默認狀況下,DataView的get方法使用大端字節序解讀數據,若是須要使用小端字節序解讀,必須在get方法的第二個參數指定true。

// 小端字節序
let v1 = dv.getUint16(1, true);

// 大端字節序
let v2 = dv.getUint16(3, false);

// 大端字節序
let v3 = dv.getUint16(3);複製代碼

【4.5】寫入內存

  • setInt8:寫入1個字節的8位整數。
  • setUint8:寫入1個字節的8位無符號整數。
  • setInt16:寫入2個字節的16位整數。
  • setUint16:寫入2個字節的16位無符號整數。
  • setInt32:寫入4個字節的32位整數。
  • setUint32:寫入4個字節的32位無符號整數。
  • setFloat32:寫入4個字節的32位浮點數。
  • setFloat64:寫入8個字節的64位浮點數。

這一系列set方法,接受兩個參數,第一個參數是字節序號,表示從哪一個字節開始寫入,第二個參數爲寫入的數據。對於那些寫入兩個或兩個以上字節的方法,須要指定第三個參數,false或者undefined表示使用大端字節序寫入,true表示使用小端字節序寫入。

// 在第1個字節,以大端字節序寫入值爲25的32位整數
dv.setInt32(0, 25, false);

// 在第5個字節,以大端字節序寫入值爲25的32位整數
dv.setInt32(4, 25);

// 在第9個字節,以小端字節序寫入值爲2.5的32位浮點數
dv.setFloat32(8, 2.5, true);複製代碼

5、具體應用

【5.1】webSocket

webSocket能夠經過arrayBuffer,發送或接收二進制數據。

let socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';

socket.addEventListener('open', function (event) {
  let typedArray = new Uint8Array(4);
  socket.send(typedArray.buffer);
});

socket.addEventListener('message', function (event) {
  let arrayBuffer = event.data;
  // ···
});複製代碼

【5.2】Ajax

傳統上,服務器經過AJAX操做只能返回文本數據,即responseType屬性默認爲text。XMLHttpRequest第二版XHR2容許服務器返回二進制數據,這時分紅兩種狀況。若是明確知道返回的二進制數據類型,能夠把返回類型(responseType)設爲arraybuffer;若是不知道,就設爲blob。

let xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';

xhr.onload = function () {
  let arrayBuffer = xhr.response;
  // ···
};

xhr.send();複製代碼

【5.3】Canvas

將Canvas數據轉換爲二進制格式

imagedata = context.getImageData(0, 0, imagewidth,imageheight);    
let canvaspixelarray = imagedata.data;   
  
let canvaspixellen = canvaspixelarray.length;  
let bytearray = new Uint8Array(canvaspixellen);  
  
for (let i=0;i<canvaspixellen;++i) {  
     bytearray[i] = canvaspixelarray[i];  
} 複製代碼

把二進制數據還原爲圖像的代碼以下,請注意咱們不能直接從arrayBuffer獲取數據直接放到Canvas中

let bytearray = new Uint8Array(event.data);  
   
let tempcanvas = document.createElement('canvas');  
    tempcanvas.height = imageheight;  
    tempcanvas.width = imagewidth;  
let tempcontext = tempcanvas.getContext('2d');  
let imgdata = tempcontext.getImageData(0,0,imagewidth,imageheight);  
let imgdataimgdatalen = imgdata.data.length;  
   
for(let i=8;i<imgdatalen;i++)  {  
    imgdata.data[i] = bytearray[i];  
}  
  
tempcontext.putImageData(imgdata,0,0);  複製代碼


文章每週持續更新,能夠微信搜索「 前端大集錦 」第一時間閱讀

相關文章
相關標籤/搜索