JavaScript 裏面的二進制

概述

最近在做 IOT 設備配網開發的時候,處理了很多跟二進制、字節相關的事情,總結了一下 JavaScript 中有關二進制方面的一些知識點。

二進制和字節

首先,現代計算機是基於二進制的,從現代計算機電路來說,只有高電平 / 低電平兩種狀態,即爲 0/1 狀態,計算機中所有的數據按照具體的編碼格式以二進制的形式存儲在設備中。

計算機通信和存儲的時候都是以 0101 這樣的二進制數據爲基礎來做處理的,這兒的一個 0 和 1 佔的地方就叫 bit(位),即一個二進制位。可以看出位(bit)是長度單位。8 位組成一個字節,所以字節 (Byte) 也是長度單位。

位和字節的換算關係如下:

1Byte=8bit

1KB=1024B

1MB=1024KB(2 的十次方)

二進制的計算

二進制數據的計算指的是位數據的計算,也就是位運算。

位運算分爲以下幾種操作:

ZUD6qS

注意:負數按補碼形式參加按位與運算。

原碼:用最高位表示符號位,其餘位表示數值位的編碼稱爲原碼。其中,正數的符號位爲 0,負數的符號位爲 1。

正數的原碼、反碼、補碼均相同。

負數的反碼:原碼的符號位保持不變,其餘位逐位取反,即可得原碼的反碼。
負數的補碼:在反碼的基礎上加 1 即得該原碼的補碼。


例如:
+11 的原碼爲: 0000 1011
+11 的反碼爲: 0000 1011
+11 的補碼爲: 0000 1011

-7 的原碼爲:1000 0111
-7 的反碼爲:1111 1000
-7 的補碼爲:1111 1001

位運算的應用很多,這裏講一個經典的,交換兩個數。

通常交換兩個數的做法如下,比如交換 a 和 b:

let temp = a;
a = b;
b = temp;

如果我們用位運算來做

a ^= b;
b ^= a;
a ^= b;

使用位運算可以少定義一個變量 temp,節省一個點內存空間;

字節順序

字節順序涉及到二進制數據在內存中怎麼存儲和網絡數據的傳輸,假設我們定義一個變量 let value = 0x12345678,它在內存中是怎麼存儲的?

前面我們說過計算機裏存儲數據都是以二進制的形式存儲的,假設一個整型佔 4 個字節,那麼先將它轉成二進制:

parseInt(12).toString(2)

00010010 00110100 01010110 01111000。

按照正常閱讀習慣,我們認爲它在計算機內部的存儲格式爲:

低地址
buf[0] (0x12) -- 高位字節
buf[1] (0x34)
buf[2] (0x56)
buf[3] (0x78) -- 低位字節
高地址

這種存儲模式叫大端模式。相對的還有小端模式

大端和小端

Little-Endian: 低地址存放低位,如下:

低地址
buf[0] (0x78) -- 低位字節
buf[1] (0x56)
buf[2] (0x34)
buf[3] (0x12) -- 高位字節
高地址

網絡傳輸一般採用大端序,也被稱之爲網絡字節序。

計算機內部的字節存儲序列叫本機序,不同 CPU 會有不同,摘自維基百科上的一段說明:

x86、MOS Technology 6502、Z80、VAX、PDP-11等處理器爲小端序;
Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC(除V9外)等處理器爲大端序;
ARM、PowerPC(除PowerPC 970外)、DEC Alpha、SPARC V9、MIPS、PA-RISC及IA64的字節序是可配置的。

總結:採用大端方式進行數據存放符合人類的正常思維,而採用小端方式進行數據存放利於計算機處理。到目前爲止,採用大端或者小端進行數據存放,其孰優孰劣也沒有定論。

創建二進制數據

基本的二進制對象是 ArrayBuffer —— 對固定長度的連續內存空間的引用。

let buffer = new ArrayBuffer(16); // 創建一個長度爲 16 的 buffer
console.log(buffer.byteLength); // 16

它會分配一個 16 字節的連續內存空間,並用 0 進行預填充

注意:ArrayBuffer 不是某種東西的數組 讓我們先澄清一個可能的誤區。ArrayBuffer 與 Array 沒有任何共同之處:

ArrayBuffer 是一個內存區域。它裏面存儲了什麼?無從判斷。只是一個原始的字節序列。如要操作 ArrayBuffer,我們需要使用 “視圖” 對象。

視圖對象

視圖對象本身並不存儲任何東西。它是一副 “眼鏡”,透過它來讀寫存儲在 ArrayBuffer 中的字節。

例如:

因此,一個 16 字節 ArrayBuffer 中的二進制數據可以解釋爲 16 個 “小數字”,或 8 個更大的數字(每個數字 2 個字節),或 4 個更大的數字(每個數字 4 個字節),或 2 個高精度的浮點數(每個數字 8 個字節)。

使用視圖操作二進制數據

ArrayBuffer 是原始的二進制數據。

但是,如果我們要操作它,我們必須使用視圖(view),例如:

let buffer = new ArrayBuffer(16); // 創建一個長度爲 16 的 buffer

let view = new Uint32Array(buffer); // 將 buffer 視爲一個 32 位整數的序列

console.log(Uint32Array.BYTES_PER_ELEMENT); // 每個整數 4 個字節

console.log(view.length); // 4,它存儲了 4 個整數
console.log(view.byteLength); // 16,字節中的大小

// 讓我們寫入一個值
view[0] = 123456;

// 遍歷值
for(let num of view) {
  console.log(num); // 123456,然後 0,0,0(一共 4 個值)
}

TypedArray

所有這些視圖(Uint8Array,Uint32Array 等)的通用術語是 TypedArray。它們共享同一方法和屬性集。

請注意,沒有名爲 TypedArray 的構造器,它只是表示 ArrayBuffer 上的視圖的通用總稱術語,包Int8Array,Uint8Array 等

當你看到 new TypedArray 之類的內容時,它表示 new Int8Array、new Uint8Array 等。

TypedArray 的行爲類似於常規數組:具有索引,並且是可迭代的。

一個 TypedArray 的構造器(無論是 Int8Array 或 Float64Array),其行爲各不相同,並且取決於參數類型。

參數有 5 種變體:

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();`
  1. 如果給定的是 ArrayBuffer 參數,則會在其上創建視圖。前面我們已經用過該語法了。
  1. 如果給定的是 Array或任何類數組對象,則會創建一個相同長度的類型化數組,並複製其內容。

    我們可以使用它來預填充數組的數據:

    let arr = new Uint8Array([0, 1, 2, 3]);
    console.log( arr.length ); // 4,創建了相同長度的二進制數組
    console.log( arr[1] ); // 1,用給定值填充了 4 個字節(無符號 8 位整數)`
  2. 如果給定的是另一個 TypedArray,也是如此:創建一個相同長度的類型化數組,並複製其內容。如果需要的話,數據在此過程中會被轉換爲新的類型。

     let arr16 = new Uint16Array([1, 1000]);
     let arr8 = new Uint8Array(arr16);
     console.log( arr8[0] ); // 1
     console.log( arr8[1] ); // 232,試圖複製 1000,但無法將 1000 放進 8 位字節中(詳述見下文)。
  3. 對於數字參數 length —— 創建類型化數組以包含這麼多元素。它的字節長度將是 length 乘以單個 TypedArray.BYTES_PER_ELEMENT 中的字節數:

    let arr = new Uint16Array(4); // 爲 4 個整數創建類型化數組 console.log( Uint16Array.BYTES_PER_ELEMENT ); // 每個整數 2 個字節 console.log( arr.byteLength ); // 8(字節中的大小)

  4. 不帶參數的情況下,創建長度爲零的類型化數組。

我們可以直接創建一個 TypedArray,而無需提及 ArrayBuffer。但是,視圖離不開底層的 ArrayBuffer,因此,除第一種情況(已提供 ArrayBuffer)外,其他所有情況都會自動創建 ArrayBuffer。

如要訪問底層的 ArrayBuffer,那麼在 TypedArray 中有如下的屬性:

因此,我們總是可以從一個視圖轉到另一個視圖:

let arr8 = new Uint8Array([0, 1, 2, 3]);

// 同一數據的另一個視圖
let arr16 = new Uint16Array(arr8.buffer);

下面是類型化數組的列表:

越界行爲

如果我們嘗試將越界值寫入類型化數組會出現什麼情況?不會報錯。但是多餘的位被切除。

例如,我們嘗試將 256 放入 Uint8Array。256 的二進制格式是 100000000(9 位),但 Uint8Array 每個值只有 8 位,因此可用範圍爲 0 到 255。

對於更大的數字,僅存儲最右邊的(低位有效)8 位,其餘部分被切除:

因此結果是 0。

257 的二進制格式是 100000001(9 位),最右邊的 8 位會被存儲,因此數組中會有 1:

換句話說,該數字對 28 取模的結果被保存了下來。示例如下

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000(二進制表示)

uint8array[0] = 256;
uint8array[1] = 257;

console.log(uint8array[0]); // 0
console.log(uint8array[1]); // 1

Uint8ClampedArray 在這方面比較特殊,它的表現不太一樣。對於大於 255 的任何數字,它將保存爲 255,對於任何負數,它將保存爲 0。此行爲對於圖像處理很有用。

DataView

DataView 視圖是一個可以從ArrayBuffer對象中讀寫多種數值類型的底層接口,在讀寫時不用考慮平臺字節序問題。

語法:

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

例如,這裏我們從同一個 buffer 中提取不同格式的數字:

// 4 個字節的二進制數組,每個都是最大值 255
let buffer = new Uint8Array([1, 2, 3, 4]).buffer;

let dataView = new DataView(buffer);

// 在偏移量爲 0 處獲取 8 位數字
alert( dataView.getUint8(0) ); // 1

// 現在在偏移量爲 0 處獲取 16 位數字,它由 2 個字節組成,一起解析爲 65535
alert( dataView.getUint16(0) ); // 258(最大的 16 位無符號整數)

// 在偏移量爲 0 處獲取 32 位數字
alert( dataView.getUint32(0) ); // 16909060(最大的 32 位無符號整數)

dataView.setUint32(0, 0); // 將 4 個字節的數字設爲 0,即將所有字節都設爲 0

當我們將混合格式的數據存儲在同一緩衝區(buffer)中時,DataView 非常有用。例如,當我們存儲一個成對序列(16 位整數,32 位浮點數)時,用 DataView 可以輕鬆訪問它們。

總結

ArrayBuffer 是核心對象,是對固定長度的連續內存區域的引用。

幾乎任何對 ArrayBuffer 的操作,都需要一個視圖。

  1. 它可以是 TypedArray:
  1. DataView —— 使用方法來指定格式的視圖,例如,getUint8(offset)。

在大多數情況下,我們直接對類型化數組進行創建和操作,而將 ArrayBuffer 作爲 “共同之處(common denominator)” 隱藏起來。我們可以通過 .buffer 來訪問它,並在需要時創建另一個視圖。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2M_8K47I_fON9ba8nZODNw