搞懂JavaScript類型化數組

簡介

所謂類型化數組就是一種類似數組的對象,它提供了一種用於訪問原始二進制數據的機制,WebGL中經常會用到,並且必須用到,但這並不是說它的出現只是因爲WebGL,隨着Web程序的越來強大,類型化數組還可以用在很多地方,讓開發者可以方便的操作內存

首先來看下爲什麼會有類型化數組這個東西,JS已經有了Array對象爲什麼還有搞出來這樣一個麻煩的東西,存在既有其合理性

  • 瀏覽器通過WebGL與顯卡進行通信,因此對性能要求較高,傳統的Array動態數組無法滿足
  • 傳統的Array可以存儲任何類型的值,因此無法直接操作內存確定數據所佔字節大小
  • 有一些程序數據的交互通過二進制數據會更加快速,比如上面的顯卡通信以及webSockets

緩衝

何爲緩衝

首先,需要了解以下什麼是緩衝區

緩衝區又稱爲緩存,它是內存空間的一部分,也就是說,在內存空間中預留了一定的存儲空間,這些存儲空間用來緩衝輸入或輸出的數據,緩衝區根據其對應的設備是輸入還是輸出,又分爲輸入緩衝區和輸出緩衝區。

例如在C語言中的內存管理機制,有兩個標準的內存管理函數:malloc()free(),用來申請固定大小的內存和動態釋放內存空間,而在JavaScript中內存的管理是由瀏覽器來控制的,當創建變量時內存會被分配,使用變量時內存會被讀寫,然後當這些變量不再使用的時候內存會被回收,這就是JavaScript的垃圾自動回收機制。

緩衝的作用

然後,看下爲什麼需要緩衝區

瞭解了緩衝區是內存空間的一部分,它等待着數據的存取,那爲什麼不直接進行數據的存取還費那麼大勁申請一塊緩衝區再存取數據,因爲緩衝區相當於一種媒介,介於輸入和輸出之間,就比如自動售貨機,你買東西不需要要直接面對商家,即方便了你也方便了商家,並且你可以在售貨機買到各種類型的東西,因此緩衝區它不是一種數據類型,在緩衝區這段內存區域中,可以放置整型數和浮點數以及別的類型

因此,JavaScript提供了一種可以緩衝區的類型,Arraybuffer,當然這裏的類型並不是類型化數組

ArrayBuffer

爲了達到最大的靈活性和效率,類型數組將實現拆分爲 緩衝視圖兩部分,一個緩衝描述的是一個數據塊,緩衝沒有格式可言,並且不提供機制訪問內容

接口定義

ArrayBuffer是一種數據類型,用來表示一個通用的、固定長度的二進制數據緩衝區,下面的代碼是其接口定義

interface ArrayBuffer {
    /**
     * Read-only. The length of the ArrayBuffer (in bytes).
     */
    readonly byteLength: number;

    /**
     * Returns a section of an ArrayBuffer.
     */
    slice(begin: number, end?: number): ArrayBuffer;
}

由此可見ArrayBuffer接口有一個只讀屬性和一個實例方法

  • byteLength:數組的字節大小。在數組創建時確定,並且不可變更。只讀
  • slice:返回一個新的 ArrayBuffer ,它的內容是這個 ArrayBuffer 的字節副本,從begin(包括),到end(不包括)。如果begin或end是負數,則指的是從數組末尾開始的索引,而不是從頭開始

靜態方法

ArrayBuffer還有一個靜態的方法,其構造函數定義如下

interface ArrayBufferConstructor {
    readonly prototype: ArrayBuffer;
    new(byteLength: number): ArrayBuffer;
    isView(arg: any): arg is ArrayBufferView;
}
  • isView:如果參數是 ArrayBuffer視圖實例則返回 true,否則返回false,至於什麼是視圖實例後面會講解

下面來使用一下ArrayBuffer

// 創建8個字節的緩衝區
let buffer = new ArrayBuffer(8);
console.log(buffer.byteLength); // 返回字節長度8

let buffer2 = buffer.slice(0, 4);
console.log(buffer2.byteLength); // 4個字節數

上面創建一個8字節的內存緩衝區,並且通過byteLength獲取字節長度

每個字節的默認值都是0,1字節等於8比特,1比特就是一個二進制位(0或1)
在這裏插入圖片描述

DataView

上面創建了一個8字節的二進制緩衝區域,要想操作這塊區域,ArrayBuffers不能直接讀寫,但可以將其傳遞給一個 類型化數組 TypedArrayDataView對象,進而操作緩衝區
首先,我們來看一下 DataView

DataVIew是一個可以從 二進制ArrayBuffer 對象中讀寫多種數值類型的底層接口,使用它時,不用考慮不同平臺的字節序 的問題。

接口定義

看一下其接口定義

interface DataView {
    readonly buffer: ArrayBuffer;
    readonly byteLength: number;
    readonly byteOffset: number;

    getFloat32(byteOffset: number, littleEndian?: boolean): number;
    getFloat64(byteOffset: number, littleEndian?: boolean): number;
    
    getInt8(byteOffset: number): number;
    getInt16(byteOffset: number, littleEndian?: boolean): number;
    getInt32(byteOffset: number, littleEndian?: boolean): number;
    
    getUint8(byteOffset: number): number;
    getUint16(byteOffset: number, littleEndian?: boolean): number;
    getUint32(byteOffset: number, littleEndian?: boolean): number;
    
    setFloat32(byteOffset: number, value: number, littleEndian?: boolean): void;
    setFloat64(byteOffset: number, value: number, littleEndian?: boolean): void;
    
    setInt8(byteOffset: number, value: number): void;
    setInt16(byteOffset: number, value: number, littleEndian?: boolean): void;
    setInt32(byteOffset: number, value: number, littleEndian?: boolean): void;
    
    setUint8(byteOffset: number, value: number): void;
    setUint16(byteOffset: number, value: number, littleEndian?: boolean): void;
    setUint32(byteOffset: number, value: number, littleEndian?: boolean): void;
}

實例屬性

實例屬性

  • buffer:類型是 ArrayBuffer,也就是被view引入的buffer只讀屬性
  • byteLength:從 ArrayBuffer中讀取的字節長度
  • byteOffset:從 ArrayBuffer讀取時的偏移字節長度

實例方法

實例方法
各個類型的set/get方法,表示從DataView起始位置以byte爲計數的指定偏移量(byteOffset)處獲取一個多少-bit數(類型).具體的類型和字節數如下表

視圖類型 說明 字節大小
Uint8Array 8位無符號整數 1字節
Int8Array 8位有符號整數 1字節
Uint8ClampedArray 8位無符號整數(溢出處理不同) 1字節
Uint16Array 16位無符號整數 2字節
Int16Array 16位有符號整數 2字節
Uint32Array 32位無符號整數 4字節
Int32Array 32位有符號整數 4字節
Float32Array 32位IEEE浮點數 4字節
Float64Array 64位IEEE浮點數 8字節

然後再看一下如果創建view對象,其構造函數定義如下

interface DataViewConstructor {
    new(buffer: ArrayBufferLike, byteOffset?: number, byteLength?: number): DataView;
}

需要傳遞三個參數,後面兩個是可選參數,參數詳解

  • buffer:一個 已經創建的ArrayBuffer也就是DataVIew的數據源
  • byteOffset:此 DataView 對象的第一個字節在 buffer 中的字節偏移。如果未指定,則默認從第一個字節開始
  • byteLength:此 DataView 對象的字節長度。如果未指定,這個視圖的長度將匹配buffer的長度

代碼示例

具體如果使用 DataView來看一下示例代碼

// 創建8個字節的緩衝區
let buffer = new ArrayBuffer(8);

// 創建一個視圖,從第一個字節開始到buffer的長度爲止,通過視圖可以操作緩衝區
let view = new DataView(buffer);

view.setUint8(0, 31);// 在 0 偏移位置存儲一個 8 位無符號整數31 佔一個字節
view.setUint8(1, 32);// 在 1 偏移位置存儲一個 8 位無符號整數31 佔一個字節

view.setInt16(2, 67);// 在 2 偏移位置也就是第3個字節開始 存儲一個 16位有符號整數 佔兩個字節

view.setFloat32(4, 12.5)// 在 4 偏移位置存儲一個有符號32位浮點數佔 4 個字節

console.log(view.getUint16(2)); // 67
console.log(view.getUint8(0));  // 31

由上面的代碼可以看到,通過DataView可以操作緩衝區,並且可以設置不同類型的數,在第一個字節和第二個字節處設置了無符號8位的整數,每個數佔一個字節,然後第3個位置設置了一個有符號整數,一個有符號整數佔兩個字節,因此最後還有4個字節沒有使用,所以最後設置了一個4字節的有符號浮點數,此時這塊緩衝區已經填滿,可以通過get方法來獲取對應字節位置的數

TypedArray

操作二進制緩衝區,不僅可以使用DataView對象同時最好用也是最長用的方式就是類型化數組,基本上在WebGL中使用32位浮點數來表示頂點的屬性信息,使用無符8位整數來表示顏色值在0到255之間,使用適合的數據類型可以優化內存提升渲染速度

取值範圍

類型 單個元素值的範圍 大小(bytes) 描述 Web IDL 類型 C 語言中的等價類型
Int8Array -128 to 127 1 8 位二進制有符號整數 byte int8_t
Uint8Array 0 to 255 1 8 位無符號整數(超出範圍後從另一邊界循環) octet uint8_t
Uint8ClampedArray 0 to 255 1 8 位無符號整數(超出範圍後爲邊界值) octet uint8_t
Int16Array -32768 to 32767 2 16 位二進制有符號整數 short int16_t
Uint16Array 0 to 65535 2 16 位無符號整數 unsigned short uint16_t
Int32Array -2147483648 to 2147483647 4 32 位二進制有符號整數 long int32_t
Uint32Array 0 to 4294967295 4 32 位無符號整數 unsigned long uint32_t
Float32Array 1.2×10-38 to 3.4×1038 4 32 位 IEEE 浮點數(7 位有效數字,如 1.1234567 unrestricted float float
Float64Array 5.0×10-324 to 1.8×10308 8 64 位 IEEE 浮點數(16 有效數字,如 1.123...15) unrestricted double double
BigInt64Array -263 to 263-1 8 64 位二進制有符號整數 bigint int64_t (signed long long)
BigUint64Array 0 to 264-1 8 64 位無符號整數 bigint uint64_t (unsigned long long)

一個類型化數組對象描述了一個底層的二進制數據緩衝區的一個類數組視圖,事實上,沒有名爲 TypedArray的全局屬性,也沒有一個名爲TypedArray的構造函數,相反,有許多不同的全局屬性,它們的值使特定元素類型的類型化數組的構造函數

接口定義

例如下面代碼Float32Array構造方法

interface Float32ArrayConstructor {
    readonly prototype: Float32Array;
    new(length: number): Float32Array;
    new(arrayOrArrayBuffer: ArrayLike<number> | ArrayBufferLike): Float32Array;
    new(buffer: ArrayBufferLike, byteOffset: number, length?: number): Float32Array;

    /**
     * The size in bytes of each element in the array.
     */
    readonly BYTES_PER_ELEMENT: number;

    /**
     * Returns a new array from a set of elements.
     * @param items A set of elements to include in the new array object.
     */
    of(...items: number[]): Float32Array;

    /**
     * Creates an array from an array-like or iterable object.
     * @param arrayLike An array-like or iterable object to convert to an array.
     */
    from(arrayLike: ArrayLike<number>): Float32Array;

    /**
     * Creates an array from an array-like or iterable object.
     * @param arrayLike An array-like or iterable object to convert to an array.
     * @param mapfn A mapping function to call on every element of the array.
     * @param thisArg Value of 'this' used to invoke the mapfn.
     */
    from<T>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => number, thisArg?: any): Float32Array;

}
declare var Float32Array: Float32ArrayConstructor;

創建方式

由上面構造函數接口的定義可知,Float32Array數組的構造可以有4種方式,下面示例代碼所示

方式一

/**
  * 1. new(length: number): Float32Array; 傳遞一個 length
  * 創建一個可以存儲8個浮點數的類型化數組
  * 當傳入length時,一個內部的 buffer 會被創建,該緩衝區的大小
  * 是傳入的length乘以數組中每個元素的字節數,並且每個元素的值都是0
  */
 let float32Array = new Float32Array(8);

 console.log(float32Array.BYTES_PER_ELEMENT);// 4每個數所佔的字節
 console.log(float32Array.byteLength);       // 總字節數 4 * 8 = 64
 console.log(float32Array.length);           // 數組中元素的數量
 console.log(float32Array.byteOffset);       // 0

方式二

/**
  * 2. new(arrayOrArrayBuffer: ArrayLike<number>): Float32Array;
  * 第二種方法是傳遞一個普通的數組,然後通過這個數組來創建實例
  * 此時類型化數組中的值是已經賦值過的
  */

 let vertices = [1.0, 1.0, 1.0, 2.0, 2.0, 2.0];

 let float32Array = new Float32Array(vertices);

 console.log(float32Array.BYTES_PER_ELEMENT);// 4每個數所佔的字節
 console.log(float32Array.byteLength);       // 總字節數 4 * 6 = 24
 console.log(float32Array.length);           // 數組中元素的數量 6
 console.log(float32Array.byteOffset);       // 0

方式三

/**
  * 3. new(ArrayBufferLike): Float32Array;
  * 第三種方法是傳遞一個緩衝區對象
  * 此時類型化數組中的元素都是0
  */

 // 創建8個字節的緩衝區
 let buffer = new ArrayBuffer(24);

 let float32Array = new Float32Array(buffer);

 console.log(float32Array.BYTES_PER_ELEMENT);// 4每個數所佔的字節
 console.log(float32Array.byteLength);       // 總字節數 4 * 6 = 24
 console.log(float32Array.length);           // 數組中元素的數量 6
 console.log(float32Array.byteOffset);       // 0

方式四

/**
  * 4. new(TypeArray): Float32Array;
  * 第四種方法是傳遞一個類型化數組,然後通過這個數組來創建實例
  * 傳入一個任意類型的類型化數組會被轉換位當前構造函數對應的類型
  * 如果當前數的範圍超過此構造函數的範圍則超出範圍被截取
  */

 let vertices = [1.0, 1.0, 1.0, 2.0, 2.0, 256];

 let float32Array = new Float32Array(vertices);

 console.log(float32Array.BYTES_PER_ELEMENT);// 每個數所佔的字節  4
 console.log(float32Array.byteLength);       // 總字節數 4 * 6 = 24
 console.log(float32Array.length);           // 數組中元素的數量  6
 console.log(float32Array.byteOffset);       // 0


 let uint8Array = new Uint8Array(float32Array);
 console.log(uint8Array.BYTES_PER_ELEMENT);// 每個數所佔的字節 1
 console.log(uint8Array.byteLength);       // 總字節數 1 * 6 = 6
 console.log(uint8Array.length);           // 數組中元素的數量 6
 console.log(uint8Array.byteOffset);       // 0

 console.log(float32Array);  // [1.0, 1.0, 1.0, 2.0, 2.0, 256]
 console.log(uint8Array);    // [1.0, 1.0, 1.0, 2.0, 2.0, 0]

方式五

/**
  * 5. new(buffer: ArrayBufferLike, byteOffset: number, length?: number): Float32Array;
  * 第五種方法和方式三類似只是多後面兩個參數,傳遞一個緩衝區對象
  *  byteOffset和length(可選)參數,指定了類型化數組將要暴露的內存範圍,length可以省略
  * length省略則默認到可支持大小
  */

 // 創建8個字節的緩衝區
 let buffer = new ArrayBuffer(24);

 let float32Array = new Float32Array(buffer, 2 * 4, 2);

 console.log(float32Array.BYTES_PER_ELEMENT);// 每個數所佔的字節 4
 console.log(float32Array.byteLength);       // 總字節數 4 * 2 = 2
 console.log(float32Array.length);           // 數組中元素的數量 2
 console.log(float32Array.byteOffset);       // 8 第八個字節開始偏移
 console.log(float32Array);                  //[0,0]

實例特性

不能動態增長
上面是構造類型化數組的幾種方式,需要注意的是,類型化數組不能向普通數組 Array一樣動態的增長,一旦設置好了長度以及大小,裏面緩衝區的大小則不會動態的改變,看下面的示例

let vertices = [1.0, 2.0, 3.0];

 let float32Array = new Float32Array(vertices);

 console.log(float32Array); // [1.0, 2.0, 3.0]

 // 增加數組元素
 vertices[vertices.length] = 44;
 vertices[vertices.length] = 55;
 console.log(vertices);// [1, 2, 3, 44, 55]
 // 類型化數組中的數據並沒有增加
 console.log(float32Array);        // [1.0, 2.0, 3.0]
 console.log(float32Array.length); // 3
 float32Array[3] = 44;
 // 依然無法添加
 console.log(float32Array);        // [1.0, 2.0, 3.0]

屬性訪問
可以像一般數組一樣來訪問類型化數組中的元素

let vertices = [1.0, 2.0, 3.0];
let float32Array = new Float32Array(vertices);

console.log(float32Array);                      // [1.0, 2.0, 3.0]
console.log(float32Array[0]);                   // 1
float32Array[float32Array.length] = 4;
console.log(float32Array[float32Array.length]); // undefined

可以給動態數組添加屬性

let vertices = [1.0, 2.0, 3.0];
let float32Array = new Float32Array(vertices);

console.log(float32Array);                      // [1.0, 2.0, 3.0]
float32Array.name = 'float32Array';
console.log(float32Array.name);// float32Array

靜態屬性

通過構造函數可以得知其靜態屬性和方法如下
靜態屬性

readonly BYTES_PER_ELEMENT: number;
name:string = "Float32Array";
readonly length:number = 3;

靜態方法

of(...items: number[]): Float32Array;

// 使用類數組(array-like)或迭代對象創建一個新的類型化數組
from(arrayLike: ArrayLike<number>): Float32Array;
// 使用類數組(array-like)或迭代對象創建一個新的類型化數組
from<T>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => number, thisArg?: any): Float32Array;

實例方法

類型化數組的實例方法大部分都與基本的Array類似,但是也有一定的區別,具體實例方法如下代碼所示

interface Float32Array {
   
    readonly BYTES_PER_ELEMENT: number;
    readonly buffer: ArrayBufferLike;
    readonly byteLength: number;
    readonly byteOffset: number;
    
    copyWithin(target: number, start: number, end?: number): this;
    every(callbackfn: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): boolean;
    fill(value: number, start?: number, end?: number): this;
    filter(callbackfn: (value: number, index: number, array: Float32Array) => any, thisArg?: any): Float32Array;
    find(predicate: (value: number, index: number, obj: Float32Array) => boolean, thisArg?: any): number | undefined;
    findIndex(predicate: (value: number, index: number, obj: Float32Array) => boolean, thisArg?: any): number;
    forEach(callbackfn: (value: number, index: number, array: Float32Array) => void, thisArg?: any): void;
    indexOf(searchElement: number, fromIndex?: number): number;
    join(separator?: string): string;
    lastIndexOf(searchElement: number, fromIndex?: number): number;

    /**
     * The length of the array.
     */
    readonly length: number;
    
    map(callbackfn: (value: number, index: number, array: Float32Array) => number, thisArg?: any): Float32Array;
    reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number): number;
    reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number, initialValue: number): number;
    reduce<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Float32Array) => U, initialValue: U): U;
    reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number): number;
    reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: Float32Array) => number, initialValue: number): number;
    reduceRight<U>(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: Float32Array) => U, initialValue: U): U;
    reverse(): Float32Array;
    set(array: ArrayLike<number>, offset?: number): void;
    slice(start?: number, end?: number): Float32Array;
    some(callbackfn: (value: number, index: number, array: Float32Array) => unknown, thisArg?: any): boolean;
    sort(compareFn?: (a: number, b: number) => number): this;
    subarray(begin?: number, end?: number): Float32Array;
    toLocaleString(): string;
    toString(): string;

    [index: number]: number;
}

具體使用方法 查看 API文檔 TypedArray

實際案例

一般在WebGL中使用類型化數組來向顯卡傳遞頂點的屬性數據,例如下面這個示例,在緩衝區中創建頂點的座標和顏色以及索引數據等

let verticesColors = new Float32Array([
    // Three triangles on the right side
    0.75, 1.0, -4.0, 0.4, 1.0, 0.4, // The back green one
    0.25, -1.0, -4.0, 0.4, 1.0, 0.4,
    1.25, -1.0, -4.0, 1.0, 0.4, 0.4,

    0.75, 1.0, -2.0, 1.0, 1.0, 0.4, // The middle yellow one
    0.25, -1.0, -2.0, 1.0, 1.0, 0.4,
    1.25, -1.0, -2.0, 1.0, 0.4, 0.4,

    0.75, 1.0, 0.0, 0.4, 0.4, 1.0,  // The front blue one 
    0.25, -1.0, 0.0, 0.4, 0.4, 1.0,
    1.25, -1.0, 0.0, 1.0, 0.4, 0.4,

    // Three triangles on the left side
    -0.75, 1.0, -4.0, 0.4, 1.0, 0.4, // The back green one
    -1.25, -1.0, -4.0, 0.4, 1.0, 0.4,
    -0.25, -1.0, -4.0, 1.0, 0.4, 0.4,

    -0.75, 1.0, -2.0, 1.0, 1.0, 0.4, // The middle yellow one
    -1.25, -1.0, -2.0, 1.0, 1.0, 0.4,
    -0.25, -1.0, -2.0, 1.0, 0.4, 0.4,

    -0.75, 1.0, 0.0, 0.4, 0.4, 1.0,  // The front blue one 
    -1.25, -1.0, 0.0, 0.4, 0.4, 1.0,
    -0.25, -1.0, 0.0, 1.0, 0.4, 0.4
]);
let n = 18; // 頂點數量

// 創建顏色緩衝區
let vertexColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

let FSIZE = verticesColors.BYTES_PER_ELEMENT;

// 傳遞頂點座標數據
let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);

// 傳遞顏色數據
let a_Color = gl.getAttribLocation(gl.program, 'a_Color');
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
gl.enableVertexAttribArray(a_Color);

// 緩衝區解綁
gl.bindBuffer(gl.ARRAY_BUFFER, null);

上面的示例通過類型化數組向顯卡傳遞數據,有些時候我們不斷操作和修改緩衝區中數據,此時類型化數組的作用就顯現了出來,數據的傳輸非常

總結

通過對緩衝區ArrarBuffer和類型化數組以及一些具體的屬性和方法介紹,瞭解到它與一般的Array相比起來使用雖然麻煩,但是它帶來的好處是顯而易見的,類型化數組不能動態增長可以很好的規避內存溢出的問題,它可以通過緩衝區來操作內存,給數據的傳輸帶來了極大的方便等等

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章