类型化数组视图
类型化数组视图
类型化数组视图提供了访问和操作ArrayBuffer中二进制数据的接口。本文将介绍Int8Array、Uint8Array、Float32Array等类型化数组的特点和使用方法。
类型化数组概述
类型化数组(Typed Arrays)是一组不同的构造函数,用于创建特定类型的数组视图,这些视图可以用来访问和操作 ArrayBuffer 中的二进制数据。与普通 JavaScript 数组不同,类型化数组严格限制了其中元素的数据类型。
类型化数组的种类
JavaScript 提供了多种类型化数组,每种都对应特定的数据类型和字节大小:
类型化数组 | 数据类型 | 字节大小 | 描述 |
---|---|---|---|
Int8Array | 有符号 8 位整数 | 1 | 范围:-128 到 127 |
Uint8Array | 无符号 8 位整数 | 1 | 范围:0 到 255 |
Uint8ClampedArray | 无符号 8 位整数(钳位) | 1 | 范围:0 到 255,自动钳位 |
Int16Array | 有符号 16 位整数 | 2 | 范围:-32768 到 32767 |
Uint16Array | 无符号 16 位整数 | 2 | 范围:0 到 65535 |
Int32Array | 有符号 32 位整数 | 4 | 范围:-2^31 到 2^31-1 |
Uint32Array | 无符号 32 位整数 | 4 | 范围:0 到 2^32-1 |
Float32Array | 32 位浮点数 | 4 | 单精度浮点数 |
Float64Array | 64 位浮点数 | 8 | 双精度浮点数 |
BigInt64Array | 有符号 64 位整数 | 8 | 范围:-2^63 到 2^63-1 |
BigUint64Array | 无符号 64 位整数 | 8 | 范围:0 到 2^64-1 |
创建类型化数组
有多种方式可以创建类型化数组:
1. 从 ArrayBuffer 创建
// 创建一个 16 字节的 ArrayBuffer
const buffer = new ArrayBuffer(16);
// 创建不同类型的视图
const int8View = new Int8Array(buffer); // 16 个元素
const int16View = new Int16Array(buffer); // 8 个元素
const int32View = new Int32Array(buffer); // 4 个元素
const float64View = new Float64Array(buffer); // 2 个元素
console.log(int8View.length); // 输出: 16
console.log(int16View.length); // 输出: 8
console.log(int32View.length); // 输出: 4
console.log(float64View.length); // 输出: 2
2. 指定 ArrayBuffer 的一部分
const buffer = new ArrayBuffer(16);
// 从第 2 个字节开始,使用 4 个字节
const int16PartialView = new Int16Array(buffer, 2, 2);
console.log(int16PartialView.length); // 输出: 2
console.log(int16PartialView.byteOffset); // 输出: 2
console.log(int16PartialView.byteLength); // 输出: 4
3. 从数组或类数组对象创建
// 从普通数组创建
const int32FromArray = new Int32Array([1, 2, 3, 4]);
console.log(int32FromArray); // 输出: Int32Array [1, 2, 3, 4]
// 从另一个类型化数组创建
const float32FromInt32 = new Float32Array(int32FromArray);
console.log(float32FromInt32); // 输出: Float32Array [1, 2, 3, 4]
4. 指定长度创建
// 创建一个包含 10 个元素的 Uint8Array,初始值都是 0
const uint8Array = new Uint8Array(10);
console.log(uint8Array); // 输出: Uint8Array [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
类型化数组的属性和方法
类型化数组继承了 Array 对象的许多方法,同时也有一些特有的属性和方法。
常用属性
- length:数组中的元素数量
- buffer:引用的 ArrayBuffer 对象
- byteLength:数组占用的字节数
- byteOffset:数组在 ArrayBuffer 中的起始偏移量
const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);
console.log(int32View.length); // 输出: 4
console.log(int32View.buffer); // 输出: ArrayBuffer { byteLength: 16 }
console.log(int32View.byteLength); // 输出: 16
console.log(int32View.byteOffset); // 输出: 0
常用方法
类型化数组支持大多数普通数组的方法,如 forEach
、map
、filter
、reduce
等:
const uint8Array = new Uint8Array([1, 2, 3, 4, 5]);
// forEach 方法
uint8Array.forEach((value, index) => {
console.log(`索引 ${index}: ${value}`);
});
// map 方法
const doubled = uint8Array.map(x => x * 2);
console.log(doubled); // 输出: Uint8Array [2, 4, 6, 8, 10]
// filter 方法
const filtered = uint8Array.filter(x => x > 2);
console.log(filtered); // 输出: Uint8Array [3, 4, 5]
// reduce 方法
const sum = uint8Array.reduce((acc, val) => acc + val, 0);
console.log(sum); // 输出: 15
特有方法
类型化数组还有一些特有的方法:
set() 方法
用于将数组或类型化数组的值复制到当前类型化数组中:
const uint8Array = new Uint8Array(5);
// 设置单个值
uint8Array[0] = 42;
// 使用 set() 方法复制数组
uint8Array.set([1, 2, 3], 1); // 从索引 1 开始设置值 [1, 2, 3]
console.log(uint8Array); // 输出: Uint8Array [42, 1, 2, 3, 0]
// 从另一个类型化数组复制
const anotherArray = new Uint8Array([5, 6]);
uint8Array.set(anotherArray, 3);
console.log(uint8Array); // 输出: Uint8Array [42, 1, 2, 5, 6]
subarray() 方法
创建一个新的类型化数组,它与原数组共享同一个 ArrayBuffer,但只包含指定范围的元素:
const uint8Array = new Uint8Array([1, 2, 3, 4, 5]);
// 创建一个包含索引 1 到 3 (不包括 4) 的子数组
const subArray = uint8Array.subarray(1, 4);
console.log(subArray); // 输出: Uint8Array [2, 3, 4]
// 修改子数组会影响原数组,因为它们共享同一个 ArrayBuffer
subArray[0] = 42;
console.log(uint8Array); // 输出: Uint8Array [1, 42, 3, 4, 5]
类型化数组的特性和限制
1. 固定类型
类型化数组中的所有元素必须是同一类型,如果尝试存储不兼容的值,会自动转换:
const int8Array = new Int8Array(3);
int8Array[0] = 127; // 最大值
int8Array[1] = 128; // 超出范围,会被转换为 -128
int8Array[2] = -129; // 超出范围,会被转换为 127
console.log(int8Array); // 输出: Int8Array [127, -128, 127]
// 浮点数会被截断为整数
const int32Array = new Int32Array(1);
int32Array[0] = 3.14;
console.log(int32Array[0]); // 输出: 3
2. Uint8ClampedArray 的特殊行为
Uint8ClampedArray
会自动将值钳位在 0-255 范围内,而不是环绕:
const uint8Array = new Uint8Array(3);
const uint8ClampedArray = new Uint8ClampedArray(3);
// 设置超出范围的值
uint8Array[0] = 256; // 环绕为 0
uint8ClampedArray[0] = 256; // 钳位为 255
uint8Array[1] = -1; // 环绕为 255
uint8ClampedArray[1] = -1; // 钳位为 0
console.log(uint8Array); // 输出: Uint8Array [0, 255, 0]
console.log(uint8ClampedArray); // 输出: Uint8ClampedArray [255, 0, 0]
3. 字节序(Endianness)
类型化数组使用平台的本地字节序(通常是小端字节序):
// 检测平台的字节序
function isLittleEndian() {
const buffer = new ArrayBuffer(2);
const uint8Array = new Uint8Array(buffer);
const uint16Array = new Uint16Array(buffer);
uint16Array[0] = 0x1234;
// 如果是小端字节序,第一个字节应该是 0x34
return uint8Array[0] === 0x34;
}
console.log(`当前平台是${isLittleEndian() ? '小端' : '大端'}字节序`);
常见应用场景
1. 图像处理
Uint8ClampedArray
常用于 Canvas 图像处理:
function invertColors(canvas) {
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // 这是一个 Uint8ClampedArray
// 反转颜色 (RGB,保持 Alpha 不变)
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // R
data[i + 1] = 255 - data[i + 1]; // G
data[i + 2] = 255 - data[i + 2]; // B
// data[i + 3] 是 Alpha 通道,保持不变
}
ctx.putImageData(imageData, 0, 0);
}
2. 音频处理
Float32Array
常用于 Web Audio API:
function createSineWave(audioContext, frequency, duration) {
const sampleRate = audioContext.sampleRate;
const numSamples = duration * sampleRate;
const buffer = audioContext.createBuffer(1, numSamples, sampleRate);
// 获取通道数据 (Float32Array)
const channelData = buffer.getChannelData(0);
// 生成正弦波
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
channelData[i] = Math.sin(2 * Math.PI * frequency * t);
}
return buffer;
}
3. 二进制数据解析
解析二进制文件格式(如图像、音频或自定义格式):
async function parseBinaryHeader(url) {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const headerView = new DataView(buffer);
// 假设文件头有以下结构:
// - 4 字节: 文件标识符 (字符串)
// - 4 字节: 版本号 (32位整数)
// - 8 字节: 文件大小 (64位整数)
// 读取文件标识符
const identifier = String.fromCharCode(
headerView.getUint8(0),
headerView.getUint8(1),
headerView.getUint8(2),
headerView.getUint8(3)
);
// 读取版本号
const version = headerView.getUint32(4, true); // true 表示小端字节序
// 读取文件大小
const fileSize = headerView.getBigUint64(8, true);
return {
identifier,
version,
fileSize
};
}
4. WebGL 图形处理
在 WebGL 中使用类型化数组传递顶点数据:
function createTriangle(gl) {
// 创建顶点数据
const vertices = new Float32Array([
-0.5, -0.5, 0.0, // 左下
0.5, -0.5, 0.0, // 右下
0.0, 0.5, 0.0 // 顶部
]);
// 创建缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 设置顶点属性指针
const positionAttribLocation = gl.getAttribLocation(shaderProgram, 'aPosition');
gl.vertexAttribPointer(positionAttribLocation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionAttribLocation);
return vertexBuffer;
}
5. 网络通信
在 WebSocket 中发送和接收二进制数据:
function setupBinaryWebSocket() {
const socket = new WebSocket('wss://example.com/binary-endpoint');
// 设置为二进制类型
socket.binaryType = 'arraybuffer';
// 发送二进制数据
socket.addEventListener('open', () => {
const data = new Float32Array([1.0, 2.0, 3.0, 4.0]);
socket.send(data.buffer);
});
// 接收二进制数据
socket.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
const buffer = event.data;
const view = new DataView(buffer);
// 处理接收到的二进制数据
console.log(`接收到 ${buffer.byteLength} 字节的数据`);
console.log(`第一个 32 位浮点数: ${view.getFloat32(0, true)}`);
}
});
}
类型化数组与普通数组的比较
性能比较
类型化数组在处理大量数值数据时通常比普通数组更高效:
function performanceComparison() {
const size = 10000000;
console.time('普通数组');
const regularArray = [];
for (let i = 0; i < size; i++) {
regularArray.push(i);
}
console.timeEnd('普通数组');
console.time('类型化数组');
const typedArray = new Int32Array(size);
for (let i = 0; i < size; i++) {
typedArray[i] = i;
}
console.timeEnd('类型化数组');
// 数学运算
console.time('普通数组数学运算');
for (let i = 0; i < regularArray.length; i++) {
regularArray[i] = regularArray[i] * 2;
}
console.timeEnd('普通数组数学运算');
console.time('类型化数组数学运算');
for (let i = 0; i < typedArray.length; i++) {
typedArray[i] = typedArray[i] * 2;
}
console.timeEnd('类型化数组数学运算');
}
功能差异
类型化数组与普通数组有一些重要的功能差异:
function functionalDifferences() {
const regularArray = [1, 2, 3];
const typedArray = new Int32Array([1, 2, 3]);
// 1. 动态调整大小
regularArray.push(4); // 正常工作
console.log(regularArray); // 输出: [1, 2, 3, 4]
// typedArray.push(4); // 错误: typedArray.push is not a function
// 2. 混合类型
regularArray.push('string', true, {});
console.log(regularArray); // 输出: [1, 2, 3, 4, 'string', true, {}]
typedArray[3] = 'string'; // 会被转换为 0
typedArray[4] = true; // 会被转换为 1
console.log(typedArray); // 输出类似: Int32Array [1, 2, 3, 0, 1]
// 3. 稀疏数组
const sparseArray = [];
sparseArray[0] = 1;
sparseArray[10] = 10;
console.log(sparseArray.length); // 输出: 11
console.log(sparseArray); // 输出: [1, empty × 9, 10]
// 类型化数组不能是稀疏的,所有未设置的值都是 0
const sparseTyped = new Int32Array(11);
sparseTyped[0] = 1;
sparseTyped[10] = 10;
console.log(sparseTyped); // 输出: Int32Array [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10]
}
类型化数组的兼容性和互操作性
与其他 Web API 的互操作
类型化数组与许多现代 Web API 无缝集成:
async function webApiInteroperability() {
// 1. Fetch API
const response = await fetch('https://example.com/binary-data');
const buffer = await response.arrayBuffer();
// 2. File API
document.getElementById('fileInput').addEventListener('change', (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = () => {
const arrayBuffer = reader.result;
processArrayBuffer(arrayBuffer);
};
reader.readAsArrayBuffer(file);
});
// 3. Canvas API
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(100, 100);
const pixels = imageData.data; // Uint8ClampedArray
// 4. Web Audio API
const audioContext = new AudioContext();
const audioBuffer = audioContext.createBuffer(2, 44100, 44100);
const leftChannel = audioBuffer.getChannelData(0); // Float32Array
// 5. WebRTC
const peerConnection = new RTCPeerConnection();
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
const candidateData = new TextEncoder().encode(JSON.stringify(event.candidate));
sendBinaryData(candidateData.buffer);
}
};
}
在 Web Workers 中使用
类型化数组特别适合在 Web Workers 中使用,可以高效地传输大量数据:
// 主线程代码
function useTypedArrayWithWorker() {
const worker = new Worker('worker.js');
// 创建大型数据集
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
const view = new Float64Array(buffer);
// 填充数据
for (let i = 0; i < view.length; i++) {
view[i] = Math.random();
}
// 将数据传输到 Worker (使用 transferable objects)
worker.postMessage({ buffer }, [buffer]);
// 接收处理后的数据
worker.onmessage = (event) => {
const resultBuffer = event.data.resultBuffer;
const resultView = new Float64Array(resultBuffer);
console.log('处理后的数据:', resultView);
};
}
// worker.js 内容
/*
self.onmessage = function(event) {
const buffer = event.data.buffer;
const view = new Float64Array(buffer);
// 处理数据 (例如: 计算平方根)
for (let i = 0; i < view.length; i++) {
view[i] = Math.sqrt(view[i]);
}
// 将处理后的数据发送回主线程
self.postMessage({ resultBuffer: buffer }, [buffer]);
};
*/
最佳实践
1. 选择合适的类型化数组
根据数据类型和范围选择最合适的类型化数组:
function chooseAppropriateTypedArray() {
// 对于像素数据 (0-255),使用 Uint8Array 或 Uint8ClampedArray
const pixelData = new Uint8ClampedArray(width * height * 4); // RGBA
// 对于音频样本 (-1.0 到 1.0),使用 Float32Array
const audioSamples = new Float32Array(sampleRate * duration);
// 对于大整数,使用 Int32Array 或 BigInt64Array
const largeIntegers = new Int32Array(1000);
// 对于高精度浮点数,使用 Float64Array
const preciseValues = new Float64Array(1000);
}
2. 内存管理
合理管理 ArrayBuffer 的内存使用:
function memoryManagement() {
// 重用 ArrayBuffer 而不是创建新的
const buffer = new ArrayBuffer(1024);
function processChunk(chunkData, offset) {
const view = new Uint8Array(buffer);
view.set(chunkData, offset);
// 处理数据...
}
// 当不再需要大型 ArrayBuffer 时,解除引用以便垃圾回收
function cleanupLargeBuffer() {
const hugeBuffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
// 使用 hugeBuffer...
// 完成后解除引用
hugeBuffer = null;
}
}
3. 性能优化
利用类型化数组的特性进行性能优化:
function performanceOptimizations() {
const size = 10000000;
const array = new Float32Array(size);
// 1. 预分配而不是动态增长
// 不好的做法:
// let dynamicArray = [];
// for (let i = 0; i < size; i++) {
// dynamicArray.push(Math.random());
// }
// 好的做法:
for (let i = 0; i < array.length; i++) {
array[i] = Math.random();
}
// 2. 使用 set() 批量设置值,而不是逐个设置
const values = new Float32Array(1000).fill(1.0);
array.set(values, 5000); // 从索引 5000 开始设置 1000 个值
// 3. 使用 subarray() 而不是创建新数组
// 不好的做法:
// const firstPart = new Float32Array(1000);
// for (let i = 0; i < 1000; i++) {
// firstPart[i] = array[i];
// }
// 好的做法:
const firstPart = array.subarray(0, 1000);
}
总结
类型化数组视图是 JavaScript 中处理二进制数据的强大工具,它们提供了高效的内存使用和性能优势,特别是在处理大量数值数据时。通过选择合适的类型化数组和遵循最佳实践,可以显著提高 Web 应用程序在处理图像、音频、网络通信和 3D 图形等方面的性能。
主要优点包括:
- 类型安全:每个数组元素都有固定的类型和大小
- 内存效率:比普通 JavaScript 数组更高效地使用内存
- 性能优势:在数值计算和二进制数据处理方面性能更好
- 与底层 API 集成:与 Canvas、WebGL、Web Audio 等现代 Web API 无缝集成
随着 Web 应用变得越来越复杂,掌握类型化数组的使用对于开发高性能的应用程序变得越来越重要。
在下一章中,我们将介绍 DataView,它提供了一种更灵活的方式来读取和写入 ArrayBuffer 中的数据。