Javascript ArrayBuffer,二進(jìn)制數(shù)組

2023-02-17 10:56 更新

在 Web 開發(fā)中,當(dāng)我們處理文件時(創(chuàng)建,上傳,下載),經(jīng)常會遇到二進(jìn)制數(shù)據(jù)。另一個典型的應(yīng)用場景是圖像處理。

這些都可以通過 JavaScript 進(jìn)行處理,而且二進(jìn)制操作性能更高。

不過,在 JavaScript 中有很多種二進(jìn)制數(shù)據(jù)格式,會有點(diǎn)容易混淆。僅舉幾個例子:

  • ?ArrayBuffer?,?Uint8Array?,?DataView?,?Blob?,?File? 及其他。

與其他語言相比,JavaScript 中的二進(jìn)制數(shù)據(jù)是以非標(biāo)準(zhǔn)方式實(shí)現(xiàn)的。但是,當(dāng)我們理清楚以后,一切就會變得相當(dāng)簡單了。

基本的二進(jìn)制對象是 ArrayBuffer —— 對固定長度的連續(xù)內(nèi)存空間的引用。

我們這樣創(chuàng)建它:

let buffer = new ArrayBuffer(16); // 創(chuàng)建一個長度為 16 的 buffer
alert(buffer.byteLength); // 16

它會分配一個 16 字節(jié)的連續(xù)內(nèi)存空間,并用 0 進(jìn)行預(yù)填充。

?ArrayBuffer? 不是某種東西的數(shù)組

讓我們先澄清一個可能的誤區(qū)。ArrayBuffer 與 Array 沒有任何共同之處:

  • 它的長度是固定的,我們無法增加或減少它的長度。
  • 它正好占用了內(nèi)存中的那么多空間。
  • 要訪問單個字節(jié),需要另一個“視圖”對象,而不是 ?buffer[index]?。

ArrayBuffer 是一個內(nèi)存區(qū)域。它里面存儲了什么?無從判斷。只是一個原始的字節(jié)序列。

如要操作 ArrayBuffer,我們需要使用“視圖”對象。

視圖對象本身并不存儲任何東西。它是一副“眼鏡”,透過它來解釋存儲在 ArrayBuffer 中的字節(jié)。

例如:

  • ?Uint8Array? —— 將 ?ArrayBuffer? 中的每個字節(jié)視為 0 到 255 之間的單個數(shù)字(每個字節(jié)是 8 位,因此只能容納那么多)。這稱為 “8 位無符號整數(shù)”。
  • ?Uint16Array? —— 將每 2 個字節(jié)視為一個 0 到 65535 之間的整數(shù)。這稱為 “16 位無符號整數(shù)”。
  • ?Uint32Array? —— 將每 4 個字節(jié)視為一個 0 到 4294967295 之間的整數(shù)。這稱為 “32 位無符號整數(shù)”。
  • ?Float64Array? —— 將每 8 個字節(jié)視為一個 ?5.0x10-324? 到 ?1.8x10308? 之間的浮點(diǎn)數(shù)。

因此,一個 16 字節(jié) ArrayBuffer 中的二進(jìn)制數(shù)據(jù)可以解釋為 16 個“小數(shù)字”,或 8 個更大的數(shù)字(每個數(shù)字 2 個字節(jié)),或 4 個更大的數(shù)字(每個數(shù)字 4 個字節(jié)),或 2 個高精度的浮點(diǎn)數(shù)(每個數(shù)字 8 個字節(jié))。


ArrayBuffer 是核心對象,是所有的基礎(chǔ),是原始的二進(jìn)制數(shù)據(jù)。

但是,如果我們要寫入值或遍歷它,基本上幾乎所有操作 —— 我們必須使用視圖(view),例如:

let buffer = new ArrayBuffer(16); // 創(chuàng)建一個長度為 16 的 buffer

let view = new Uint32Array(buffer); // 將 buffer 視為一個 32 位整數(shù)的序列

alert(Uint32Array.BYTES_PER_ELEMENT); // 每個整數(shù) 4 個字節(jié)

alert(view.length); // 4,它存儲了 4 個整數(shù)
alert(view.byteLength); // 16,字節(jié)中的大小

// 讓我們寫入一個值
view[0] = 123456;

// 遍歷值
for(let num of view) {
  alert(num); // 123456,然后 0,0,0(一共 4 個值)
}

TypedArray

所有這些視圖(Uint8Array,Uint32Array 等)的通用術(shù)語是 TypedArray。它們共享同一方法和屬性集。

請注意,沒有名為 TypedArray 的構(gòu)造器,它只是表示 ArrayBuffer 上的視圖之一的通用總稱術(shù)語:Int8Array,Uint8Array 及其他,很快就會有完整列表。

當(dāng)你看到 new TypedArray 之類的內(nèi)容時,它表示 new Int8Array、new Uint8Array 及其他中之一。

類型化數(shù)組的行為類似于常規(guī)數(shù)組:具有索引,并且是可迭代的。

一個類型化數(shù)組的構(gòu)造器(無論是 Int8Array 或 Float64Array,都無關(guān)緊要),其行為各不相同,并且取決于參數(shù)類型。

參數(shù)有 5 種變體:

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
  1. 如果給定的是 ArrayBuffer 參數(shù),則會在其上創(chuàng)建視圖。我們已經(jīng)用過該語法了。
  2. 可選,我們可以給定起始位置 byteOffset(默認(rèn)為 0)以及 length(默認(rèn)至 buffer 的末尾),這樣視圖將僅涵蓋 buffer 的一部分。

  3. 如果給定的是 Array,或任何類數(shù)組對象,則會創(chuàng)建一個相同長度的類型化數(shù)組,并復(fù)制其內(nèi)容。
  4. 我們可以使用它來預(yù)填充數(shù)組的數(shù)據(jù):

    let arr = new Uint8Array([0, 1, 2, 3]);
    alert( arr.length ); // 4,創(chuàng)建了相同長度的二進(jìn)制數(shù)組
    alert( arr[1] ); // 1,用給定值填充了 4 個字節(jié)(無符號 8 位整數(shù))
  5. 如果給定的是另一個 TypedArray,也是如此:創(chuàng)建一個相同長度的類型化數(shù)組,并復(fù)制其內(nèi)容。如果需要的話,數(shù)據(jù)在此過程中會被轉(zhuǎn)換為新的類型。
  6. let arr16 = new Uint16Array([1, 1000]);
    let arr8 = new Uint8Array(arr16);
    alert( arr8[0] ); // 1
    alert( arr8[1] ); // 232,試圖復(fù)制 1000,但無法將 1000 放進(jìn) 8 位字節(jié)中(詳述見下文)。
  7. 對于數(shù)字參數(shù) length —— 創(chuàng)建類型化數(shù)組以包含這么多元素。它的字節(jié)長度將是 length 乘以單個 TypedArray.BYTES_PER_ELEMENT 中的字節(jié)數(shù):
  8. let arr = new Uint16Array(4); // 為 4 個整數(shù)創(chuàng)建類型化數(shù)組
    alert( Uint16Array.BYTES_PER_ELEMENT ); // 每個整數(shù) 2 個字節(jié)
    alert( arr.byteLength ); // 8(字節(jié)中的大小)
  9. 不帶參數(shù)的情況下,創(chuàng)建長度為零的類型化數(shù)組。

我們可以直接創(chuàng)建一個 TypedArray,而無需提及 ArrayBuffer。但是,視圖離不開底層的 ArrayBuffer,因此,除第一種情況(已提供 ArrayBuffer)外,其他所有情況都會自動創(chuàng)建 ArrayBuffer。

如要訪問底層的 ArrayBuffer,那么在 TypedArray 中有如下的屬性:

  • ?arr.buffer? —— 引用 ?ArrayBuffer?。
  • ?arr.byteLength? —— ?ArrayBuffer? 的長度。

因此,我們總是可以從一個視圖轉(zhuǎn)到另一個視圖:

let arr8 = new Uint8Array([0, 1, 2, 3]);

// 同一數(shù)據(jù)的另一個視圖
let arr16 = new Uint16Array(arr8.buffer);

下面是類型化數(shù)組的列表:

  • ?Uint8Array?,?Uint16Array?,?Uint32Array? —— 用于 8、16 和 32 位的整數(shù)。
    • ?Uint8ClampedArray? —— 用于 8 位整數(shù),在賦值時便“固定“其值(見下文)。
  • ?Int8Array?,?Int16Array?,?Int32Array? —— 用于有符號整數(shù)(可以為負(fù)數(shù))。
  • ?Float32Array?,?Float64Array? —— 用于 32 位和 64 位的有符號浮點(diǎn)數(shù)。

沒有 ?int8? 或類似的單值類型

請注意,盡管有類似 Int8Array 這樣的名稱,但 JavaScript 中并沒有像 int,或 int8 這樣的單值類型。

這是合乎邏輯的,因?yàn)?nbsp;Int8Array 不是這些單值的數(shù)組,而是 ArrayBuffer 上的視圖。

越界行為

如果我們嘗試將越界值寫入類型化數(shù)組會出現(xiàn)什么情況?不會報錯。但是多余的位被切除。

例如,我們嘗試將 256 放入 Uint8Array。256 的二進(jìn)制格式是 100000000(9 位),但 Uint8Array 每個值只有 8 位,因此可用范圍為 0 到 255。

對于更大的數(shù)字,僅存儲最右邊的(低位有效)8 位,其余部分被切除:


因此結(jié)果是 0。

257 的二進(jìn)制格式是 100000001(9 位),最右邊的 8 位會被存儲,因此數(shù)組中會有 1


換句話說,該數(shù)字對 28 取模的結(jié)果被保存了下來。

示例如下:

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000(二進(jìn)制表示)

uint8array[0] = 256;
uint8array[1] = 257;

alert(uint8array[0]); // 0
alert(uint8array[1]); // 1

Uint8ClampedArray 在這方面比較特殊,它的表現(xiàn)不太一樣。對于大于 255 的任何數(shù)字,它將保存為 255,對于任何負(fù)數(shù),它將保存為 0。此行為對于圖像處理很有用。

TypedArray 方法

TypedArray 具有常規(guī)的 Array 方法,但有個明顯的例外。

我們可以遍歷(iterate),mapslice,find 和 reduce 等。

但有幾件事我們做不了:

  • 沒有 ?splice? —— 我們無法“刪除”一個值,因?yàn)轭愋突瘮?shù)組是緩沖區(qū)(buffer)上的視圖,并且緩沖區(qū)(buffer)是固定的、連續(xù)的內(nèi)存區(qū)域。我們所能做的就是分配一個零值。
  • 無 ?concat? 方法。

還有兩種其他方法:

  • ?arr.set(fromArr, [offset])? 從 ?offset?(默認(rèn)為 0)開始,將 ?fromArr? 中的所有元素復(fù)制到 ?arr?。
  • ?arr.subarray([begin, end])? 創(chuàng)建一個從 ?begin? 到 ?end?(不包括)相同類型的新視圖。這類似于 ?slice? 方法(同樣也支持),但不復(fù)制任何內(nèi)容 —— 只是創(chuàng)建一個新視圖,以對給定片段的數(shù)據(jù)進(jìn)行操作。

有了這些方法,我們可以復(fù)制、混合類型化數(shù)組,從現(xiàn)有數(shù)組創(chuàng)建新數(shù)組等。

DataView

DataView 是在 ArrayBuffer 上的一種特殊的超靈活“未類型化”視圖。它允許以任何格式訪問任何偏移量(offset)的數(shù)據(jù)。

  • 對于類型化的數(shù)組,構(gòu)造器決定了其格式。整個數(shù)組應(yīng)該是統(tǒng)一的。第 i 個數(shù)字是 ?arr[i]?。
  • 通過 ?DataView?,我們可以使用 ?.getUint8(i)? 或 ?.getUint16(i)? 之類的方法訪問數(shù)據(jù)。我們在調(diào)用方法時選擇格式,而不是在構(gòu)造的時候。

語法:

new DataView(buffer, [byteOffset], [byteLength])
  • ?buffer? —— 底層的 ?ArrayBuffer?。與類型化數(shù)組不同,?DataView? 不會自行創(chuàng)建緩沖區(qū)(buffer)。我們需要事先準(zhǔn)備好。
  • ?byteOffset? —— 視圖的起始字節(jié)位置(默認(rèn)為 0)。
  • ?byteLength? —— 視圖的字節(jié)長度(默認(rèn)至 ?buffer? 的末尾)。

例如,這里我們從同一個 buffer 中提取不同格式的數(shù)字:

// 4 個字節(jié)的二進(jìn)制數(shù)組,每個都是最大值 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// 在偏移量為 0 處獲取 8 位數(shù)字
alert( dataView.getUint8(0) ); // 255

// 現(xiàn)在在偏移量為 0 處獲取 16 位數(shù)字,它由 2 個字節(jié)組成,一起解析為 65535
alert( dataView.getUint16(0) ); // 65535(最大的 16 位無符號整數(shù))

// 在偏移量為 0 處獲取 32 位數(shù)字
alert( dataView.getUint32(0) ); // 4294967295(最大的 32 位無符號整數(shù))

dataView.setUint32(0, 0); // 將 4 個字節(jié)的數(shù)字設(shè)為 0,即將所有字節(jié)都設(shè)為 0

當(dāng)我們將混合格式的數(shù)據(jù)存儲在同一緩沖區(qū)(buffer)中時,DataView 非常有用。例如,當(dāng)我們存儲一個成對序列(16 位整數(shù),32 位浮點(diǎn)數(shù))時,用 DataView 可以輕松訪問它們。

總結(jié)

ArrayBuffer 是核心對象,是對固定長度的連續(xù)內(nèi)存區(qū)域的引用。

幾乎任何對 ArrayBuffer 的操作,都需要一個視圖。

  • 它可以是 ?TypedArray?:
    • ?Uint8Array?,?Uint16Array?,?Uint32Array? —— 用于 8 位、16 位和 32 位無符號整數(shù)。
    • ?Uint8ClampedArray? —— 用于 8 位整數(shù),在賦值時便“固定”其值。
    • ?Int8Array?,?Int16Array?,?Int32Array? —— 用于有符號整數(shù)(可以為負(fù)數(shù))。
    • ?Float32Array?,?Float64Array? —— 用于 32 位和 64 位的有符號浮點(diǎn)數(shù)。
  • 或 ?DataView? —— 使用方法來指定格式的視圖,例如,?getUint8(offset)?。

在大多數(shù)情況下,我們直接對類型化數(shù)組進(jìn)行創(chuàng)建和操作,而將 ArrayBuffer 作為“共同之處(common denominator)”隱藏起來。我們可以通過 .buffer 來訪問它,并在需要時創(chuàng)建另一個視圖。還有另外兩個術(shù)語,用于對二進(jìn)制數(shù)據(jù)進(jìn)行操作的方法的描述:

  • ?ArrayBufferView? 是所有這些視圖的總稱。
  • ?BufferSource? 是 ?ArrayBuffer? 或 ?ArrayBufferView? 的總稱。

我們將在下一章中學(xué)習(xí)這些術(shù)語。BufferSource 是最常用的術(shù)語之一,因?yàn)樗囊馑际恰叭魏晤愋偷亩M(jìn)制數(shù)據(jù)” —— ArrayBuffer 或其上的視圖。

這是一份備忘單:


任務(wù)


拼接類型化數(shù)組

給定一個 Uint8Array 數(shù)組,請寫一個函數(shù) concat(arrays),將數(shù)組拼接成一個單一數(shù)組并返回。

打開帶有測試的沙箱。


解決方案

function concat(arrays) {
  // sum of individual array lengths
  let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);

  let result = new Uint8Array(totalLength);

  if (!arrays.length) return result;

  // for each array - copy it over result
  // next array is copied right after the previous one
  let length = 0;
  for(let array of arrays) {
    result.set(array, length);
    length += array.length;
  }

  return result;
}

使用沙箱的測試功能打開解決方案。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號