JS `DataView`:对 `ArrayBuffer` 进行更灵活的字节操作

嘿,大家好!今天咱们来聊聊 JavaScript 里一个略显低调,但实力强劲的小伙伴:DataView。说它低调,是因为很多人可能平时很少直接用到它;说它实力强劲,是因为它能让你像个黑客一样,直接操控 ArrayBuffer 里的每一个字节!是不是听起来就有点兴奋了?

咱们先从 ArrayBuffer 开始说起。

ArrayBuffer:内存里的原始粮仓

你可以把 ArrayBuffer 想象成一片连续的内存空间,就像一个大仓库,里面堆满了原始的字节数据。但是呢,ArrayBuffer 本身并不知道这些字节代表什么,它只负责存储。

const buffer = new ArrayBuffer(16); // 创建一个 16 字节的 ArrayBuffer
console.log(buffer.byteLength); // 输出:16

上面的代码创建了一个 16 字节的 ArrayBufferbyteLength 属性告诉你这个粮仓有多大。但问题来了,我们怎么往这个粮仓里放东西,又怎么把东西拿出来呢? 这时候,DataView 就闪亮登场了!

DataView:字节级别的操控大师

DataView 就像一个工具箱,里面装满了各种精密的工具,可以让你精确地读取和写入 ArrayBuffer 里的数据,还能指定数据的类型和字节顺序。它给了我们一种更加灵活的方式来处理二进制数据。

咱们先创建一个 DataView,把它和刚才的 ArrayBuffer 关联起来:

const buffer = new ArrayBuffer(16);
const dataView = new DataView(buffer);

现在,dataView 就拥有了访问和操作 buffer 的能力。

DataView 的常用方法:读写基本类型

DataView 提供了很多方法来读取和写入不同类型的数值。这些方法的名字都很有规律,方便记忆:

  • 读取: getUint8(), getInt8(), getUint16(), getInt16(), getUint32(), getInt32(), getFloat32(), getFloat64()
  • 写入: setUint8(), setInt8(), setUint16(), setInt16(), setUint32(), setInt32(), setFloat32(), setFloat64()

这些方法都需要一个参数:byteOffset,表示从 ArrayBuffer 的哪个字节开始读取或写入。 有些方法还可以接受一个可选的参数:littleEndian,表示是否使用小端字节序。

什么是字节序 (Endianness)?

简单来说,字节序指的是多字节数据(比如 16 位、32 位整数)在内存中存储的顺序。有两种常见的字节序:

  • 大端字节序 (Big-Endian): 高位字节存储在低地址,低位字节存储在高地址。 就像我们平时写数字一样,从左到右,先写高位,再写低位。
  • 小端字节序 (Little-Endian): 低位字节存储在低地址,高位字节存储在高地址。 就像倒过来写数字一样,先写低位,再写高位。

不同的计算机体系结构可能使用不同的字节序。网络传输中通常使用大端字节序。

举个例子:读写整数

咱们先往 ArrayBuffer 里写入一个 32 位整数,然后把它读出来:

const buffer = new ArrayBuffer(16);
const dataView = new DataView(buffer);

// 写入一个 32 位整数 (默认大端字节序)
dataView.setInt32(0, 123456789); // 从偏移量 0 开始写入

// 从偏移量 0 读取一个 32 位整数
const value = dataView.getInt32(0);
console.log(value); // 输出:123456789

// 写入一个 32 位整数 (小端字节序)
dataView.setInt32(4, 123456789, true); // 从偏移量 4 开始写入,使用小端字节序

// 从偏移量 4 读取一个 32 位整数 (小端字节序)
const littleEndianValue = dataView.getInt32(4, true);
console.log(littleEndianValue); // 输出:123456789

在这个例子里,我们先使用默认的大端字节序写入了一个整数,然后又使用小端字节序写入了一个整数。注意 setInt32()getInt32() 的第三个参数,true 表示使用小端字节序,false 或省略表示使用大端字节序。

再来一个例子:读写浮点数

DataView 同样可以读写浮点数:

const buffer = new ArrayBuffer(8); // 需要 8 个字节来存储一个 64 位浮点数
const dataView = new DataView(buffer);

// 写入一个 64 位浮点数
dataView.setFloat64(0, 3.141592653589793);

// 读取这个 64 位浮点数
const floatValue = dataView.getFloat64(0);
console.log(floatValue); // 输出:3.141592653589793

DataViewTypedArray 的区别

你可能会问,TypedArray(比如 Uint8Array, Int32Array)也可以操作 ArrayBuffer,那 DataView 有什么优势呢?

TypedArray 只能操作特定类型的数值,并且它会把 ArrayBuffer 当作一个连续的、相同类型元素的数组来处理。 而 DataView 更加灵活,它允许你从任意字节偏移量开始,读取或写入任意类型的数据。

咱们用一个表格来总结一下它们的区别:

特性 TypedArray DataView
数据类型 只能操作特定类型的数据 可以操作任意类型的数据
灵活性 ArrayBuffer 视为同类型元素的数组 可以从任意偏移量读写任意类型的数据
字节序 只能使用平台的默认字节序 可以指定大端或小端字节序
使用场景 大量同类型数据的批量操作 需要灵活地处理不同类型的数据,或者需要控制字节序
创建方式 new Uint8Array(buffer) new DataView(buffer)

实战演练:解析网络数据包

咱们来一个稍微复杂一点的例子,模拟解析一个简单的网络数据包。假设这个数据包的格式如下:

字段 类型 长度 (字节) 描述
Magic Number uint32 4 用于标识数据包的魔数
Version uint8 1 数据包的版本号
Payload Length uint16 2 负载数据的长度
Payload byte[] 变长 实际的负载数据

我们可以用 DataView 来解析这个数据包:

function parsePacket(buffer) {
  const dataView = new DataView(buffer);
  let offset = 0;

  // 读取 Magic Number
  const magicNumber = dataView.getUint32(offset);
  offset += 4;
  console.log("Magic Number:", magicNumber);

  // 读取 Version
  const version = dataView.getUint8(offset);
  offset += 1;
  console.log("Version:", version);

  // 读取 Payload Length
  const payloadLength = dataView.getUint16(offset);
  offset += 2;
  console.log("Payload Length:", payloadLength);

  // 读取 Payload
  const payload = new Uint8Array(buffer, offset, payloadLength); // 使用 TypedArray 创建 payload 的视图
  console.log("Payload:", payload);

  return {
    magicNumber,
    version,
    payloadLength,
    payload,
  };
}

// 模拟一个数据包
const packetBuffer = new ArrayBuffer(13);
const packetDataView = new DataView(packetBuffer);

// 写入 Magic Number
packetDataView.setUint32(0, 0x12345678);

// 写入 Version
packetDataView.setUint8(4, 1);

// 写入 Payload Length
packetDataView.setUint16(5, 6);

// 写入 Payload
const payloadData = new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]);
for (let i = 0; i < payloadData.length; i++) {
  packetDataView.setUint8(7 + i, payloadData[i]);
}

// 解析数据包
const parsedPacket = parsePacket(packetBuffer);

在这个例子中,我们首先定义了一个 parsePacket 函数,它接收一个 ArrayBuffer 作为参数,然后使用 DataView 逐步读取数据包中的各个字段。注意我们使用 offset 变量来记录当前读取的位置,每次读取完一个字段,就把 offset 加上相应字段的长度。 对于变长的 Payload 字段,我们使用 Uint8Array 创建了一个视图,直接指向 ArrayBufferPayload 数据的起始位置和长度。

高级技巧:处理字符串

DataView 本身并没有直接读写字符串的方法,但我们可以通过 TextDecoderTextEncoder 来实现字符串的转换。

  • TextDecoder 用于将 ArrayBuffer 中的字节数据解码成字符串。
  • TextEncoder 用于将字符串编码成 ArrayBuffer 中的字节数据。
// 编码字符串
const encoder = new TextEncoder();
const encoded = encoder.encode("Hello, DataView!"); // 返回一个 Uint8Array

// 创建 ArrayBuffer
const buffer = encoded.buffer;

// 创建 DataView
const dataView = new DataView(buffer);

// 解码字符串
const decoder = new TextDecoder();
const decoded = decoder.decode(dataView);

console.log(decoded); // 输出:Hello, DataView!

兼容性问题

DataView 的兼容性非常好,几乎所有现代浏览器都支持它。 所以你可以放心使用它,不用担心兼容性问题。

总结

DataView 是一个非常强大的工具,可以让你直接操控 ArrayBuffer 里的字节数据。 它在处理二进制数据、网络数据包、文件格式等方面都有广泛的应用。 虽然它可能不如 TypedArray 那么常用,但在某些特定的场景下,DataView 绝对是你的得力助手。

希望今天的讲座能让你对 DataView 有更深入的了解。 下次遇到需要处理二进制数据的场景,不妨试试 DataView,相信它会给你带来惊喜! 记住,玩转字节,你也能成为数据操控大师!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注