20. 打字化陣列
目錄
請支持這本書:購買它(PDF、EPUB、MOBI)捐款
(廣告,請不要阻擋。)

20. 打字化陣列



20.1 概觀

打字化陣列是 ECMAScript 6 處理二進位資料的 API。

程式碼範例

const typedArray = new Uint8Array([0,1,2]);
console.log(typedArray.length); // 3
typedArray[0] = 5;
const normalArray = [...typedArray]; // [5,1,2]

// The elements are stored in typedArray.buffer.
// Get a different view on the same data:
const dataView = new DataView(typedArray.buffer);
console.log(dataView.getUint8(0)); // 5

ArrayBuffer 的實例儲存要處理的二進位資料。兩種「檢視」用於存取資料

下列瀏覽器 API 支援 Typed Arrays(詳細資訊請參閱專屬區段

20.2 簡介

在網路上遇到的許多資料都是文字:JSON 檔案、HTML 檔案、CSS 檔案、JavaScript 程式碼等。對於處理此類資料,JavaScript 內建的字串資料類型運作良好。然而,直到幾年前,JavaScript 對於處理二進位資料的配備仍不齊全。2011 年 2 月 8 日,Typed Array 規格 1.0 標準化了處理二進位資料的設施。到目前為止,Typed Arrays 已受到各種引擎的良好支援。透過 ECMAScript 6,它們成為核心語言的一部分,並在過程中獲得許多先前僅供陣列使用的函式(map()filter() 等)。

Typed Arrays 的主要使用案例為

在 Typed Array API 中,有兩種物件會共同運作

這是分組陣列 API 結構的圖表(注意:所有分組陣列都有共同的超類別)

20.2.1 元素類型

API 支援下列元素類型

元素類型 位元組 說明 C 類型
Int8 1 8 位元有號整數 signed char
Uint8 1 8 位元無號整數 unsigned char
Uint8C 1 8 位元無號整數(壓縮轉換) unsigned char
Int16 2 16 位元有號整數 short
Uint16 2 16 位元無號整數 unsigned short
Int32 4 32 位元有號整數 int
Uint32 4 32 位元無號整數 unsigned int
Float32 4 32 位元浮點數 float
Float64 8 64 位元浮點數 double

元素類型 Uint8C 很特別:DataView 不支援它,它只存在於啟用 Uint8ClampedArray。這個分組陣列由 canvas 元素使用(它取代了 CanvasPixelArray)。Uint8CUint8 之間唯一的差異是溢位和下溢的處理方式(如下一節所述)。建議避免使用前者 – 引述 Brendan Eich

為了非常清楚(而且我從它誕生時就在場),Uint8ClampedArray 完全 是歷史產物(HTML5 canvas 元素)。除非您真的在做 canvas 相關的事情,否則請避免使用它。

20.2.2 處理溢位和下溢

通常,當值超出元素類型的範圍時,會使用模數運算將其轉換為範圍內的數值。對於有號和無號整數,這表示

無號 8 位元整數的模數轉換

> const uint8 = new Uint8Array(1);
> uint8[0] = 255; uint8[0] // highest value within range
255
> uint8[0] = 256; uint8[0] // overflow
0
> uint8[0] = 0; uint8[0] // lowest value within range
0
> uint8[0] = -1; uint8[0] // underflow
255

有號 8 位元整數的模數轉換

> const int8 = new Int8Array(1);
> int8[0] = 127; int8[0] // highest value within range
127
> int8[0] = 128; int8[0] // overflow
-128
> int8[0] = -128; int8[0] // lowest value within range
-128
> int8[0] = -129; int8[0] // underflow
127

箝制轉換不同

> const uint8c = new Uint8ClampedArray(1);
> uint8c[0] = 255; uint8c[0] // highest value within range
255
> uint8c[0] = 256; uint8c[0] // overflow
255
> uint8c[0] = 0; uint8c[0] // lowest value within range
0
> uint8c[0] = -1; uint8c[0] // underflow
0

20.2.3 位元序

每當類型(例如 Uint16)儲存為多個位元組時,位元序很重要

位元序傾向於每個 CPU 架構固定,並且在原生 API 中保持一致。類型化陣列用於與這些 API 通訊,這就是為什麼它們的位元序遵循平台的位元序並且無法更改的原因。

另一方面,協定和二進位檔案的位元序會有所不同,並且在各平台間固定。因此,我們必須能夠以任一種位元序存取資料。DataView 服務此用例,並讓您在取得或設定值時指定位元序。

引用維基百科關於位元序:

您可以使用下列函數來判斷平台的位元序。

const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');
function getPlatformEndianness() {
    const arr32 = Uint32Array.of(0x12345678);
    const arr8 = new Uint8Array(arr32.buffer);
    switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8\
[3])) {
        case 0x12345678:
            return BIG_ENDIAN;
        case 0x78563412:
            return LITTLE_ENDIAN;
        default:
            throw new Error('Unknown endianness');
    }
}

還有一些平台會以與字元組內部位元組不同的位元序排列字元(成對的位元組)。這稱為混合位元序。如果您想支援這樣的平台,那麼很容易延伸先前的程式碼。

20.2.4 負數索引

使用方括號運算子 [ ],您只能使用非負數索引(從 0 開始)。ArrayBuffer、類型化陣列和 DataView 的方法工作方式不同:每個索引都可以為負數。如果是這樣,它會從長度向後計算。換句話說,它會加到長度以產生一個正常的索引。因此,-1 表示最後一個元素,-2 表示倒數第二個,依此類推。一般陣列的方法工作方式相同。

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

另一方面,偏移量必須是非負數。例如,如果您傳遞 -1

DataView.prototype.getInt8(byteOffset)

那麼您會得到一個 RangeError

20.3 ArrayBuffers

ArrayBuffers 儲存資料,檢視(類型化陣列和資料檢視)讓您可以讀取和變更它。為了建立資料檢視,您需要提供其建構函式一個 ArrayBuffer。類型化陣列建構函式可以選擇性地為您建立一個 ArrayBuffer。

20.3.1 ArrayBuffer 建構函式

建構函式的簽章是

ArrayBuffer(length : number)

透過 new 呼叫這個建構函式會建立一個容量為 length 位元組的執行個體。這些位元組中的每個最初都是 0。

20.3.2 靜態 ArrayBuffer 方法

20.3.3 ArrayBuffer.prototype 屬性

20.4 類型化陣列

各種類型的類型化陣列僅在其元素的類型方面有所不同

20.4.1 類型化陣列與一般陣列

類型化陣列很像一般陣列:它們有 length,元素可透過括號運算子 [ ] 存取,而且它們具有所有標準陣列方法。它們與陣列的差異如下

20.4.2 類型化陣列可迭代

類型化陣列實作一個其金鑰為 Symbol.iterator 的方法,因此可迭代(請參閱章節「可迭代物件和迭代器」以取得更多資訊)。這表示您可以在 ES6 中使用 for-of 迴圈和類似機制

const ui8 = Uint8Array.of(0,1,2);
for (const byte of ui8) {
    console.log(byte);
}
// Output:
// 0
// 1
// 2

ArrayBuffer 和 DataView 不可迭代。

20.4.3 將類型化陣列轉換為一般陣列,反之亦然

若要將一般陣列轉換為類型化陣列,請將其設為類型化陣列建構函式的參數。例如

> const tarr = new Uint8Array([0,1,2]);

將類型化陣列轉換為陣列的傳統方式是在其上呼叫 Array.prototype.slice。此技巧適用於所有類陣列物件(例如 arguments),而且類型化陣列是類陣列。

> Array.prototype.slice.call(tarr)
[ 0, 1, 2 ]

在 ES6 中,您可以使用展開運算子 (...),因為類型化陣列可迭代

> [...tarr]
[ 0, 1, 2 ]

另一種 ES6 替代方案是 Array.from(),它可與可迭代物件或類陣列物件搭配使用

> Array.from(tarr)
[ 0, 1, 2 ]

20.4.4 類型化陣列的 Species 模式

有些方法會建立與 this 類似的新的執行個體。Species 模式讓您可以設定應使用哪個建構函式來執行此動作。例如,如果您建立 Array 的子類別 MyArray,則預設值是 map() 會建立 MyArray 的執行個體。如果您想要建立 Array 的執行個體,您可以使用 Species 模式來達成此目的。詳細資訊說明於章節「Species 模式」中的類別章節。

ArrayBuffer 在以下位置使用 Species 模式

類型化陣列在以下位置使用 Species 模式

DataViews 不使用 species 模式。

20.4.5 Typed Array 的繼承層次

正如您在本章開頭的圖表中所見,所有 Typed Array 類別(Uint8Array 等)都有共同的超類別。我稱呼該超類別為 TypedArray,但它無法直接從 JavaScript 存取(ES6 規格稱之為內在物件 %TypedArray%)。TypedArray.prototype 包含 Typed Array 的所有方法。

20.4.6 靜態 TypedArray 方法

靜態 TypedArray 方法皆由其子類別繼承(Uint8Array 等)。

20.4.6.1 TypedArray.of()

此方法具有簽章

TypedArray.of(...items)

它會建立一個新的 Typed Array,為 this 的執行個體(呼叫 of() 的類別)。該執行個體的元素為 of() 的參數。

您可以將 of() 視為 Typed Array 的自訂文字

> Float32Array.of(0.151, -8, 3.7)
Float32Array [ 0.151, -8, 3.7 ]
20.4.6.2 TypedArray.from()

此方法具有簽章

TypedArray<U>.from(source : Iterable<T>, mapfn? : T => U, thisArg?)

它會將可迭代的 source 轉換為 this 的執行個體(Typed Array)。

例如,一般陣列可迭代,且可以使用此方法轉換

> Uint16Array.from([0, 1, 2])
Uint16Array [ 0, 1, 2 ]

Typed Array 也可迭代

> const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
> ui16 instanceof Uint16Array
true

選用的 mapfn 讓您可以在 source 的元素成為結果的元素之前轉換它們。為何要一次執行兩個步驟對應轉換?與透過 source.map() 分別執行第一步相比,有兩個優點

  1. 不需要中介陣列或 Typed Array。
  2. 在將 Typed Array 轉換為元素具有更高精度的 Typed Array 時,對應步驟可以使用該更高的精度。

為了說明第二個優點,我們使用 map() 將 Typed Array 的元素加倍

> Int8Array.of(127, 126, 125).map(x => 2 * x)
Int8Array [ -2, -4, -6 ]

正如您所見,值會溢位並轉換為 Int8 值範圍。如果透過 from() 對應,您可以選擇結果的類型,以避免值溢位

> Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
Int16Array [ 254, 252, 250 ]

根據 Allen Wirfs-Brock的說法,在 Typed Array 之間對應是 from()mapfn 參數的動機。

20.4.7 TypedArray.prototype 屬性

Typed Array 方法所接受的索引可以是負值(它們像傳統的 Array 方法那樣運作)。偏移量必須是非負值。有關詳細資訊,請參閱「負索引」一節。

20.4.7.1 特定於 Typed Array 的方法

下列屬性特定於 Typed Array,一般 Array 沒有這些屬性

20.4.7.2 Array 方法

下列方法基本上與一般 Array 的方法相同

由於陣列可以使用所有這些方法,因此你可以參閱以下兩個來源,以進一步了解它們的工作方式

請注意,雖然一般陣列方法是泛型的(任何類陣列的 this 都可以),但本節列出的方法並非如此(this 必須是 Typed Array)。

20.4.8 «ElementType»Array 建構函式

每個 Typed Array 建構函式都有遵循 «ElementType»Array 樣式的名稱,其中 «ElementType» 是開頭表格中的元素類型之一。這表示 Typed Array 有 9 個建構函式:Int8ArrayUint8ArrayUint8ClampedArray(元素類型 Uint8C)、Int16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64Array

每個建構函式有五個重載版本 – 它的行為會根據接收到的引數數量及其類型而有所不同

以下程式碼顯示了建立相同 Typed Array 的三種不同方式

const tarr1 = new Uint8Array([1,2,3]);

const tarr2 = Uint8Array.of(1,2,3);

const tarr3 = new Uint8Array(3);
tarr3[0] = 0;
tarr3[1] = 1;
tarr3[2] = 2;

20.4.9 靜態 «ElementType»Array 屬性

20.4.10 «ElementType»Array.prototype 屬性

20.4.11 串接 Typed Arrays

Typed Arrays 沒有像一般 Arrays 的 concat() 方法。解決方法是使用

typedArray.set(arrayOrTypedArray, offset=0)

此方法會將現有的 Typed Array (或一般 Array) 複製到 typedArray 中的索引 offset。然後你只需要確保 typedArray 足夠大,可以容納所有你想要串接的 (Typed) Arrays

function concatenate(resultConstructor, ...arrays) {
    let totalLength = 0;
    for (const arr of arrays) {
        totalLength += arr.length;
    }
    const result = new resultConstructor(totalLength);
    let offset = 0;
    for (const arr of arrays) {
        result.set(arr, offset);
        offset += arr.length;
    }
    return result;
}
console.log(concatenate(Uint8Array,
    Uint8Array.of(1, 2), Uint8Array.of(3, 4)));
        // Uint8Array [1, 2, 3, 4]

20.5 DataViews

20.5.1 DataView 建構函式

20.5.2 DataView.prototype 屬性

20.6 支援 Typed Array 的瀏覽器 API

Typed Array 已經存在一段時間了,因此有許多瀏覽器 API 支援它們。

20.6.1 檔案 API

檔案 API 讓您可以存取本機檔案。以下程式碼示範如何取得已提交本機檔案的位元組,並儲存在 ArrayBuffer 中。

const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
    const arrayBuffer = reader.result;
    ···
};

20.6.2 XMLHttpRequest

在較新的 XMLHttpRequest API 版本中,您可以讓結果傳遞到 ArrayBuffer

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

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

xhr.send();

20.6.3 Fetch API

XMLHttpRequest 類似,Fetch API 讓您可以要求資源。但它是基於 Promise,這讓它更方便使用。以下程式碼示範如何將 url 指向的內容下載為 ArrayBuffer

fetch(url)
.then(request => request.arrayBuffer())
.then(arrayBuffer => ···);

20.6.4 Canvas

引用 HTML5 規格:

canvas 元素提供具解析度依賴性位圖畫布的指令碼,可立即用於繪製圖表、遊戲圖形、藝術或其他視覺影像。

canvas 的 2D Context 讓您可以將位圖資料擷取為 Uint8ClampedArray 的執行個體

const canvas = document.getElementById('my_canvas');
const context = canvas.getContext('2d');
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;

20.6.5 WebSockets

WebSockets 讓您可以透過 ArrayBuffers 傳送和接收二進位資料

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

// Wait until socket is open
socket.addEventListener('open', function (event) {
    // Send binary data
    const typedArray = new Uint8Array(4);
    socket.send(typedArray.buffer);
});

// Receive binary data
socket.addEventListener('message', function (event) {
    const arrayBuffer = event.data;
    ···
});

20.6.6 其他 API

20.7 進階範例:JPEG SOF0 解碼器

此範例是一個網頁,讓你可以上傳 JPEG 檔案,並解析其結構以判斷影像的高度和寬度,以及更多資訊。

20.7.1 JPEG 檔案格式

JPEG 檔案是一連串的區段(類型化資料)。每個區段都從以下四個位元組開始

JPEG 檔案在所有平台上都是大端序。因此,此範例說明了在使用 DataViews 時,我們能夠指定端序的重要性。

20.7.2 JavaScript 程式碼

以下函式 processArrayBuffer() 是實際程式碼的縮寫版本;我已移除一些錯誤檢查以減少雜訊。processArrayBuffer() 會接收一個包含已提交 JPEG 檔案內容的 ArrayBuffer,並反覆處理其區段。

// JPEG is big endian
var IS_LITTLE_ENDIAN = false;

function processArrayBuffer(arrayBuffer) {
    try {
        var dv = new DataView(arrayBuffer);
        ···
        var ptr = 2;
        while (true) {
            ···
            var lastPtr = ptr;
            enforceValue(0xFF, dv.getUint8(ptr),
                'Not a marker');
            ptr++;
            var marker = dv.getUint8(ptr);
            ptr++;
            var len = dv.getUint16(ptr, IS_LITTLE_ENDIAN);
            ptr += len;
            logInfo('Marker: '+hex(marker)+' ('+len+' byte(s))');
            ···

            // Did we find what we were looking for?
            if (marker === 0xC0) { // SOF0
                logInfo(decodeSOF0(dv, lastPtr));
                break;
            }
        }
    } catch (e) {
        logError(e.message);
    }
}

此程式碼使用以下輔助函式(未在此處顯示)

decodeSOF0() 會解析區段 SOF0

function decodeSOF0(dv, start) {
    // Example (16x16):
    // FF C0 00 11 08 00 10 00 10 03 01 22 00 02 11 01 03 11 01
    var data = {};
    start += 4; // skip marker 0xFFC0 and segment length 0x0011
    var data = {
        bitsPerColorComponent: dv.getUint8(start), // usually 0x08
        imageHeight: dv.getUint16(start+1, IS_LITTLE_ENDIAN),
        imageWidth: dv.getUint16(start+3, IS_LITTLE_ENDIAN),
        numberOfColorComponents: dv.getUint8(start+5),
    };
    return JSON.stringify(data, null, 4);
}

有關 JPEG 檔案結構的更多資訊

20.8 可用性

大部分的 Typed Array API 已由所有現代 JavaScript 引擎實作,但有幾個功能是 ECMAScript 6 的新功能

可能需要一段時間才能在各處使用這些功能。與往常一樣,kangax 的「ES6 相容性表」說明了現狀。

下一頁:21. 可迭代和迭代器