5 分鐘知識系列 | 前端文件基礎:二進制數組

在開發前端「後臺管理系統」時,免不了有操做文件的需求,好比說要運營人員要上傳一份 txt 形式的號碼包文件,一行就是一個號碼,而後展示在前端界面上給運營人員 review,沒問題以後再將號碼包文件上傳。javascript

在咱們理清 Blob 和 FileReader 這些你有可能接觸過的概念以前,咱們先來打牢一下基礎,看看咱們 js 是如何操做內存中的二進制數據?瞭解完這塊部分,可以幫助咱們更好地去操做文件。html

這篇文章,我帶你們理清以下概念:前端

  • ArrayBuffer
  • TypedArray
  • DataView
  • Uint8Array / Uint16Array 等等

前言

ArrayBuffer 對象、TypedArray 對象、DataView 對象是 JavaScript 操做二進制數據的一個接口。這些對象早就存在,屬於獨立的規格,ES6將它們歸入了 ECMAScript 規格,而且增長了新的方法。java

這些對象原始的設計目的,與 WebGL 項目有關。所謂 WebGL,就是指瀏覽器與顯卡之間的通訊接口,爲了知足 JavaScript 與顯卡之間大量的、實時的數據交換,它們之間的數據通訊必須是二進制的,而不能是傳統的文本格式。文本格式傳遞一個 32 位整數,兩端的 JavaScript 腳本與顯卡都要進行格式轉化,將很是耗時。這時要是存在一種機制,能夠像 C 語言那樣,直接操做字節,將 4 個字節的 32 位整數,以二進制形式原封不動地送入顯卡,腳本的性能就會大幅提高。數組

二進制數組就是在這種背景下誕生的。它很像 C 語言的數組,容許開發者以數組下標的形式,直接操做內存,大大加強了 JavaScript 處理二進制數據的能力,使得開發者有可能經過 JavaScript 與操做系統的原生接口進行二進制通訊。瀏覽器

二進制數組的讀寫通常是經過如下三類對象來操做:app

  1. ArrayBuffer
  2. TypedArray
  3. DataView

ArrayBuffer

ArrayBuffer 對象,表明內存之中的一段二進制數據,能夠經過「視圖」進行操做。「視圖」部署了數組接口,這意味着,能夠用數組的方法操做內存。函數

建立一個指定長度的 ArrayBuffer 對象,並讀取該對象的字節大小:post

// 8 表明所須要分配的內存大小(單位:字節)
const buffer = new ArrayBuffer(8);

console.log(buffer.byteLength);
// 8
複製代碼

可是 ArrayBuffer 對象只讀,是不能被直接操做的,只能經過「視圖實例對象」來操做,它們會將緩衝區中的數據表示爲特定的格式,並經過這些格式來讀寫緩衝區的內容。性能

而且,ArrayBuffer 類提供了一個方法判斷某變量是否爲「視圖實例對象」:

console.log( ArrayBuffer.isView(arg) );
複製代碼

而所謂的「視圖實例對象」,也就是「類型數組對象」(TypedArray對象) 或 DataView 對象。

二者的區別在於,後者能夠自定義格式和字節序。例如,前者只能實例化一樣格式(好比都是 Unit8Array 類型)的對象,然後者能夠第一個字節爲 Unit8 類型,第二個字節爲 Int16 類型,等等。

DataView 類型對象筆者平常工做狀況下比較少用,可是應用場景很是豐富,筆者打算另起一篇文章來單獨闡述其使用方法和場景。

TypedArray

TypedArray 視圖類型很特殊,它不是一個構造函數,而是一組構造函數的統稱,也就是說不能在程序中直接使用 TypedArray 來實例化對象,由於它只屬於一個「概念」。

而真正能實例化對象、且隸屬於 TypedArray 視圖的構造函數有以下:

  • Int8Array: 8位有符號整數,長度1個字節
  • Uint8Array:8位無符號整數,長度1個字節
  • Uint8ClampedArray:8位無符號整數,長度1個字節,溢出處理不一樣
  • Int16Array:16位有符號整數,長度2個字節
  • Uint16Array:16位無符號整數,長度2個字節
  • Int32Array:32位有符號整數,長度4個字節
  • Uint32Array:32位無符號整數,長度4個字節
  • Float32Array:32位浮點數,長度4個字節
  • Float64Array:64位浮點數,長度8個字節

咱們在此僅拿 Uint8Array 類型舉例說明,其餘同理的。

首先看看 Uint8Array 每一個元素的所佔字節數:

console.log( Uint8Array.BYTES_PER_ELEMENT );
// 1
// 表示 1 個字節
複製代碼

實例化對象能夠有以下寫法:

// 寫法 1:實例化一個空的視圖對象
new Uint8Array();

// 寫法 2:建立初始化爲 0 的,包含 2 個元素的無符號整型數組
new Uint8Array(2);

// 寫法 3:從 ArrayBuffer 中建立
// uint8Buff 和 uint16Buff 視圖對象底層對應的是同一段內存對象
// 一方修改,會影響到另外一方
const buff = new ArrayBuffer(8);
const uint8Buff = new Uint8Array(buff);
const uint16Buff = new Uint16Array(buff);
console.log(uint8Buff.length);
// 8
複製代碼

另外,很厲害的一點是:普通數組的操做方法和屬性,對 TypedArray 類型數組徹底適用。

ArrayBuffer 與字符串的互相轉換

在肯定好編碼方式的前提下,ArrayBuffer 與字符串是能夠互相轉換的。咱們就經過 String.prototype.charCodeAtString.fromCharCode 來實現一下。咱們知道這兩個函數都是基於 UTF-16 編碼格式將字符串和整數值進行轉換的,因此能夠肯定的編碼方式就是 「UTF-16」。

來看看代碼的實現:

function ab2str (arraybuffer) {
    return String.fromCharCode.apply(
        null,
        new Uint16Array(arraybuffer)
    );
}

function str2ab (str) {
    // UTF-16 編碼中,一個字符在內存中須要佔用兩個字節
    var arraybuffer = new ArrayBuffer(str.length * 2);
    var u16Arr = new Uint16Array(arraybuffer);
    var len = u16Arr.length;

    for (var i=0; i<len; i++) {
        u16Arr[i] = str.charCodeAt(i);
    }
    return u16Arr;
}

console.log(
    ab2str( str2ab('Hello World') )
);
複製代碼

二進制數組的應用

回到一開始的命題,「二進制數組」的知識可以幫助咱們更好地去操做文件。

那咱們來看看,在讀取文件的時候,驗證下使用 ArrayBuffer 方式讀取出來的文件內容是否正確。

建立一份 test.txt 文件,準備讀取

Hello World
複製代碼

前端 html 代碼

<input id="fileInputer" type="file" />
複製代碼

前端 js 代碼

const fileInputer = document.getElementById('fileInputer');

function ab2str (arraybuffer) {
    return String.fromCharCode.apply(
        null,
        // 注意換成了 Uint8Array
        // 由於 txt 文檔在電腦上通常都是 UTF-8 編碼
        new Uint8Array(arraybuffer)
    );
}

fileInputer.addEventListener('change', function (event) {
    const target = event.target;
    if (!target.files[0]) {
        return ;
    }

    const file = evt.target.files[0];
    const reader1 = new FileReader();
    const reader2 = new FileReader();

    // 以 ArrayBuffer 方式讀取,獲取結果後轉成字符串
    reader1.onloadend = function (evt) {
        console.log(
            reader1.result
        );
        console.log(
            ab2str(new Uint8Array(reader1.result))
        );
    };
    reader1.readAsArrayBuffer(file);

    // 以 text 方式讀取,獲取結果後直接展現
    reader2.onloadend = function (evt) {
        console.log(
            reader2.result
        );
    };
    reader2.readAsText(file);
}, false);
複製代碼

看到這裏,你們可能會有個疑問,爲何 FileReader 的實例已經提供了 readAsText 方法了,你還要使用 readAsArrayBuffer 方法費盡地去讀取,再轉換成字符串呢?

是這個道理沒錯,可是實際會有這兩種狀況:

  1. 讀取的 txt 文件內容太多,前端展現須要作分頁;
  2. 讀取的 txt 文件內容太大,整個文件讀到內存中,會很是浪費資源。

我來給你們解答下:

  1. txt 文件內容條目太多,前端展現須要作分頁是正確的,可是爲了節省瀏覽器佔用的內存資源,咱們追求下性能,那固然是須要多少讀多少是最好的;
  2. 在分頁展現的狀況下,一次性使用 readAsText 方法將文件讀到內存,是很是浪費以及損耗性能的,好比,1 G的日誌文件,700 MB 的號碼包文件,分分鐘會把瀏覽器卡死,嚴重影響用戶體驗。

那麼,使用 readAsArrayBuffer 就能夠解決問題了嗎?是的,能夠的,這時候,就須要使用到 Blob 以及 Blob 與文件之間關係的知識了,咱們下一篇文章繼續闡述。

參考連接

相關文章
相關標籤/搜索