JavaScript内核与高级编程之:`JavaScript`的`Typed Arrays`:底层`ArrayBuffer`和`DataView`的内存操作。

嘿,大家好!今天咱们聊聊 JavaScript 里那些“硬核”的东西:Typed Arrays!

先别慌,虽然听起来像是什么深奥的 C++ 黑魔法,但其实 Typed Arrays 是 JavaScript 为了更好地处理二进制数据而生的。它们和底层的 ArrayBufferDataView 配合,能让你像操作 C 语言的指针一样,直接在内存里“动手动脚”,是不是有点小兴奋?

1. 为什么需要 Typed Arrays?

你可能会问:“JavaScript 不是有数组吗?啥都能往里塞,为啥还要搞这些花里胡哨的?”

问得好!JavaScript 的普通数组 ( Array ) 就像一个大杂烩,可以放数字、字符串、对象等等。但它有个致命的缺点:效率不高!尤其是在处理大量二进制数据时,比如图像、音频、视频,Array 的性能简直惨不忍睹。

原因很简单:

  • 类型不固定: JavaScript 数组里的元素类型可以随意变化,每次访问都需要进行类型检查,耗时!
  • 存储不连续: JavaScript 数组在内存中不一定是连续存储的,可能分散在各处,访问效率低!
  • 没有直接操作内存的能力: 无法像 C 语言那样,直接用指针操作内存。

Typed Arrays 就解决了这些问题。它们是类型化的数组,每个元素都必须是同一种类型,而且在内存中是连续存储的,效率杠杠的!

2. ArrayBuffer:内存的“毛坯房”

ArrayBuffer 是 Typed Arrays 的基础,它代表了一块原始的、连续的内存区域,就像一块未装修的“毛坯房”。 你可以把它理解为一段字节序列,但你不能直接访问或修改 ArrayBuffer 里的数据。

创建 ArrayBuffer:

// 创建一个 16 字节的 ArrayBuffer
const buffer = new ArrayBuffer(16);

console.log(buffer.byteLength); // 输出: 16

byteLength 属性表示 ArrayBuffer 的大小(字节数)。

注意事项:

  • ArrayBuffer 只能分配内存,不能读写数据。
  • ArrayBuffer 的大小在创建后就不能改变。

3. Typed Arrays:内存的“精装修”

Typed Arrays 是基于 ArrayBuffer 的,它们将 ArrayBuffer 划分成一个个固定大小的单元,并指定每个单元的数据类型。 就像在“毛坯房”里装上不同的“房间”,每个房间都有特定的用途。

常见的 Typed Arrays 类型:

类型 描述 字节大小
Int8Array 8 位有符号整数 1
Uint8Array 8 位无符号整数 1
Int16Array 16 位有符号整数 2
Uint16Array 16 位无符号整数 2
Int32Array 32 位有符号整数 4
Uint32Array 32 位无符号整数 4
Float32Array 32 位浮点数 (单精度) 4
Float64Array 64 位浮点数 (双精度) 8
BigInt64Array 64 位有符号大整数 (ES2020) 8
BigUint64Array 64 位无符号大整数 (ES2020) 8
Uint8ClampedArray 8 位无符号整数 (值会被截断到 0-255) 1

创建 Typed Arrays:

// 基于之前的 ArrayBuffer 创建一个 Int32Array
const int32View = new Int32Array(buffer);

console.log(int32View.length); // 输出: 4 (16 字节 / 4 字节/元素 = 4 个元素)
console.log(int32View.byteLength); // 输出: 16 (与 ArrayBuffer 相同)
console.log(int32View.byteOffset); // 输出: 0 (从 ArrayBuffer 的起始位置开始)

// 或者,直接创建一个指定大小的 Typed Array,它会自动创建一个 ArrayBuffer
const uint8View = new Uint8Array(8); // 创建一个 8 字节的 Uint8Array

console.log(uint8View.length); // 输出: 8
console.log(uint8View.byteLength); // 输出: 8

访问和修改 Typed Arrays:

// 设置值
int32View[0] = 12345;
int32View[1] = -67890;

// 获取值
console.log(int32View[0]); // 输出: 12345
console.log(int32View[1]); // 输出: -67890

// 越界访问
console.log(int32View[4]); // 输出: undefined (不会报错,但行为不确定)

// 修改 ArrayBuffer 会影响 Typed Arrays
buffer[0] = 1; // 直接这样操作 ArrayBuffer 是不允许的

重要概念:

  • length: Typed Array 中元素的个数。
  • byteLength: Typed Array 对应的 ArrayBuffer 的大小(字节数)。
  • byteOffset: Typed Array 在 ArrayBuffer 中的起始位置(字节数)。

注意事项:

  • Typed Arrays 的 length 是固定的,不能像普通数组那样动态添加或删除元素。
  • Typed Arrays 只能存储特定类型的数据,不能存储混合类型的数据。
  • 越界访问 Typed Arrays 不会报错,但会返回 undefined,需要注意。

4. DataView:内存的“瑞士军刀”

DataView 就像一把“瑞士军刀”,它可以让你以不同的数据类型,在 ArrayBuffer 的任意位置读取和写入数据。 它是 Typed Arrays 的一个更灵活的版本,可以让你更精细地控制内存操作。

创建 DataView:

// 基于之前的 ArrayBuffer 创建一个 DataView
const dataView = new DataView(buffer);

// 或者,指定 byteOffset 和 byteLength
const dataView2 = new DataView(buffer, 4, 8); // 从 buffer 的第 4 个字节开始,读取 8 个字节

使用 DataView 读取和写入数据:

// 写入数据
dataView.setInt8(0, 123); // 在偏移量 0 处写入一个 8 位有符号整数
dataView.setInt32(4, 0x12345678); // 在偏移量 4 处写入一个 32 位有符号整数 (默认大端序)
dataView.setFloat64(8, 3.1415926); // 在偏移量 8 处写入一个 64 位浮点数 (默认大端序)

// 读取数据
console.log(dataView.getInt8(0)); // 输出: 123
console.log(dataView.getInt32(4)); // 输出: 305419896 (0x12345678)
console.log(dataView.getFloat64(8)); // 输出: 3.1415926

// 指定字节序 (Endianness)
dataView.setInt32(4, 0x12345678, true); // 小端序
console.log(dataView.getInt32(4, true)); // 读取时也要指定小端序,输出: 2018915346 (0x78563412)

重要方法:

  • getInt8(byteOffset)
  • setInt8(byteOffset, value)
  • getUint8(byteOffset)
  • setUint8(byteOffset, value)
  • getInt16(byteOffset, littleEndian)
  • setInt16(byteOffset, value, littleEndian)
  • getUint16(byteOffset, littleEndian)
  • setUint16(byteOffset, value, littleEndian)
  • getInt32(byteOffset, littleEndian)
  • setInt32(byteOffset, value, littleEndian)
  • getUint32(byteOffset, littleEndian)
  • setUint32(byteOffset, value, littleEndian)
  • getFloat32(byteOffset, littleEndian)
  • setFloat32(byteOffset, value, littleEndian)
  • getFloat64(byteOffset, littleEndian)
  • setFloat64(byteOffset, value, littleEndian)
  • getBigInt64(byteOffset, littleEndian)
  • setBigInt64(byteOffset, value, littleEndian)
  • getBigUint64(byteOffset, littleEndian)
  • setBigUint64(byteOffset, value, littleEndian)

注意事项:

  • DataView 可以跨越 Typed Arrays 的边界,在 ArrayBuffer 的任意位置读取和写入数据。
  • DataView 需要手动指定数据类型和字节序。
  • littleEndian 参数表示字节序,true 表示小端序,false 表示大端序(默认)。

5. 字节序 (Endianness)

字节序是指多字节数据在内存中的存储顺序。 有两种主要的字节序:

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

举个例子,假设我们要将整数 0x12345678 存储到内存中:

  • 大端序: 12 34 56 78
  • 小端序: 78 56 34 12

不同的 CPU 架构可能使用不同的字节序。 例如,PowerPC 和 SPARC 架构通常使用大端序,而 x86 和 ARM 架构通常使用小端序。

在使用 DataView 时,需要根据实际情况选择正确的字节序,否则可能会导致数据错误。

6. 应用场景

Typed Arrays 和 DataView 在以下场景中非常有用:

  • 处理二进制数据: 例如,读取和写入图像、音频、视频文件。
  • WebGL: 用于将数据传递给 WebGL 着色器。
  • WebSockets: 用于通过 WebSockets 发送和接收二进制数据。
  • 游戏开发: 用于处理游戏资源和物理引擎数据。
  • 加密算法: 用于处理加密和解密数据。
  • 科学计算: 用于处理大量的数值数据。

7. 实际例子:解析 PNG 图片

这是一个简单的例子,演示如何使用 Typed Arrays 和 DataView 解析 PNG 图片的 IHDR (Image Header) 数据块:

// 假设我们已经从文件中读取了 PNG 图片的二进制数据,存储在 ArrayBuffer 中
async function loadPngData(url) {
    const response = await fetch(url);
    const arrayBuffer = await response.arrayBuffer();
    return arrayBuffer;
}

async function parsePngIHDR(url) {
  const arrayBuffer = await loadPngData(url);

  const dataView = new DataView(arrayBuffer);

  // PNG 文件头 (8 bytes)
  const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
  for (let i = 0; i < pngSignature.length; i++) {
    if (dataView.getUint8(i) !== pngSignature[i]) {
      throw new Error("Invalid PNG signature");
    }
  }

  // IHDR 数据块 (从第 8 字节开始)
  let offset = 8;
  const ihdrLength = dataView.getUint32(offset); // IHDR 数据块长度 (4 bytes)
  offset += 4;

  const ihdrType = String.fromCharCode(
    dataView.getUint8(offset),
    dataView.getUint8(offset + 1),
    dataView.getUint8(offset + 2),
    dataView.getUint8(offset + 3)
  ); // IHDR 数据块类型 (4 bytes)
  offset += 4;

  if (ihdrType !== "IHDR") {
    throw new Error("IHDR chunk not found");
  }

  const width = dataView.getUint32(offset); // 图像宽度 (4 bytes)
  offset += 4;

  const height = dataView.getUint32(offset); // 图像高度 (4 bytes)
  offset += 4;

  const bitDepth = dataView.getUint8(offset); // 位深度 (1 byte)
  offset += 1;

  const colorType = dataView.getUint8(offset); // 颜色类型 (1 byte)
  offset += 1;

  const compressionMethod = dataView.getUint8(offset); // 压缩方法 (1 byte)
  offset += 1;

  const filterMethod = dataView.getUint8(offset); // 滤波器方法 (1 byte)
  offset += 1;

  const interlaceMethod = dataView.getUint8(offset); // 隔行扫描方法 (1 byte)
  offset += 1;

  console.log("Image Width:", width);
  console.log("Image Height:", height);
  console.log("Bit Depth:", bitDepth);
  console.log("Color Type:", colorType);
  console.log("Compression Method:", compressionMethod);
  console.log("Filter Method:", filterMethod);
  console.log("Interlace Method:", interlaceMethod);

  return {
    width,
    height,
    bitDepth,
    colorType,
    compressionMethod,
    filterMethod,
    interlaceMethod,
  };
}

// 使用示例
parsePngIHDR("your_image.png").catch((error) => console.error(error));

代码解释:

  1. loadPngData 函数:从 URL 加载 PNG 图像数据,并将其转换为 ArrayBuffer
  2. parsePngIHDR 函数:
    • 创建一个 DataView 对象,用于读取 ArrayBuffer 中的数据。
    • 验证 PNG 文件头,确保文件是一个有效的 PNG 图像。
    • 读取 IHDR 数据块的长度和类型。
    • 读取图像的宽度、高度、位深度、颜色类型等信息。
    • 打印提取的信息。

注意: your_image.png 替换成你自己的png图片地址

这个例子只是一个简单的演示,实际的 PNG 解析要复杂得多,需要处理各种数据块和压缩算法。 但它展示了如何使用 Typed Arrays 和 DataView 来读取和解析二进制数据。

8. 总结

Typed Arrays、ArrayBuffer 和 DataView 是 JavaScript 中处理二进制数据的利器。 它们提供了高效的内存操作能力,可以让你在 JavaScript 中处理各种复杂的二进制数据格式。 虽然它们看起来有点复杂,但只要掌握了基本概念和用法,就能让你在性能优化和底层数据处理方面更上一层楼。

希望今天的讲座对你有所帮助!下次再见!

发表回复

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