预备知识

JS 操作二进制数据涉及到三个基本类型:

ArrayBufferTypedArrayDataView

为了达到最大的灵活性和效率,JavaScript 类型数组(Typed Arrays)将实现拆分为 缓冲视图 两部分。ArrayBuffer 作用是作为缓冲区存放实际的二进制数据,TypedArray 和 DataView 作为视图访问底层的 ArrayBuffer。

ArrayBuffer 是一种数据类型,用来表示一个通用的、固定长度的二进制数据缓冲区;

TypedArray 含类型的Array View,例如 Uint8Array

DataView 是一种底层接口,它提供有可以操作缓冲区中任意数据的读写接口。

Buffer and View
Buffer and View

TypedArray

下面用一个例子来说明 TypedArray 和 ArrayBuffer 的关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const fs = require('fs')

const buf = new ArrayBuffer(16)   // 创建一个 16 bytes 的缓存区
const int32Arr = new Int32Array(buf)   // type=int32 (4 Bytes) array view
const uint8Arr = new Uint8Array(buf)   // type=uint8 (1 Bytes) array view
const arr = [1, 2, 3, 4]
arr.forEach((val, i) => {
  int32Arr[i] = val
})

console.log(int32Arr)
// Int32Array [ 1, 2, 3, 4 ]
console.log(uint8Arr)
// Uint8Array [ 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0 ]

fs.writeFileSync('output.bin', new Buffer(buf))  // 写入到文件

看两次输出的结果,分别是 [ 1, 2, 3, 4 ][ 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0 ]

这个 buffer 的16进制排列为:

1
2
hex: 0100 0000 | 0200 0000 | 0300 0000 | 0400 0000
num:     1           2           3           4

采用16进制显示二进制数据是常用的做法,每一位数为 4bit,所以 1(int32) = 0x0001 。

在 Linux 系统中可使用命令 xxd output.bin 查看二进制。

Int32Array 为有符号32位整型(4 Bytes = 32 bit),Uint8Array 类型为无符号8位整型(1 Byte = 8 bit)。这里表示 TypedArray 中的每个数组成员所占的空间分别为 4 Bytes 和 1 Bytes,int32Arr[0] 即对应 uint8Arr[0:3]

Int32Array [ 1 ] == Uint8Array [ 1, 0, 0, 0 ] 。这里为什么不是对应 Uint8Array [ 0, 0, 0, 1 ] 呢?这是因为这和字节序有关。

TypedArray 使用的是系统字节序 。现在 X86 CPU 使用的是小端字节序,即 最低有效位排列在最高有效位前面 ,例如 0x123456 使用小端字节序表示就是 563412

Tips

多个 view 可以绑定同一个 ArrayBuffer 来进行数据操作,就是类似于 C语言的 Union 的效果。

DataView

很多协议的每个字段是使用不同的字节长度的,因此单一的 TypedArray 在使用上很不方便。DataView 就提供了一个比 TypedArray 更加灵活的操作 ArrayBuffer 的接口。

目前 DataView 提供以下类型的读写方法,方法名称都是以 set/get 开头,例如: setInt8 / getInt8

  • Int8
  • Uint8
  • Int16
  • Uint16
  • Int32
  • Uint32
  • Float32
  • Float64

读方法的函数签名为: dataview.getUint32(byteOffset [, littleEndian])

写方法的函数签名为: dataview.setUint32(byteOffset, value [, littleEndian])

需要注意的是 DataView 默认以大端字节序读写,在最后一个参数传 true 表示使用小端字节序操作。

由于 DataView 可以自由的选择字节序来读写 ArrayBuffer ,我们就可以通过 TypedArray (默认小端)和 DataView 两者的不同来判断出到底运行代码机器的字节序是大端还是小端。

1
2
3
4
5
6
var isLittleEndian = (function () {
  var buf = new ArrayBuffer(2);
  new DataView(buf).setInt16(0, 256, true); // 小端字节序写入 256
  return new Int16Array(buf)[0] === 256;    // 判断机器是否使用小端字节序
} ());
console.log(isLittleEndian);

Buffer

在这里顺便提一下 Node.JS 中的 Buffer 类型 。由于历史的原因,在 Node.JS 诞生之初,TypedArray 还没有被提出,所以 Node.JS 自己实现了个 Buffer 类型,随着 ECMA 2015 标准的提出,Buffer 已经转为使用 Uint8Array 实现。所以 Buffer 名字上给人感觉更像是 ArrayBuffer ,但是其实它是一个视图 (view)。

Buffer instances are also Uint8Array instances.

Reference