Blob、File、ArrayBuffer、TypedArray、DataView究竟應該如何應用

語言: CN / TW / HK

theme: awesome-green

引言

Blob、File、ArrayBuffer、TypedArray、DataView、Object URL ..等等 Web 應用中有關於進位制的應用你瞭解多少?

其實我們可以利用上述 Web API 來做很多事情,並且它們之前存在著惟妙惟肖的關聯關係。

來吧,一篇文章讓你帶你暢遊 Web 世界中最直觀的二進位制應用。

ArrayBuffer

概念

ArrayBuffer 物件用來表示通用的、固定長度的原始二進位制資料緩衝區。它是一個位元組陣列,通常在其他語言中稱為“byte array”。

你不能直接操作 ArrayBuffer 的內容,而是要通過型別陣列物件或 DataView 物件來操作,它們會將緩衝區中的資料表示為特定的格式,並通過這些格式來讀寫緩衝區的內容。

它的含義類似 NodeJs 中的 Buffer 。簡單來說,我們可以通過 ArrayBuffer 來開闢一段二進位制資料空間,但是它只能通過 TypedArray 或者 DataView 來進行操作。

用法

在計算機中我們都瞭解每 8 位代表一個位元組,在 Web Api 中提供給了我們一個 ArrayBuffer 內建模組,通過例項化 new ArrayBuffer(number) 可以建立對應固定 number 大小的位元組長度緩衝區。

比如:

```ts // 建立一個長度為 8 的 ArrayBuffer ,此時開闢一個固定 8 個位元組的緩衝區也就是 64 位 const buffer = new ArrayBuffer(8);

console.log(buffer.byteLength); // expected output: 8 ```

具體關係如下圖所示:

image.png

ArrayBuffer 代表的含義就和他的名字類似,它是一個數組,陣列中包含的元素就是位元組。

上圖中每一個無間隔小塊代表一個位,8位組成一個位元組。每一個存在間隔的長塊表示一個位元組,整個8個位元組組成了我們建立的 buffer 物件。

TypedArray

上邊我們說到通過 new ArrayBuffer(number) 可以建立對應長度位元組的固定緩衝區。

同時也提供要操作建立的緩衝區例項物件,需要通過型別陣列物件(TypedArray)或者 DataView 來進行操作。

那麼我們就先來看一看什麼是 TypedArray。

概念

型別化陣列TypedArray) 物件描述了一個底層的二進位制資料緩衝區(binary data buffer)的一個類陣列檢視(view)。

稍微翻譯下上邊的話,也就是說可以通過 TypedArray 來操作 ArrayBuffer 的例項。

其次,沒有名為 TypedArray 的全域性屬性,也沒有一個名為 TypedArray 的建構函式。相反,有許多不同的全域性屬性,它們的值是特定元素型別的型別化陣列建構函式

這句話簡單來講,你可以將 TypedArray 理解為一種介面的形式。所謂 TypedArray 它並不包含具體的實現而是代表一系列具有相同特性(類陣列檢視)的集合概念。

也就是說 TypedArray 不可被直接例項化,本身也無法訪問。但是它有很多種不同的實現方式。

也許有部分同學不太理解 TypedArray 是一種介面的形式,彆著急,稍後我們來舉個例子你馬上就會明白了。

關於 TypedArray 的具體實現,它存在以下方式:

| 型別 | 單個元素值的範圍 | 大小(bytes) | 描述 | Web IDL 型別 | C 語言中的等價型別 | | :----------------------------------------------------------- | :------------------------------------------- | :---------- | :------------------------------------------------ | :-------------------- | :----------------------------- | | Int8Array | -128127 | 1 | 8 位二進位制有符號整數 | byte | int8_t | | Uint8Array | 0255 | 1 | 8 位無符號整數(超出範圍後從另一邊界迴圈) | octet | uint8_t | | Uint8ClampedArray | 0255 | 1 | 8 位無符號整數(超出範圍後為邊界值) | octet | uint8_t | | Int16Array | -3276832767 | 2 | 16 位二進位制有符號整數 | short | int16_t | | Uint16Array | 065535 | 2 | 16 位無符號整數 | unsigned short | uint16_t | | Int32Array | -21474836482147483647 | 4 | 32 位二進位制有符號整數 | long | int32_t | | Uint32Array | 04294967295 | 4 | 32 位無符號整數 | unsigned long | uint32_t | | Float32Array | -3.4E383.4E38 最小正數為:1.2E-38 | 4 | 32 位 IEEE 浮點數(7 位有效數字,如 1.1234567) | unrestricted float | float | | Float64Array | -1.8E3081.8E308 最小正數為:5E-324 | 8 | 64 位 IEEE 浮點數(16 有效數字,如 1.123...15) | unrestricted double | double | | BigInt64Array | -2^632^63-1 | 8 | 64 位二進位制有符號整數 | bigint | int64_t (signed long long) | | BigUint64Array | 02^64 - 1 | 8 | 64 位無符號整數 | bigint | uint64_t (unsigned long long |

Uint8Array

乍一看特別多對吧,其實它們的用法是類似的。這裡我們以為 Int8Array 和 Unint8Array 來舉一個簡單的例子:

```js // 建立8個位元組長度的快取衝 const buffer = new ArrayBuffer(8);

// 將buffer轉化為Uint8Array // Uint8Array中每一個元素表示一個位元組(8位) const uint8Array = new Uint8Array(buffer);

// log: [0, 0, 0, 0,0, 0, 0, 0] console.log(uint8Array);

// 64位 8位元組(log:8) console.log(uint8Array.length); ```

我們稍微用一張圖來解釋下對應的 Unint8Array 的含義:

image.png

  • 上述程式碼我們首先通過 new ArrayBuffer(8) 建立了 8 個位元組大小的緩衝區。

  • 之後通過new Uint8Array(buffer)建立了一個 Unint8Array。

所謂 Unint8Array 中每個元素代表 8 位(一個位元組)大小,我們可以通過 Unint8Array 來操控剛才建立的 ArrayBuffer 。

之後我們列印了,unint8Array 的長度,因為 buffer 大小為8個位元組64位自然通過 buffer 建立的 unit8Array 大小為 64/8 = 8 個長度大小的 unit8Array。

Uint8Array 意味無符號整形陣列,故而在二進位制中每個元素最大為8個1,最小為8個0。自然轉化為10進位制後每個元素範圍為0~255。

同理 Int8Array 表示有符號的整形陣列,每個位首代表正負符號。故而 Int8Array 每個元素大小範圍為-128~127。

關於 Uint8Array 更加詳盡的 API 你可以查閱這裡

Uint16Array

在清楚了 Uint8Array 代表的含義後,趁熱打鐵我們來看看 Uint16Array 是如何使用的。

其實在上述我們說過,無論是 Uint16Array 、 Uint8Array 還是其他類似 API 本質上用法都是一樣的。它們統一被歸類為 TypedArray。

```js // 建立8個位元組長度的快取衝 const buffer = new ArrayBuffer(8);

// 將buffer轉化為Uint16Array // Uint8Array中每一個元素表示兩個位元組(16位) const uint8Array = new Uint16Array(buffer);

// log: Uint16Array(4) [ 0, 0, 0, 0 ] console.log(uint8Array);

// 64位 8位元組 -> 4個元素(log:4) console.log(uint8Array.length); ```

image.png

同樣,Uint16Array 代表16位無符號整數,Uint16Array 中每個元素儲存16位(2個位元組)。

自然,我們輸出它的長度位 4。Int16Array 同樣每個元素儲存為有符號 16 位整數,每個元素位首位置表示正負數。

換算為10進位制,Uint16Array中每個元素大小範圍為 0 ~ 2^16 也就是 0 ~ 65536 。

DataView

在瞭解了 TypedArray 之後,我們來看看另一種操作 ArrayBuffer 的方式:DataView。

相較於 TypedArray,DataView 對於 ArrayBuffer 的操作更加靈活。

我們可以發現在 TypedArray 中操作二進位制 ArrayBuffer 時每個元素佔用的位元組大小是固定的,要麼每個元素佔用8位、16位或者32位。

而 DataView 對於 ArrayBuffer 的操作就顯得更加靈活了,我們可以通過 DataView 從 ArrayBuffer 中自由的讀寫多種資料型別,從而控制位元組順序。

概念

DataView 檢視是一個可以從 二進位制ArrayBuffer 物件中讀寫多種數值型別的底層介面,使用它時,不用考慮不同平臺的位元組序問題。

簡單來講,想較與 TypedArray 每個元素中固定的位元組大小,我們可以通過 DataView 來自由的操作 ArrayBuffer 。

## 用法

同樣關於 DataView 的操作 Api 也是非常龐大的,但是它們都是大同小異。

具體所有相關 API 你可以在這裡查閱

這裡我們同樣以 8位無符號以及 16位無符號整數來舉例如何通過 DataView 來操控 ArrayBuffer:

建立DataView

js new DataView(buffer [, byteOffset [, byteLength]])

建立 DataView 支援傳入三個引數:

  • 第一個引數 buffer 為必填,它支援傳入一個 ArrayBuffer 表示 DataView 中的源資料。

  • 第二個引數 byteOffset 選填,它表示建立 DataView 時開頭從 buffer 的哪個位元組開始,可以作為啟始偏移量。未指定時,預設從第一個位元組開始。

  • 第三個引數 btyeLength 選填,它表示建立該 DataView 時的長度,當不傳遞預設時表示匹配 buffer 的長度。

```js // 建立8個位元組長度的快取衝 const buffer = new ArrayBuffer(8);

// 根據傳入的buffer 從第一個位元組開始,並且位元組長度為匹配buffer的長度 const dataView = new DataView(buffer);

/* * DataView { byteLength: 8, byteOffset: 0, buffer: ArrayBuffer { [Uint8Contents]: <00 00 00 00 00 00 00 00>, byteLength: 8 } } / console.log(dataView, 'dataView');

// log: 8 console.log(dataView.byteLength, 'dataView'); ```

比如上述的程式碼,我們通過 new DataView 建立了對應 buffer 的 DataView 。

接下來我們來看看如何利用 DataView 來操作 ArrayBuffer,這裡我們以為 setUint8 來舉例:

setUint8

setUint8() 表示從DataView起始位置以byte為計數的指定偏移量(byteOffset)處儲存一個8-bit數(無符號位元組).

比如:

```js // 建立8個位元組長度的快取衝 const buffer = new ArrayBuffer(8);

// 根據傳入的buffer 從第一個位元組開始,並且位元組長度為匹配buffer的長度 const dataView = new DataView(buffer);

// 將DataView中偏移量為0個位元組的位元組,也就是第一個位元組設定為十進位制的1 dataView.setUint8(0, 1); // 將DataView中偏移量為1個位元組的位元組,也就是第二個位元組設定為十進位制的2 dataView.setUint8(1, 2); ```

具體的 DataView.prototype.setUint8 API 你可以在這裡看到

setUint8 支援傳入兩個引數,分別表示:

  • 第一引數為 byteOffset,它表示設定的位元組偏移量,偏移量單位為位元組。

  • 第二個引數 value,它表示設定的值。為 10 進製表示法。

比如上述我們 Demo 中通過 setUint8 來操縱建立好的 ArrayBuffer ,當首次建立ArrayBuffer時內部所有位全部為空也就是:

image.png

當代碼執行到 dataView.setUint8(0, 1) 時,表示我們將要給 dataView 中以 8位(一個位元組位單位)設定偏移量為 0 (表示第一個位元組),設定它的值為 1 (10進位制)。

此時,dataView 中的 ArrayBuffer 如下圖所示:

image.png

分別將第一個位元組(8位)的值變為 1 和將第二個位元組變為 10 進位制的 2。

關於10進位制和2進位制的轉化,這裡我就不做過多介紹了。這裡基礎不太明白的同學可以自行百度。

此時,我們就通過 DataView 將 ArrayBuffer 中的 第一個位元組變成了 1 以及將第二個位元組變成了 2。

getUint8

瞭解了 setUint8 之後,我們一起來看看 getUint8 吧。

getUint8() 方法``從DataView相對於起始位置偏移 n 個位元組處開始,獲取一個無符號的8-bit整數(一個位元組).

getUint8 的用法和 setUint8 的用法類似,只不過一個是作為獲取另一個是作為設定來說的。

```js // 建立8個位元組長度的快取衝 const buffer = new ArrayBuffer(8);

// 根據傳入的buffer 從第一個位元組開始,並且位元組長度為匹配buffer的長度 const dataView = new DataView(buffer);

// 將DataView中偏移量為0個位元組的位元組,也就是第一個位元組設定為十進位制的1 dataView.setUint8(0, 1); // 將DataView中偏移量為1個位元組的位元組,也就是第二個位元組設定為十進位制的2 dataView.setUint8(1, 2);

// 從dataView中偏移第0個位元組,也就是第一個位元組,獲取8位 // log: 1 dataView.getUint8(0);

// 從dataView中偏移第一個位元組獲取八位,也就是獲取第二個位元組的值 // log: 2 dataView.getUint8(1);

console.log(dataView.getUint8(0)); console.log(dataView.getUint8(1));

```

相信通過上述的例子配合註釋對於 getInt8 表示的含義大家應該都可以明白了。

setUint16 & getUint16

接下來我們在看看看 DataView 中另一組 Api : setUint16 和 getUnint16。

setUint16() DataView起始位置以byte為計數的指定偏移量(byteOffset)處儲存一個16-bit數(無符號短整型).

setUint16 和 setUint8 用法是完全一致的,唯一的區別就是setUint16設定的是後續16位也就是兩個位元組的值,而setUint8設定的僅僅是後續8位也就是一個位元組的值。

同理,getUnit16 和 getUint8 也是同樣。

我們來看一個簡短的例子:

```js // 建立8個位元組長度的快取衝 const buffer = new ArrayBuffer(8);

// 根據傳入的buffer 從第一個位元組開始,並且位元組長度為匹配buffer的長度 const dataView = new DataView(buffer);

// 將DataView中偏移量為0個位元組的位元組,也就是第一個位元組設定為十進位制的1 dataView.setUint8(0, 1); // 將DataView中偏移量為1個位元組的位元組,也就是第二個位元組設定為十進位制的2 dataView.setUint8(1, 2);

// 從dataView中偏移第0個位元組,也就是第一個位元組,獲取8位 // log: 1 dataView.getUint8(0);

// 從dataView中偏移第一個位元組獲取八位,也就是獲取第二個位元組的值 // log: 2 dataView.getUint8(1);

// 偏移量為0個位元組,獲取後續16位大小(也就是獲取前兩個位元組大小) // log: 258 dataView.getUint16(0);

// 偏移量為2個位元組,設定後16位大小為256(也就是設定第三個位元組和第四個位元組大小和為256) dataView.setUint16(2, 256);

// 偏移量為2個位元組,獲取後16位大小 // log: 256 dataView.getUint16(2); ```

同樣,我們來用一張圖來表示:

image.png

結合圖示來理解上述的程式碼就會容易許多,相對 int8 來說 int16 與它不同僅僅是 8 位和 16位的區別。

當然關於 DataView 還有許多 16位 、32位等等之類 API,但是用法都是大同小異。大家掌握了上述的含義之後都是觸類旁通的。

ArrayBuffer & TypedArray & DataView

上邊我們理清了什麼是 ArrayBuffer 以及 TypedArray 和 DataView 與它的關係。

本質上,ArrayBuffer 位元組陣列就是一段固定長度大小的二進位制資料緩衝區。它並不能直接操作,我們需要通過 TypedArray 和 DataView 來進行對於 ArrayBuffer 的操作。

當然,它們三者之間也是可以互相轉化的。同樣,我們用一張圖來進行階段性的總結:

image.png

我們可以通過 new DataView 構造 DataView 例項,同樣可以通過 new TypedArray 來將 buffer 例項轉化為 TypedArray 進行操作。

同樣,也可以通過它們各自的 buffer 屬性來獲取對應 ArrayBuffer 的內容。

```js // 建立8個位元組長度的快取衝 const buffer = new ArrayBuffer(8);

const dataView = new DataView(buffer); // 獲取對應buffer內容 console.log(dataView.buffer)

const typedArray = new Uint8Array(buffer); // 獲取對應buffer內容 console.log(typedArray.buffer); ```

Blob

瞭解了 Web 的一些基礎二進位制操作後,我們來看看 Web 中基於它們的延伸。

相信大家在日常工作中或多多少都遇到過 blob 相關的應用,比如 blob 格式的 Url 以及對於檔案上傳中切片等等應用場景。接下來,我們一起來看看所謂 blob 物件。

基礎概念

Blob 物件表示一個不可變、原始資料的類檔案物件。它的資料可以按文字或二進位制的格式進行讀取,也可以轉換成 ReadableStream 來用於資料操作。

注意 File 物件是繼承與 blob 的,我們會在之後探討 File 。

js const aBlob = new Blob( array, options );

Blob() 建構函式返回一個新的 Blob 物件。 blob的內容由引數陣列中給出的值的串聯組成。

通過 new Blob 可以建立一個新的 blob 物件例項,建構函式支援接受兩個引數:

  • 第一個引數 array 是一個由ArrayBufferArrayBufferViewBlobDOMString 等物件構成的 Array ,或者其他類似物件的混合體,它將會被放進 Blob。DOMStrings會被編碼為UTF-8。

  • 第二個引數 options 是一個物件,它擁有如下屬性:

    • type,預設值為 "",它代表了將會被放入到blob中的陣列內容的MIME型別。

    • endings,預設值為"transparent",用於指定包含行結束符\n的字串如何被寫入。 它是以下兩個值中的一個: "native",代表行結束符會被更改為適合宿主作業系統檔案系統的換行符,或者 "transparent",代表會保持blob中儲存的結束符不變。

我們來試試建立一個 blob 物件:

```js

const name = JSON.stringify({ name: '19QIngfeng' });

// 傳入DOMString建立blob const blob = new Blob([name], { type: 'application/json', });

// log: 21 utf8中一個英文代表一個位元組 console.log(blob.size);

const buffer = new ArrayBuffer(8);

// 傳入ArrayBuffer建立blob const bufferToBlob = new Blob([buffer]);

// log: 8 console.log(bufferToBlob.size); ```

讀取blob內容

通過上邊的基礎內容,我們清楚瞭如何利用 DOMString、ArrayBuffer 等建立 blob 物件,但是如何讀取 blob 中的內容呢?

這個時候,就引出了另一個關於檔案操作中的常見 Web Api :fileReader

FileReader 物件允許Web應用程式非同步讀取儲存在使用者計算機上的檔案(或原始資料緩衝區)的內容,使用 File 或 Blob 物件指定要讀取的檔案或資料。

我們可以結合 FileReader Api 來讀取對應 blob 的內容,並且將它轉化為各種我們需要得到的格式:

```js const name = JSON.stringify({ name: '19QIngfeng' });

// 傳入DOMString建立blob
const blob = new Blob([name], {
  type: 'application/json',
});

/**
 *
 * @param {*} blob blob 物件
 * @param {*} type 輸出的結果
 */
function getBlobByType(blob, type) {
  const fileReader = new FileReader(blob);
  switch (type) {
    // 讀取檔案的 ArrayBuffer 資料物件.
    case 'arrayBuffer':
      fileReader.readAsArrayBuffer(blob);
      break;
      // 讀取檔案為的字串
    case 'DOMstring':
      fileReader.readAsText(blob, 'utf8');
      break;
      // 讀取檔案為data: URL格式的Base64字串
    case 'dataUrl':
      fileReader.readAsDataURL(blob);
      break;
      // 讀取檔案為檔案的原始二進位制資料(已廢棄不推薦使用)
    case 'binaryString':
      fileReader.readAsBinaryString(blob);
      break;
    default:
      break;
  }

  return new Promise((resolve) => {
    // 當檔案讀取完成時候觸發
    fileReader.onload = (e) => {
      // 獲取最終讀取結果
      const result = e.target.result;
      resolve(result);
    };
  });
}

// ArrayBuffer 物件
getBlobByType(blob, 'arrayBuffer').then((res) => console.log(res));

// {"name":"19QIngfeng"}
getBlobByType(blob, 'DOMstring').then((res) => console.log(res));

// data:application/json;base64,eyJuYW1lIjoiMTlRSW5nZmVuZyJ9
getBlobByType(blob, 'dataUrl').then((res) => console.log(res));

// {"name":"19QIngfeng"}
getBlobByType(blob, 'binaryString').then((res) => console.log(res));

```

上邊的程式碼我們通過 fileReader 對應的 API 傳入 blob 物件,從而讀取 blob 內容為各種格式的內容。

Blob & ArrayBuffer

細心的小夥伴可能已經發現了,此時,我們又清楚了 ArrayBuffer 和 Blob 微妙的關係。

讓我們繼續再來完善上邊的關係圖:

image.png

關於 File 介面,它提供有關檔案的資訊,並允許網頁中的 JavaScript 訪問其內容。

通常情況下, File 物件是來自使用者在一個 <input> 元素上選擇檔案後返回的 FileList 物件,也可以是來自由拖放操作生成的 DataTransfer 物件,或者來自 HTMLCanvasElement 上的 mozGetAsFile() API。

簡單來說,File 就是基於 Blob 而來。它擁有Blob的所有功能的同時擴充套件了一系列關於檔案的屬性。

Object URL

概念

大多數情況下,我們可以看到一些網頁內部可以看到一些諸如此類的 Blob Url:

image.png

我們可以看到視訊標籤 vide 的 src 屬性正式一個 Blob 型別的 Url。

關於 Blob URL/Object URL 其實它們是一種偽協議,允許將 Blob 和 File 物件用作影象、二進位制資料下載連結等的 URL 源。它的好處其實有很多,比如:

平常我們並不可以直接處理 Image 標籤之類的原始二進位制資料,所以對於圖片等需要 Url 作為源的標籤通常做法是將圖片上傳到伺服器上得到一個 Url 從而通過 URL 載入二進位制資料。

與其上傳二進位制資料,然後通過 URL 將其返回,不如使用 Blob/Object Url 無需額外的步驟,使用瀏覽器本地 Api 即可直接訪問資料而不需要通過伺服器來上傳資料。

當然,一些小夥伴可能會有疑惑。Base64 字串編碼不也可以解決上述說的問題嗎。重點是相較於 base64 編碼來說, Blob 是純二進位制位元組陣列,不會像 Data-URI 那樣有任何顯著的開銷,這使得它們處理起來更快更小。

同時這些 URL 只能在瀏覽器的單個例項和同一會話(即頁面/文件的生命週期)中本地使用,這意味者離開當前瀏覽器例項這個 URL 就會失效。

我們可以通過 URL.createObjectURL(object) 來建立對應的 Object URL,這個方法會返回一個 DOMString 字串,其中包含一個表示引數中給出的物件的URL。

同時,這個 URL 的生命週期和建立它的視窗中的 document 繫結。這個新的URL 物件表示指定的 File 物件或 Blob 物件。

在建立時候它會接受一個引數:

同樣它會返回一個DOMString包含了一個物件URL,該URL可用於指定源 object的內容。

返回的 DOMString 格式為 blob:<origin>/<uuid>

當然,當在你的網頁上不再使用通過 URL.createObjectURL(object) 建立的 URL 時,記得使用 URL.revokeObjectURL(url) 來主動釋放它們。

使用

我們來結合一個例項來看看如何使用 URL.createObjectURL :

```JS const name = JSON.stringify({ name: '19QIngfeng', });

// 傳入DOMString建立blob const blob = new Blob([name], { type: 'application/json', });

// 建立 Object Url const url = URL.createObjectURL(blob);

const aLink = document.createElement('a');

// href屬性 aLink.href = url; // 定義下載的檔名 aLink.download = 'name.json';

// 派發a連結的點選事件 aLink.dispatchEvent(new MouseEvent('click'));

// 下載完成後,釋放 URL.createObjectURL() 建立的 URL 物件。 URL.revokeObjectURL(url); ```

這段 JS 程式碼會在我們開啟 html 頁面後自動下載一個 name.json 的檔案,而下載的 name.json 的 URL 來源正是我們通過 URL.createObjectURL(blob); 建立的。

URL & Blob

根據上述提到的 Object URL 的基本概念以及與 Blob 之間的聯絡,趁熱打鐵我們來在繼續完善上述的關係圖:

image.png

上圖可以看到一個完整清晰的流程,我們可以通過 URL.createObjectURL 將 blob 轉化為 url 使用。

寫在結尾

首先,感謝每一位可以看到這裡的小夥伴。

當然,文章中對於 Web 世界中的二進位制應用僅僅起到了一個拋磚引玉的作用。

對於一些更加底層的二進位制基礎,比如基數、位權等等。文章並沒有過多涉獵,當然有興趣的小夥伴可以私下去補充這部分基礎知識。

同時,對於一些更多 ArrayBuffer 後續的應用,比如 Blob 與 Canvas 以及 image 與 Canvas 、 DataUrl 之間的關係這裡我也沒有過多延伸。

後續,如果有小夥伴對於這部分感興趣我可以額外補充一篇文章去延伸更多在 Web 中這些進位制 API 的應用和關聯。

最後,大家加油!