嘿,大家好!今天咱们聊聊 JavaScript 里那些“硬核”的东西:Typed Arrays!
先别慌,虽然听起来像是什么深奥的 C++ 黑魔法,但其实 Typed Arrays 是 JavaScript 为了更好地处理二进制数据而生的。它们和底层的 ArrayBuffer
、DataView
配合,能让你像操作 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));
代码解释:
loadPngData
函数:从 URL 加载 PNG 图像数据,并将其转换为ArrayBuffer
。parsePngIHDR
函数:- 创建一个
DataView
对象,用于读取ArrayBuffer
中的数据。 - 验证 PNG 文件头,确保文件是一个有效的 PNG 图像。
- 读取 IHDR 数据块的长度和类型。
- 读取图像的宽度、高度、位深度、颜色类型等信息。
- 打印提取的信息。
- 创建一个
注意: your_image.png
替换成你自己的png图片地址
这个例子只是一个简单的演示,实际的 PNG 解析要复杂得多,需要处理各种数据块和压缩算法。 但它展示了如何使用 Typed Arrays 和 DataView 来读取和解析二进制数据。
8. 总结
Typed Arrays、ArrayBuffer 和 DataView 是 JavaScript 中处理二进制数据的利器。 它们提供了高效的内存操作能力,可以让你在 JavaScript 中处理各种复杂的二进制数据格式。 虽然它们看起来有点复杂,但只要掌握了基本概念和用法,就能让你在性能优化和底层数据处理方面更上一层楼。
希望今天的讲座对你有所帮助!下次再见!