文章出自個人博客https://knightyun.github.io/2020/03/09/js-binary-data,轉載請申明
編程江湖,終日血雨腥風,論及二進制數據,又有多少豪傑談笑風生,風生水起,水起船高,高深莫測…
不扯遠了,想必談到二進制數據,大家聯想到的就會是 1010110110001
或者 00000000 11111111 00000101
這樣的數據流;而這武林之中,號稱三劍客之一的 JavaScript,在其行走江湖之際(日常開發),可能廝殺(處理)最多的類型就是直觀的數字、字符串或者對象等;那麼與極少露面的隱士(二進制)狹路相逢之時,它又將作何應對(描述與處理二進制數據)呢?是波瀾壯闊,還是全身而退,抑或是力挽狂瀾,且聽本文中分解。
ArrayBuffer
未曾識得英雄面,只緣身在此山中;先來了解第一個概念,ArrayBuffer
表示的是一個原始二進制數據緩衝區(buffer),長度固定,並且內容是隻讀的;如果需要執行寫操作,那麼需要使用 類型化數組(TypedArray
)或者 數據視圖(DataView
) 來實現;
知己知彼,方可百戰百勝;在 JavaScript 中與二進制數據接觸最緊密的可能就是 ArrayBuffer
了,之前講目的是要描述和操作二進制數據,那麼就要把這些數據先存放到某個地方,然後才能對其進行操作,這裏的 ArrayBuffer 緩衝區就可以被看成這麼一種地方;當然,可能最直觀的方式就是將其保存到字符串中,如 "101011011"
,又或者存入數組,如 [1,0,1,0,1,1,0,1]
,這樣確實是方便人類了,但是機器執行的效率也降低了,因爲畢竟字符和數組是另外兩種基本類型,並且也不是專門爲此設計的;所以,就出現了專門爲緩衝數據設計的 ArrayBuffer
,並通過結合視圖來提供一個訪問和操作數據的接口;
語法
實例化 ArrayBuffer
構造函數時,只接受一個參數,就是要創建的 Arraybuffer 的大小,單位是字節,不指定的話默認爲 0;同時它也提供了一個實例屬性 byteLength
(只讀),實現對當前 ArrayBuffer 字節值的訪問;
舉例:
var buffer = new ArrayBuffer(3);
console.log(buffer.byteLength); // 3
另外,由於 ArrayBuffer 只是負責創建這麼一段數據區域,並沒有提供初始化賦值的接口,所以這 n 字節的數據都爲空,即都置 0;
方法
由於 ArrayBuffer 構造函數本身是用於創建數據緩衝區,並且數據只讀,所以提供的屬性和方法也只有少數幾個;
.slice()
用於返回一個新的緩衝區,語法爲 .slice(start, end)
,即以當前緩衝區爲母本,從索引爲 start 的位置開始,到 end 位置結束(end 位置不包含在內),然後複製並返回這一區段的數據;其用法大致與 Array.prototype.slice()
類似,舉例說明:
var buffer1 = new ArrayBuffer(5);
var buffer2 = buffer1.slice(0, 3);
var buffer3 = buffer1.slice(2);
var buffer4 = buffer1.slice(1, -1);
console.log(buffer2.byteLength); // 3
console.log(buffer3.byteLength); // 3
console.log(buffer4.byteLength); // 3
ArrayBuffer.isView()
該方法用來判斷所提供的參數值是否是一種 ArrayBuffer
視圖,比如類型化數組(TypedArray)和數據視圖(DataView),例如:
console.log(ArrayBuffer.isView()); // false
console.log(ArrayBuffer.isView([1, 2, 3])); // false
console.log(ArrayBuffer.isView(new ArrayBuffer(3))); // false
console.log(ArrayBuffer.isView(new Int8Array())); // true
console.log(ArrayBuffer.isView(new Uint32Array(3))); // true
console.log(ArrayBuffer.isView(new DataView(new ArrayBuffer()))); // true
類型化數組(TypedArray)
概述
工欲善其事必先利其器;前面提到操作 ArrayBuffer
創建的數據緩衝區需要使用視圖(view)實現,類型化數組就是這麼一個描述二進制數據緩衝區(buffer)的視圖(view),這個視圖是一個 類數組。另外,不存在 TypedArray()
這個構造函數,它指的是一類數組,因此它有多種實現,即多個類型化數組構造器函數;可以姑且理解爲 水果 之於 蘋果 和 香蕉,水果指的是一類食物,都知道並不存在名爲 水果 的一種具體食物,但是 蘋果 和 香蕉 是具體存在的;
有效的類型如下:
Int8Array(); // 8 位二進制有符號整數
Uint8Array(); // 8 位無符號整數(超出範圍後從另一邊界循環)
Uint8ClampedArray(); // 8 位無符號整數(超出範圍後爲邊界值)
Int16Array(); // 16 位二進制有符號整數
Uint16Array(); // 16 位無符號整數signed
Int32Array(); // 32 位二進制有符號整數
Uint32Array(); // 32 位無符號整數
Float32Array(); // 32 位 IEEE 浮點數(7 位有效數字,如 1.1234567)
Float64Array(); // 64 位 IEEE 浮點數(16 有效數字,如 1.123...15)
BigInt64Array(); // 64 位二進制有符號整數
BigUint64Array(); // 64 位無符號整數
語法:
萬法不離其宗,一招一式都有跡可循;後面就都以 Int8Array()
爲例進行說明,以下代碼展示了可以傳入的參數類型:
new Int8Array(); // ES2017 中新增
new Int8Array(length);
new Int8Array(typedArray);
new Int8Array(object);
new Int8Array(buffer [, byteOffset [, length]]);
無參數
最好的招式是沒有招式;實例化構造函數時不傳入任何參數,則返回一個空的類型化數組:
var i8 = new Int8Array();
console.log(i8); // Int8Array []
console.log(i8.length); // 0
console.log(i8.byteLength); // 0
console.log(i8.byteOffset); // 0
length
一寸長一寸強;傳入一個數字類型的參數,表示申明類型化數組中元素的個數:
var i8 = new Int8Array(3);
var _i8 = new Int8Array('3'); // 字符串會先被轉換成數字
console.log(i8); // Int8Array(3) [0, 0, 0]
console.log(_i8); // Int8Array(3) [0, 0, 0]
console.log(i8.length); // 3
console.log(i8.byteLength); // 3
console.log(i8.byteOffset); // 0
typedArray
好招不怕效仿;當傳入的一個參數同樣是一個類型化數組時,則返回一個原類型數組的拷貝(不是引用):
var i8 = new Int8Array(3);
var _i8 = new Int8Array(i8);
console.log(i8 == _i8); // false
console.log(i8 === _i8); // false
console.log(_i8); // Int8Array(3) [0, 0, 0]
object
海納百川有容乃大;使用該參數時類似於用 TypedArray.prototype.from()
方法創建的類型數組,同時該方法也和 Array.from()
方法類似,即這個 object
參數是一個類數組的對象,或者是可迭代的對象;舉例:
// 數組
var i81 = new Int8Array([1, 2, 3]);
console.log(i81);
// Int8Array(3) [1, 2, 3]
// 等價的操作
var i81 = Int8Array.from([1, 2, 3]);
// Int8Array(3) [1, 2, 3]
// 類數組
var i82 = new Int8Array({
0: 1,
1: 2,
2: 3,
length: 3
});
console.log(i82);
// Int8Array(3) [1, 2, 3]
// 可迭代對象
var i83 = new Int8Array(new Set([1, 2, 3]));
console.log(i83);
// Int8Array(3) [1, 2, 3]
buffer, byteOffset, length
衆人拾柴火焰高;該構造函數也支持同時提供三個參數,第一個 buffer
指的是數組緩衝區,是 ArrayBuffer
的實例,同時也是 Int8Array.prototype.buffer
這個屬性的值;butyOffset
指的是元素的偏移值,表示從數組中第幾個元素開始讀取,默認是 0,也就是數組的第一個元素;length
指的是在設置了偏移值後,要讀取的元素長度,默認是整個數組的長度;舉例說明:
var buf = new Int8Array([1,2,3,4,5]).buffer;
var i8 = new Int8Array(buf, 1, 2);
console.log(i8);
// Int8Array(2) [2, 3]
也就是讓申明的類型化數組在提供的 buffer
的基礎上,從它的索引爲 1 的元素(第二個元素)開始讀取,然後向後讀取 2 個元素;該操作一般用於對緩衝區數據的截取;
類型差異
存在即合理;根據前面的介紹,TypedArray
定義了多種類型,如 Int8Array
, Uint8Array
, Int16Array
等,這樣做也是爲了適應不同的應用場景,接下來大致瞭解一下幾個典型的類型化數組之間的區別;
有無符號
以 Int8Array
和 Uint8Array
爲例,其實 有符號 的意思是數組中的元素可以存在符號,即可以是負數;因此 無符號 的意思就是元素只能是非負數,舉例:
var i8 = new Int8Array([1, 2, 3]);
var _i8 = new Int8Array([-1, -2, -3]);
var ui8 = new Uint8Array([1, 2, 3]);
var _ui8 = new Uint8Array([-1, -2, -3]);
console.log(i8); // Int8Array(3) [1, 2, 3]
console.log(_i8); // Int8Array(3) [-1, -2, -3]
console.log(ui8); // Uint8Array(3) [1, 2, 3]
console.log(_ui8);// Uint8Array(3) [255, 254, 253]
可以發現有符號類型之處初始化負數元素,而無符號則會對負數進行轉換,具體轉換方式後面會提到;
元素範圍
有無符號的類型數組,除了元素的值的正負區別外,元素的取值範圍也有所不同;下面是一份具體的清單:
Type | Range |
---|---|
Int8Array | -128 ~ 127 |
Uint8Array | 0 ~ 255 |
Uint8ClampedArray | 0 ~ 255 |
Int16Array | -32768 ~ 32767 |
Uint16Array | 0 ~ 65535 |
Int32Array | -2147483648 ~ 2147483647 |
Uint32Array | 0 ~ 4294967295 |
Float32Array | 1.2×10-38 ~ 3.4×1038 |
Float64Array | 5.0×10-324 ~ 1.8×10308 |
BigInt64Array | -263 ~ 263-1 |
BigUint64Array | 0 ~ 264-1 |
可以看出,爲了顧及有無符號類型的單個元素取值範圍區間一樣,所以就調整了它們的取值上下限;
字節位數
以有符號類型爲例,可以發現有 Int8Array
, Int16Array
等幾個不同的類型數組,唯一的區別就是他們構造函數名字中間的數字不同,其實這個數字指的是實例化後的類型化數組的單個元素的大小,即多少位,8 就是 8 位,即一字節,16 就是 2 字節,類推;其實,這個數字也反應了類型數組中 BYTES_PER_ELEMENT
這個屬性的值,從名字也可以看出代表的是每個元素的字節數;舉例說明:
var i8 = new Int8Array(3);
var i16 = new Int16Array(3);
var i32 = new Int32Array(3);
console.log(i8.BYTES_PER_ELEMENT); // 1
console.log(i16.BYTES_PER_ELEMENT); // 2
console.log(i32.BYTES_PER_ELEMENT); // 4
console.log(i8.length); // 3
console.log(i16.length); // 3
console.log(i32.length); // 3
console.log(i8.byteLength); // 3
console.log(i16.byteLength); // 6
console.log(i32.byteLength); // 12
另外 byteLength
這個屬性其實指的是類型數組總的字節大小,其值等於單個元素字節值乘以元素個數(byteLength = BYTES_PER_ELEMENT x length
);
Clamped
鶴難隱於雞羣;從前面的清單中可以找到 Uint8ClampedArray
這個獨特的類型數組,區別就是中間多了 clamped
這個單詞,詞典解釋的意思是“夾緊,箝位”,具體功能是什麼,下面通過代碼來解釋:
var i8 = new Uint8Array([1, 2, 3]);
var _i8 = new Uint8Array([-1, -2, -3]);
var _i8_ = new Uint8Array([255, 256, 257]);
var uic8 = new Uint8ClampedArray([1, 2, 3])
var _uic8 = new Uint8ClampedArray([-1, -2, -3])
var _uic8_ = new Uint8ClampedArray([255, 256, 257])
console.log(i8); // Int8Array(3) [1, 2, 3]
console.log(_i8); // Int8Array(3) [255, 254, 253]
console.log(_i8_); // Uint8Array(3) [255, 0, 1]
console.log(uic8); // Uint8ClampedArray(3) [1, 2, 3]
console.log(_uic8); // Uint8ClampedArray(3) [0, 0, 0]
console.log(_uic8_);// Uint8Array(3) [255, 255, 255]
不知諸位可否查探出端倪,這裏也能解釋之前說的無符號類型數組實例化時轉換負值的問題;通過分析不難發現,轉換方式類似於素組循環取值,就是如果傳入的值超過了元素的取值範圍的上限或下限之一時,那麼超過的部分就會,從範圍的另一個界限開始依次向後計數;所以上例中 -1 會被轉換爲 255,257 會被轉換成 1;
而對於 Uint8ClampedArray
這個類型數組,其實差不多也是字面的意思,類似於一個 “夾住” 的操作:超出範圍不會發生循環轉換,無論超出多少都只會被置爲對應的邊界值,所以上例中 -1, -2, -3
都被轉換爲 0
,256, 257
則都被轉換成了 255
;
浮點數
論世間誰主浮沉;僅有的兩個浮點類型的類型數組,Float32Array
和 Float64Array
,浮點的意思就是元素值可以是小數,因爲之前介紹過的都是 int
(整數) 類型的;依然來舉例說明:
var f32 = new Float32Array([1.11, 2.12345678911, -3.33333333333333333333333333])
var f64 = new Float64Array([1.11, 2.12345678911, -3.33333333333333333333333333])
console.log(f32);
// Float32Array(3) [1.1100000143051147, 2.1234567165374756, -3.3333332538604736]
console.log(f64);
// Float64Array(3) [1.11, 2.12345678911, -3.3333333333333335]
從結果來看 32 位浮點類型數組每個元素都保留到小數點後 16 位,而 64 位是最多保留到 16 位,具體的細節就不深究了;
操作元素
欲與二進制數據一決高低,首先肯定是選幾樣趁手兵器;雖然類型化數組擁有普通數組的大部分方法,比如 every
, forEach
, slice
等等,但也有自己特有的方法值得說一下,比如 .set() 這個方法;
.set()
方法用於把指定數組的(所有)元素添加到當前數組的指定位置,接受的參數爲 .set(array[, ofset])
,這裏的 array 可以是普通數組或類型化數組,offset 指的是偏移值,即從哪個位置開始寫入指定數組元素;舉例說明:
var i8 = new Int8Array(6);
var i81 = new Int8Array(6);
var i82 = new Int8Array(6);
var arr = [1, 2, 3];
var arr1 = [1, 2, 3, 4, 5, 6];
i8.set(arr, 2);
console.log(i8);
// Int8Array(6) [0, 0, 1, 2, 3, 0]
i81.set(arr1, 2);
console.log(i81);
// Uncaught RangeError: offset is out of bounds
i82.set(arr, 6);
console.log(i82);
// Uncaught RangeError: offset is out of bounds
證明無論是拷貝的數組大小超過原數組,還是偏移值過大使得拷貝結果超過原數組,都會報錯提示偏移超過邊界,因此使用時需計算準確;
操作緩衝區
箭在弦上,東風將至;前面將 TypedArray 描述爲操作 ArrayBuffer 中數據的視圖,下面就來看一下具體的操作方法;
數據讀取
數據轉換
敵不動我不動;使用類型化數組操作 ArrayBuffer 的數據前,需要先獲取其中的數據,也就是把 ArrayBuffer 轉換爲 TypedArray 類型;先來看一下這兩種類型互相轉換的方法:
ArrayBuffer 轉換爲 TypedArray:
var buffer = new ArrayBuffer(5); // 先初始化 5 字節長的區域
var i8 = new Int8Array(buffer); // 再把數據傳遞進 TypedArray
console.log(i8); // Int8Array(5) [0, 0, 0, 0, 0]
這裏也可以驗證,ArrayBuffer 新創建的區域數據都被置 0 了;
TypedArray 轉化爲 ArrayBuffer:
var i8 = new Int8Array(5);
var buffer = i8.buffer;
console.log(buffer); // ArrayBuffer(5) {}
讀取方式
前面講道,類型化數組有多種不同的實現,比如 1 字節有符號元素的 Int8Array
,2 字節的 Int16Array
等;根據 ArrayBuffer
的定義,緩衝區是以 1 字節 爲單位進行創建的,所以我們通常讀取文本類數據使用 Uint8Array
,因爲它也正好每個元素的大小爲 1 字節,當然,也可以選擇用 Uint16Array
來 2 字節地挨個讀,其他類型類推;
通過代碼來觀察一下具體的讀取方式:
var data = new Uint8Array([1, 2, 3, 4])
var buffer = data.buffer;
var ui8 = new Uint8Array(buffer);
var ui16 = new Uint16Array(buffer);
console.log(ui8); // Uint8Array(4) [1, 2, 3, 4]
console.log(ui16); // Uint16Array(2) [513, 1027]
原始數據 data
是 4 字節大小,通過 Uint8Array
就是以 1 字節爲單位,所以得到的也是原始的數據 [1, 2, 3, 4]
,這裏由於數據小所以有無符號無影響;而通過 Uint16Array
則是以 2 字節爲單位進行讀取,所以總的元素長度爲 2(2 = 4 / 2),但是其中的單個元素 513
, 1027
又分別是如何得到的呢?我們可以通過計算來探究一下:
首先看 1, 2
這兩個元素,根據結果它們被讀取成爲了 513
,那麼就把這幾個元素的二進制數表示出來(緩衝區就是存儲的二進制數據):
"1": 00000001
"2": 00000010
"12": 00000001 00000010
"513": 00000010 00000001
"21": 00000010 00000001
規律顯而易見了,513
這個 2 字節的數據,其實是把 1
和 2
這兩個挨着的 1 字節的數據,以 倒序 方式拼接在一起的;
再來看一下 3
和 4
這兩個是否也是以同樣的方法得出 1027
這個數據的:
"3": 00000011
"4": 00000100
"34": 00000011 00000100
"1027": 00000100 00000011
"43": 00000100 00000011
結果不出所料,所以像 Uint32Array
等以多個字節讀取數據的類型數組,方法也可以類推;
字節序
另外值得一提的是,上面所說的 倒序 拼接方式,其實有個專業術語,叫做 字節序(Endian),對應這個英文單詞應該會感覺似曾相識,例如 Linux 中執行 lscpu
得到的結果中,就會發現它的存在:
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 36 bits physical, 48 bits virtual
字節序,或字節順序(“Endian”、“endianness” 或 “byte-order”),描述了計算機如何組織字節,組成對應的數字。
這個字節序可以分爲:
Little Endia(低字節序):低位數據放入存儲地址的低位,高位數據放入高位地址;
這種順序就顯得和內存上的存儲地址順序(閱讀模式下低位在右,高位在左)保持一致,並且也是一種常見的方式,比如上面的英特爾處理器;只不過對於這種順序人類閱讀時就要反着讀了(從右至左),比如上面例子中的數據 12
就是以 21
的順序讀取的,也可以類比這種日期格式:"Sat 07 Mar 2020"
;
Big Endian(高字節序):低位數據存入高位地址,高位數據放入低位;
這種順序可能更符合人類的閱讀習慣(從左至右),它一般應用在互聯網標準的數據結構中,可以類比 "2020-03-07"
這種日期格式;
數據修改
下面通過類型化數組視圖來嘗試修改一下 ArrayBuffer 緩衝區中的內容:
var buffer = new ArrayBuffer(3);
var i8 = new Int8Array(buffer);
console.log(i8); // Int8Array(3) [0, 0, 0]
for (let i = 0; i < i8.length; i++) {
i8[i] = 1;
}
var _i8 = new Int8Array(buffer); // 新建個視圖驗證是否修改成功
console.log(_i8); // Int8Array(3) [1, 1, 1]
數據拼接
用之前講過的 .set()
方法來嘗試將數據拼接進緩衝區:
var buffer = new ArrayBuffer(6);
var i80 = new Int8Array(buffer);
console.log(i80); // Int8Array(6) [0, 0, 0, 0, 0, 0]
var i81 = new Int8Array([1, 2, 3]);
var i82 = new Int8Array([4, 5, 6]);
i80.set(i81);
i80.set(i82, 3);
var _i80 = new Int8Array(buffer); // 驗證是否修改成功
console.log(_i80); // Int8Array(6) [1, 2, 3, 4, 5, 6]
注意:這裏不能使用數組的 .concat()
這個方法來進行元素拼接,因爲類型化數組中並沒有內置這個方法,不然會報錯,如下:
var arr1 = [1, 2, 3];
var arr2 = arr1.concat(4, 5, 6);
console.log(arr2); // [1, 2, 3, 4, 5, 6]
var i81 = new Int8Array([1, 2, 3]);
var i82 = i81.concat(4, 5, 6);
console.log(i82);
// Uncaught TypeError: i81.concat is not a function
同樣地,.splice()
這個可以替換元素的方法也不存在於類型化數組中;
數據視圖(DataView)
概述
一個好漢三個幫;DataView
是另外一個用於從 ArrayBuffer
緩衝區中讀寫數據的視圖接口,其特點就是考慮了 字節序 的問題,後面會講;
語法爲:
new DataView(buffer [, byteOffset [, byteLength]]);
其中 buffer
指傳入的數據緩衝區,如 ArrayBuffer;byteOffset
指偏移的字節量,默認第一個字節,byteLength
指要傳入的數據的字節長度,默認整個 buffer 的長度;並且這三個參數都可以在實例化後通過相應屬性(只讀)訪問到;
var buffer = new Int8Array([1, 2, 3, 4]).buffer;
var dv = new DataView(buffer, 1, 2);
console.log(dv); // DataView(2) {}
console.log(dv.buffer); // ArrayBuffer(4) {}
console.log(dv.byteOffset); // 1
console.log(dv.byteLength); // 2
操作數據
DataView 提供了一系列的方法用於操作緩衝區的數據,先簡單預覽一下:
Read | Write |
---|---|
getInt8() | setInt8() |
getUint8() | setUint8() |
getInt16() | setInt16() |
getUint16() | setUint16() |
getInt32() | setInt32() |
getUint32() | setUint32() |
getFloat32() | setFloat32() |
getFloat64() | setFloat64() |
Read
以 getInt8()
方法爲例,可提供一個參數 byteOffset
,表示偏移指定字節數,然後讀取 1 字節(8 位)數據,默認 爲 0(第一字節);而如果是 getInt16()
等用於獲取大於 1 字節值以及浮點值的方法,還接受第二個可選參數 littleEndian
,就是是否使用 little endian(低字節序,上文有講)格式來讀取數據,傳入 true
就表示使用 little endian 格式,傳入 false
或者不填,就使用 big endian(高字節序) 格式;
var buffer = new Int8Array([1, 2, 3, 4]).buffer;
var dv = new DataView(buffer);
console.log(dv.getInt8(1)); // 2
console.log(dv.getInt16(0, true)); // 513
console.log(dv.getInt16(0, false)); // 258
console.log(dv.getInt16(0)); // 258
結果爲 513
的這一行代碼,使用的是 little endian 格式,並且 513 這個值也與之前 TypedArray
中關於 Int16Array
例子的結果一致,證明 TypedArray
默認使用的是 little endian 格式在操作數據緩衝區;
Write
以 setInt8()
爲例,接受兩個參數:setInt8(byteOffset, value)
,第一個表示偏移字節量,用於定位,第二個則是要設置的具體值,非數字類型會報錯;類似地,setInt16
等用於設置超過 1 字節的方法,也提供第三個可選參數 littleEndian
,表示是否以 little endian 格式設置;
var buffer1 = new ArrayBuffer(2);
var buffer2 = new ArrayBuffer(4);
var dv1 = new DataView(buffer1);
var dv2 = new DataView(buffer2);
dv1.setInt8(0, 1);
dv1.setInt8(1, 2);
var i8 = new Int8Array(dv1.buffer);
console.log(i8); // Int8Array(2) [1, 2]
dv2.setUint16(0, 513, true);
dv2.setUint16(2, 513);
var i16 = new Uint16Array(dv2.buffer);
console.log(i16); // Int16Array(2) [513, 258]
需要注意的就是,因爲 byteOffset
這個參數的單位始終是 1 字節,所以當寫入超過一字節的數據時,相應的偏移值也需要增加,就像上例所以展示的一樣;
對比
與前文所講的 TyptedArray
視圖接口相比,DataView
視圖雖然兼容了不同平臺的字節序問題,但是也沒有了一些對整段數據進行修改拼接的功能,只能修改單個元素值;另外也不能用構造函數初始賦值,比如下面的情況:
console.log(new Int8Array([1, 2, 3]));
// Int8Array(3) [1, 2, 3]
console.log(new DataView([1, 2, 3]));
// Uncaught TypeError: First argument to DataView constructor must be an ArrayBuffer
所以,需要靈活地結合二者使用,以應對複雜的場景;兄弟齊心,其力斷金;
Blob
Blob
構造函數用於描述一個 blob(Binary Large OBject,二進制大對象),即保存原始數據的類文件對象,支持保存 多種類型 的數據(不像 TypedArray
,只能使用數字類型),並且數據是隻讀的,不可修改;另一個基於 Blob 的構造函數 File
,就是用來處理用戶上傳文件的(<input type="file">
)數據。
語法:
new Blob(array, options);
array
指的是一系列類型的數據構成的數組或者類數組,這些數據可以是字符串、ArrayBuffer、DataView、TypedArray、Blob、DOMString 等等;options
則是一個對象,可以包含以下兩個屬性:
{
type: "", // 傳入的數據的 MIMS 類型,比如 text/plain,默認爲空
endings: "" // 如何處理數據中的換行符,比如 \n 和 \r\n,因操作系統而異
// 值爲 transparent 或者 native,默認爲 transparent
// native 表示替換爲當前系統的換行符
// transparent 則表示不替換,保持數據內容
}
寫入數據
通過幾個例子來說明:
var blob1 = new Blob([1, 2, 3]);
var blob2 = new Blob(['a', 'bc', 'd e']);
var blob3 = new Blob(['hello'], {type: 'text/plain'});
var blob4 = new Blob(new Int8Array([4, 5, 6]));
var blob5 = new Blob([blob2]);
console.log(blob1); // Blob {size: 3, type: ""}
console.log(blob2); // Blob {size: 6, type: ""}
console.log(blob3); // Blob {size: 5, type: "text/plain"}
console.log(blob4); // Blob {size: 3, type: ""}
console.log(blob5); // Blob {size: 6, type: ""}
如果參入的參數不是類數組的類型,則會報錯:
var blob1 = new Blob(123);
var blob2 = new Blob('123');
var blob3 = new Blob({foo: 'bar'});
var blob4 = new Blob(true);
var blob5 = new Blob(blob1);
console.log(blob1);
// VM3497:1 Uncaught TypeError: Failed to construct 'Blob':
// The provided value cannot be converted to a sequence.
console.log(blob2);
// VM3497:1 Uncaught TypeError: Failed to construct 'Blob':
// The provided value cannot be converted to a sequence.
console.log(blob3);
// VM3497:1 Uncaught TypeError: Failed to construct 'Blob':
// The provided value cannot be converted to a sequence.
console.log(blob4);
// VM3497:1 Uncaught TypeError: Failed to construct 'Blob':
// The provided value cannot be converted to a sequence.
console.log(blob5);
// VM3497:1 Uncaught TypeError: Failed to construct 'Blob':
// The provided value cannot be converted to a sequence.
讀取數據
寫入 Blob 實例中的數據雖然不能修改,但是還是可以讀取的,首先可以獲取數據總的大小和類型(只讀):
var blob = new Blob(['a', 'b', 'c'], {type: 'text/plain'});
console.log(blob.size); // 3
console.log(blob.type); // text/plain
.text() 方法用於獲取 Blob 中的文本數據,返回值是一個 promise 對象,包含一個 resolved 狀態的文本數據,無提供的參數;
var blob = new Blob([1, 2, 3]);
blob.text().then(data => {
console.log(data, typeof data);
});
// 123 string
.arrayBuffer() 方法也用於獲取 Blob 中的數據,並且返回一個 promise,無參數提供,只不過返回的是數據的 ArrayBuffer
,即二進制數據緩衝區;
var blob1 = new Blob([1, 2, 3]);
var blob2 = new Blob(['a', 'b', 'c']);
blob1.arrayBuffer().then(data => {
console.log(new Uint8Array(data));
});
// Uint8Array(3) [49, 50, 51]
blob2.arrayBuffer().then(data => {
console.log(new Uint8Array(data));
});
// Uint8Array(3) [97, 98, 99]
計算以下也可以驗證,類型數組中的數值確實是對應的原始數據的二進制值。
TextEncoder
臨陣磨槍,不快也光;這還是一個處於 實驗階段 的接口,當前的接口將來可能發生改變,並且目前 IE 系列瀏覽器都還不支持,這裏只作簡單介紹;
顧名思義,這個構造函數的作用就是負責編碼文本,其實就是以指定的編碼格式,將傳入的文本轉換成該數據對應的 類型化數組;實例化時可以提供一個參數,用於編碼格式,不過目前默認並且只使用 UTF-8 格式編碼,所以可以省略;
var encoder = new TextEncoder();
var arr = encoder.encode('abc');
console.log(encoder.encoding); // utf-8
console.log(arr); // Uint8Array(3) [97, 98, 99]
有編碼就自然有解碼,TextDecode
這個構造函數就與之對應,即將 ArrayBuffer
或者 ArrayBuffer View
類型的數據解碼爲相應的文本;
var ui8 = new Uint8Array([97, 98, 99]);
var buffer = ui8.buffer;
var decoder = new TextDecoder();
var text1 = decoder.decode(ui8);
var text2 = decoder.decode(buffer);
console.log(text1); // abc
console.log(text2); // abc
這樣,除了上面的 Blob
,這裏的 TextEncoder
也可以用於將文本數據保存爲 JavaScript 中的二進制緩衝數據;
處理文件數據
人外有人,天外有天,跨過了這二進制,便是更廣闊的天地;說了一系列的關於二進制數據的保存和讀寫方法,也該談談其用武之地了;
要知道 JavaScript 中保存文本字符串什麼的用變量就行了,緩衝區、類型數組、Blob 這些接口其實多數是用於處理文件數據相關的,因爲它們有着不同的 MIME 類型,比如 .jpg .mp4 .bin
這些後綴的文件,JavaScript 並沒有內置一些直接處理這些數據類型的接口(例如 .txt 文檔就能可以處理),所以就需要以原生二進制數據的方式來保存或處理,方便用戶上傳、下載或預覽;下面就將介紹一些文件處理相關的接口;
File
前面講到,File
是基於 Blob
的,所以也就繼承了它的一些方法;File
用於提供有關文件的信息和內容,語法如下:
new File(content, name[, options]);
content
指要創建的文件內容,是 ArrayBuffer, View, Blob, DOMString
等類型構成的 數組 或者類數組;name
則是文件的名稱或者路徑;options
參數可選,包含 type
和 lastModified
兩個屬性;
舉例:
var content = new TextEncoder().encode('hello world!');
var file = new File(content, 'test.txt', {
type: 'text/plain', // 可選,默認爲空
lastModified: Date.now() // 可選,後面是默認值
});
console.log(file.name); // test.txt
console.log(file.size); // 12
console.log(file.type); // text/plain
console.log(file.lastModified); // 1583638485180
File
構造函數自身並沒有自帶一些方法,而是繼承了 Blob
的方法,例如:
var file = new File(['hello world!'], 'text.txt');
// 初始化內容可以直接是字符串,只是需要放在數組中
file.text().then(data => {
console.log(data); // hello world!
});
file.arrayBuffer().then(data => {
var text = new TextDecoder().decode(data);
console.log(text); // hello world!
});
其實,一般很少像這樣用 File
接口來直接創建一個文件對象,多數是用在用戶上傳文件等情況,比如在網頁中用 <input type="file" />
標籤來上傳文檔,而用戶點擊上傳後,與文件相關的信息就被包含在了這個 input
標籤的節點引用的 files
屬性中,這個 files
屬性值是一個 FileList
接口的實例,就是包含所有上傳文件的數組,其中每個元素都是一個 File
接口的實例;
通過一個簡單的 demo 進行說明:
<!DOCTYPE HTML>
<html>
<head></head>
<body>
<input type="file" class="upload" />
<!--
如果要上傳多個文件,則使用:
<input type="file" class="upload" multiple />
-->
<input type="submit" value="Upload" onclick="doUpload()" />
<script>
var upload = document.querySelector('.upload');
// 用戶點擊 Upload 按鈕後執行
function doUpload() {
var file = upload.files[0];
console.log(file);
// File {name: "test.txt", lastModified: 1583634142542, lastModifiedDate:
// Sun Mar 08 2020 10:22:22 GMT+0800 (中國標準時間), webkitRelativePath: "", size: 12, …}
}
// 也可以用這種方法獲取文件對象,
// 這個函數中的代碼會在用戶完成上傳操作就執行,即使沒點上傳按鈕
upload.onchange = function(el) {
var file = el.files[0];
// 執行的操作...
}
</script>
</body>
</html>
FileReader
FileReader
是另一個用於讀取文件數據的接口,其實例化後的一些方法與 Blob
中的 .text()
和 .arrayBuffer
方法類似,只不過返回的不再是一個 promise 對象,而是一個基於 事件 的接口;FileReader
一般也用於讀取用戶上傳文件的數據;
語法:
new FileReader(); // 實例化無須提供任何參數
事件處理
既然是以基於事件,那麼就需要一系列處理不同事件的方法,列出如下:
.onabort()
:該事件在讀取操作被中斷時觸發。.onerror()
:該事件在讀取操作發生錯誤時觸發。.onload()
:該事件在讀取操作完成時觸發。.onloadstart()
:該事件在讀取操作開始時觸發。.onloadend()
:該事件在讀取操作結束時(要麼成功,要麼失敗)觸發。.onprogress()
:該事件在讀取Blob時觸發。
以上事件也可以使用 addEventListener()
方法的相應格式來設置回調函數;
加載狀態
因爲是基於事件的接口,所以 FileReader
提供了 readyState
這個屬性,以不同值代表不同的數據加載狀態:
- 0:數據尚未加載;
- 1:數據正在加載中;
- 2:數據加載完成;
數據加載完成後,可以使用 result
這個屬性來獲取文件內容;
數據加載
readAsText()
.readAsText(file[, encoding])
以文本字符串的形式讀取 file
(文件對象或者 Blob)中的數據,以 encoding
格式進行編碼(默認 utf-8);
var file = new File(['abc'], 'test.txt');
var reader = new FileReader();
reader.onloadstart = event => {
console.log('loadstart state:', event.target.readyState);
}
reader.onload = event => {
console.log('load state:', event.target.readyState);
console.log('result:', event.target.result);
}
reader.onloadend = event => {
console.log('loadend state:', event.target.readyState);
}
reader.readAsText(file);
// loadstart state: 1
// load state: 2
// result: abc
// loadend state: 2
readAsArrayBuffer()
readAsArrayBuffer(file)
以 ArrayBuffer
的形式讀取 file
中(文件或 Blob)的數據;
var blob = new Blob(['a', 'b', 'c']);
var reader = new FileReader();
// 使用監聽器觸發效果相同
reader.addEventListener('load', event => {
console.log(event.target.result);
console.log(new Uint8Array(event.target.result));
})
reader.readAsArrayBuffer(blob);
// ArrayBuffer(3) {}
// Uint8Array(3) [97, 98, 99]
readAsDataURL()
readAsDataURL(file)
同樣是讀取 file
中的數據,只是將文件中的內容以 base64 編碼後,放進一個 DataURL
中(內容可以通過 URL 鏈接直接訪問);
var file = new File(['abc'], 'test.txt', {
type: 'text/plain'
})
var reader = new FileReader();
reader.onload = event => {
console.log(event.target.result);
}
reader.readAsDataURL(file);
// data:text/plain;base64,YWJj
如果將最後的輸出內容粘貼複製進瀏覽器的地址欄,回車後就能直接看見文本內容;
呈現數據
DataURL
Data URL 指的是一種 URL
協議,語法格式爲:
data:[<mediatype>][;base64],<data>
可以類比常見的 http:
協議,例如上例中的返回值:
data:text/plain;base64,YWJj
具體用法如之前所述,輸入到瀏覽器地址欄回車後會直接呈現出原內容,比如上例就是一串文本(文件類型被指定爲 text/plain
),如果類型 image/png
等圖片格式的,則會直接顯示該圖片;
Data URL 除了可以通過瀏覽器地址欄訪問,也可以在 HTML 文檔中展示,例如使 <img>
的 src
屬性值等於這個 Data URL,這個標籤就會展示爲相應的圖片,同樣地,數據指定給 <iframe>
的 src
屬性,也可以展示圖片或者文本數據,指定給 <video>
標籤的 src
則可以展示視頻;
ObjectURL
ObjectURL 使用 URL.createObjectURL()
方法創建,返回結果也是一種類型的 URL,類似於上面的 Data URL,區別在於 ObjectURL 的生命週期與當前網站頁面相關,例如 刷新頁面 頁面後不無法繼續訪問了;
例如在本地網頁控制檯中運行下面的代碼:
var blob = new Blob(['abc']);
console.log(URL.createObjectURL(blob));
則會輸出類似下面的內容:
blob:http://127.0.0.1:8080/4064e759-231f-466e-a6ef-778505e56d2b
鏈接臨時有效,會展示數據內容刷新頁面失效,不過格式基本一致;同樣,ObjectURL 也可以用於設置爲 <img>
或 <iframe>
的 src
屬性,進行單獨展示;
需要 注意,createObjectURL()
方法每次調用都會返回一個新的 ObjectURL 對象,即使數據源相同,所以如果調用量較多,可能就會內存劇增,這時需要手動回收,使用的是 revokeObjectURL()
這個方法,示例:
var url = URL.createObjectURL(new Blob(['test']));
URL.revokeObjectURL(url); // 完成回收
文件下載
除了使用 <img
, <iframe>
等標籤對數據進行展示,也可以將文件提供給用戶下載,使用的是 <a>
標籤,把 DataURL 或者 ObjectURL 指定給它的 href
屬性即可,另外還要指定 download
屬性值,不然有可能會是跳轉到相關頁面而不是下載;
一個下載組件的示例:
<a href="data:text/plain;base64,YWJj" download="test.txt" type="text/plain">Download</a>
download
屬性指代下載到用戶本地的文件名稱,不加後綴則系統自動識別類型,同樣 type
屬性也是可選的,可用於固定下載文件類型;
上傳數據
讓用戶通過網頁上傳文件,最重要的當然就是最後的上傳階段了,即把用戶選擇的文件上傳到服務器;下面的例子使用 XMLHttpRequest()
接口來實現數據的上傳;
var file = new File(['hello world!'], 'hello.txt', {
type: 'text/plain'
}); // 此處用於模擬用戶上傳的文件,即有具體的文件名、類型和內容
var xhr = new XMLHttpRequest();
var reader = new FileReader();
// 查看上傳進度
xhr.upload.onprogress = event => {
if (event.lengthComputable) {
console.log('進度:', event.loaded + '/' + event.total);
}
}
// 上傳完成的回調
xhr.upload.onload = event => {
console.log('upload success.');
}
// 上傳地址,參數換成實際地址
xhr.open('POST', 'http://localhost/upload/upload.php');
// 服務器沒有指定文件類型則自行指定
xhr.overrideMimeType('text/plain');
reader.onload = event => {
// 數據讀取完畢就開始上傳
xhr.send(event.target.result);
}
reader.readAsText(file);
另外也可以使用 form 表格來上傳文件,更加直接:
<form action="upload/upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="upload" >
<input type="submit" value="Upload">
</form>
需要 注意 的是,上傳文件時 必須 加上 enctype="multipart/form-data"
,不然上傳上去的只是一個文件名;
接收數據
投我以木瓜,報之以瓊琚;有時也會接收來着服務端的數據,通常就是使用 XMLHttpRequest
來異步獲取文本或 JSON 數據,但是它也能用於獲取其他類型的數據,只不過需要手動設置 responseType
這個屬性進行申明,該屬性支持以下幾個值:
""
:默認值,與 text 類型相同;"text"
:以文本類型響應;"arraybuffer"
:以ArrayBuffer
二進制數據響應;"blob"
:以Blob
類型數據響應;"json"
:響應解析爲 JSON 對象;"document"
:解析爲 HTML 或 XML 內容;
一個接收數據的實例:
var xhr = new XMLHttpRequest();
xhr.responseType = 'arraybuffer';
xhr.onload = () => {
var buffer = xhr.response;
// 可以轉換爲類型化數組進行數據修改
console.log(new Uint8Array(buffer));
}
xhr.open('GET', 'test.png');
xhr.send();
至此,歷經幾番交戰,刀光劍影,戰況激烈空前,難分難解,不下幾十回合,能閱讀至此處的諸位也都是真正的勇士,敢於面對慘淡的生活,正視淋漓的鮮血…又扯遠了,俗話說,物以稀爲貴,人以和爲貴,JavaScript 劍客與二進制隱士此番交戰,不求勝負,若這過程中的原理能被大家理解參透得透徹,也算是名留青史了;
恩怨自了結,情仇終消散,天下沒有不散的宴席,一個人的路也叫江湖,暫且就此別過,江湖再見!