由於高度封裝與抽象,JavaScript 的執行效率比不上 C 的語言。例如 JavaScript 的 Array 下標(subscript)是根據 hash key 而非實體記憶體位址 offset 取值,雖然方便,卻多了效能開銷。當 Canvas、WebGL、WebVR 開始走紅,效能越來越受重視,如何讓 JavaScript 達到如同 C 指標般操作 binary data 變得至關重要。
存在許久但最近才變為 ES6 標準「
Typed Array
」就是解放 JavaScript 操作 binary data 能力的好工具!一起來了解 Typed Array 吧!
(基於 ECMAScript 6+,Node.js 8.3)
Typed Array 家族
,可以分為兩大類:
Buffer
與
View
。
所謂
Buffer
是一個指向儲存資料的記憶體區塊之物件,類似於
malloc
配置出來的空間,無法直接存取或修改 buffer 內部的資料,在 JavaScript 中 Buffer 的實作就是
ArrayBuffer
。
如果我們想存取某些 buffer 底下的內容,我們需要
View
(視圖),透過宣告不同資料型別的 view,電腦就會了解如何操作這段 data chunk,該當作 float32 讀取呢?抑或以 unsigned integer 來操作。
ES6 規範了三個 Typed Array 相關物件,對應類別如下:
ArrayBuffer
:Buffer,代表一段記憶體區塊,僅能透過 View 操作其內容。
TypedArray
:View,儲存固定型別資料的 Array,例如
Uint8Array
(8-bit unsigned integer)、
Float64Array
(64-bit IEEE floating point number)。
DataView
:View,不限制型別,可自定義從哪個 byte,以什麼型別,用哪種 byte order(endian)存取。
ArrayBuffer
代表一段固定大小的記憶體區塊,也稱為 byte-array。主要的功能就是配置實體記憶體來儲存 raw binary data。一般很少直接操作 ArrayBuffer,實際上也只能將其 reference 傳給其他物件,讓其他物件來處理/使用資料。
建立一個 ArrayBuffer 有非常多種方法,可以直接配置,
1 2 3 4 5 6
const buffer = new ArrayBuffer (8 )const bufferCopied = buffer.slice (-4 )
或是最常使用的,HTTP response 選擇接收 buffer,
1 2 3 4 5 6 7 8 9 10 11 12 13
const xhr = new XMLHttpRequest ()xhr.open ('GET' , '/path/to/黑人問號.jpg' , true ) xhr.responseType = 'arraybuffer' xhr.onload = function (e ) { console .log (this .response ) } xhr.send () const response = await fetch ('/path/to/柯P火影.gif' )const buffer = await response.arrayBuffer () console .log (buffer.byteLength )
當然,也可以透過
File
與
FileReader
API,讀取使用者上傳的資料。
1 2 3 4 5 6 7 8 9 10 11 12
const input = document .querySelector ('input' )input.addEventListener ('change' , handleFiles, false ); function handleFiles (files ) { if (files[0 ]) { const reader = new FileReader () reader.onload = function ({ target: { result } } ) { console .log (result.byteLength ) } reader.readAsArrayBuffer = files[0 ] } }
endianness
而定),僅顯示餘下 4 個 bytes 的資料,記憶體位址變得不連續。
從 ArrayBuffer 建立
不過,也可以透過
TypedArray#buffer
取得並共享當前的 buffer,該 array 的記憶體區間就會是連續的。
1 2
const u8_continuous = new Uint8Array (u16.buffer )
當然,ArrayBuffer 可直接配置一塊記憶體區塊,並使用它建構 TypedArray,甚至透過
length
和
byteOffset
指定該 buffer 不同的區間來建構。
1 2 3 4 5
const buffer = new ArrayBuffer (16 )const i32 = new Int32Array (buffer) const i8 = new Iint8Array (buffer, 4 , 7 )
「溢位(Overflow)」
,例如 Uint8Array 中僅能放入 1 byte = 8 bits 的資料,如果放入
0x100
(256,9 bits),就會溢位。
那溢位後,資料會怎麼呈現呢?
每個語言實作不盡相同,TypedArray 的溢位處理規則和多數語言相同:
捨棄溢出的 high bits。
我們來看簡單的例子。
1 2
Uint8Array .of (0xff , 0x100 )
第一個例子中,我們選用 Uint8Array,一個元素最多儲存 8 bits 的資料,第二個元素是 256,需要第 9 bit 來儲存,因此溢位。
1 2 3 4 5 6 7 8 9
0b11111111 0b100000000 0x100 & 0xFF
Note:underflow 的處理方式與 overflow 相同。
buffer overflow
,所謂的 buffer overflow 是「
當寫入一筆資料到指定 buffer 中,若寫入的資料大小超過該 buffer 的 boundary,溢出值就會覆寫下個 byte
」。這種不安全的性質,也讓 buffer overflow 成為許多駭客的攻擊手法,有潛在的安全性問題。而透過 DataView setter 賦予一個超過型別最大值的數字,並不會覆蓋臨近記憶體位址的資料,而是內部先檢查邊界,處理 overflow 之後,再寫入該記憶體區間,彌補了 buffer overflow 的漏洞。
Endianness (Byte order)
Data Structure Alignment
BOM(byte order mark)
」來判斷資料屬於哪種 endianness。BOM 是一個 Unicode magic number,通常放置在 text stream 的最前端。不過,並不是每個資料都會加上這個 header,而且有時候我們不需要 BOM 資訊,使用資料前還必須先
strip bom
,說實話挺麻煩的。
natural alignment
檢查。
1 2 3 4 5
const buffer = new ArrayBuffer (6 )new Uint16Array (buffer, 1 )new Uint32Array (buffer, 0 )
所以囉,當我們在設計複合資料時,想想對應的 C struct alignment,多考量記憶體底層,才不會讓操作 binary data 產生效能低落的反效果。
Buffer class
,也針對 V8 引擎做最佳化。Buffer 在 Node.js 的環境中是 Global object,其功能可視為 ArrayBuffer + TypedArray + DataView 的複合體,甚至可以配置 non-zero-filled 的 unsafe buffer,好危險啊。
在使用上,Buffer 可從 ArrayBuffer 建構,也可從自身建構 TypedArray。事實上,Node.js v3+ 之後,Buffer 就繼承自
Uint8Array
了,不過有些 memory share/copy 的實作與 spec 有出入,在與 TypedArray
ArrayBuffer 轉換時,
需注意這些小細節
。
Blob
是一個不可變(immutable)的 raw binary sequence,只有兩個 attribute 和一個 method。
size
:blob 實例的 byte 大小。
type
:blob 實例的
MIME type
。
slice
:切割一部分的 blob 實例,返回新的 blob。
Blob 的 spec 寫在
W3C File API draft
中,為
File
class 的父類別。主要目的是提供可代表與儲存 JavaScript native 以外的格式,例如以 blob 儲存
死肥宅.jpg
。Blob 除了可以從 object 建構,也可傳入 TypedArray 或 DOMString 建構。此外,File API,Fetch API、XMLHttpRequest v2 也都可以將 Request/Response 的 body 轉換成 Blob,非常泛用途呢!
而 Blob 最強大的地方就是配合
URL.createObjectURL
生成一個 Blob URL。如同你我認知中的 URL,任何運用 URL 之處,都可以傳入 Blob URL,比起
Image
、
ImageData
、
MediaSource
,URL 接受與使用度肯定更為廣闊,這讓資料處理,物件傳遞的耦合性變得更低。
當我們建立 Blob URL 後,若可預期的未來內不需要用到該 URL,就使用
URL.revokeObjectURL
取消註冊,否則該 URL 指向的 Blob 會持續留存,佔用你的儲存空間,直到瀏覽器執行 unload document cleanup 的步驟(如關閉分頁),才會將所有 Blob URL 清除。所以說,如需管理 Blob URL,還是老老實實把這些 URL 記錄起來吧!
即將成為 Webpack 一等公民
的
WebAssembly
,JavaScript/Web 的世界更是不可限量啊!或許,使用 Rust 寫網頁的世代即將來臨 XD。
前端工程師們,活到老,學到掛吧!