Skip to main content

二进制数据与文件处理

在处理音视频、Canvas/WebGL 绘图、WebSockets 传输或大文件分片上传时,传统的字符串和对象显得力不从心。前端必须掌握对二进制数据(Binary Data)的处理能力。 核心脉络:ArrayBuffer (底层内存) -> TypedArray / DataView (读写视图) -> Blob (不可变类文件对象) -> File (附带元数据的 Blob)。

1. ArrayBuffer 与视图 (TypedArray / DataView)

1.1 ArrayBuffer:纯粹的内存块

ArrayBuffer 是用来表示通用的、固定长度的原始二进制数据缓冲区。 注意:你不能直接操作 ArrayBuffer 的内容。它只是一块内存,要读写它,必须通过“视图”(TypedArray 或 DataView)。

// 分配一段 16 字节的连续内存,初始值全为 0
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16

🌟 ES2024 新特性:动态调整大小 (Resizable ArrayBuffer)

传统 ArrayBuffer 长度固定,扩容需要重新分配内存并拷贝。新标准支持创建可动态调整大小的 Buffer。

// 创建最大支持 1024 字节的可调 Buffer
const resizableBuffer = new ArrayBuffer(16, { maxByteLength: 1024 });

console.log(resizableBuffer.resizable); // true
resizableBuffer.resize(32); // 扩容到 32 字节
console.log(resizableBuffer.byteLength); // 32

🌟 性能优化利器:所有权转移 (transfer)

在 Web Worker 通信或数据处理时,深度拷贝大块 Buffer 极其耗时。transfer() 可以直接转移底层内存的所有权,实现“零拷贝”。转移后,原 Buffer 所在的变量会被“剥夺”(长度变为 0,无法再访问)。

const buffer1 = new ArrayBuffer(8);
// 转移所有权到 buffer2,buffer1 被清空
const buffer2 = buffer1.transfer();

console.log(buffer1.byteLength); // 0 (已剥离)
console.log(buffer2.byteLength); // 8

1.2 TypedArray:特定类型的读写视图

如果你知道内存里存的全是同一种类型的数据(比如全是 8 位无符号整数,或全是 32 位浮点数),就用 TypedArray。 常见类型:Uint8Array (0~255)、Int16ArrayFloat32Array 等。

const buffer = new ArrayBuffer(16);
// 把这 16 字节当做 32 位浮点数(4字节)来看待,所以一共能存 4 个数字
const float32View = new Float32Array(buffer);

float32View[0] = 3.14;
console.log(float32View.length); // 4 (元素个数)
console.log(float32View.byteLength); // 16 (占用字节数)

1.3 DataView:复合类型的读写视图

如果一段内存里混合了多种类型的数据(例如第 1 个字节是标识符 Uint8,后 4 个字节是数据 Float32),或者你需要手动控制字节序(Endianness),就必须用 DataView

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

view.setUint8(0, 255); // 在第 0 个字节处写入一个 Uint8
view.setFloat32(1, 3.14, true); // 在第 1 个字节处写入 Float32,并指定为小端字节序(Little-Endian)

console.log(view.getFloat32(1, true)); // 3.14

2. Blob (Binary Large Object)

Blob 表示一个不可变、原始数据的类文件对象。 如果说 ArrayBuffer 偏向于“内存计算”,那么 Blob 则偏向于“网络传输和高层应用”。

2.1 创建 Blob

你可以用字符串、ArrayBuffer 或其他 Blob 拼接出一个新 Blob,并指定 MIME 类型。

const htmlString = "<div>Hello Blob</div>";
const blob = new Blob([htmlString], { type: "text/html" });

console.log(blob.size); // 21 (字节数)
console.log(blob.type); // 'text/html'

2.2 切片 (slice) - 大文件分片上传的核心

Blob.prototype.slice(start, end, contentType) 返回一个新的 Blob。它之所以是大文件分片上传的杀手锏,关键在懒加载:调用时只记录 start/end 引用,不会立即复制底层数据——真正的字节复制延迟到读取时(arrayBuffer() / stream() / FileReader)才发生。所以对 5GB 的 File 切 5000 段几乎零开销。

const CHUNK_SIZE = 1024 * 1024; // 1MB
const chunk = bigBlob.slice(0, CHUNK_SIZE, "application/octet-stream");

ArrayBuffer.prototype.slice() 的差异:两者都叫 slice,签名也像,但底层完全不同。

维度ArrayBuffer.slice()Blob.slice()
返回类型新的 ArrayBuffer新的 Blob
是否立即拷贝✅ 立即把字节拷一份❌ 只记录 start/end
内存开销O(n),n 是切片长度O(1)
MIME 类型不支持第三参数 contentType
读取方式同步(直接挂 TypedArray)异步(arrayBuffer() / stream()
典型场景中小规模内存数据大文件分片、文件操作

简单记:Blob 负责"搬运",ArrayBuffer 负责"加工"。一个完整的大文件处理链路通常是 File → Blob.slice() → arrayBuffer() → ArrayBuffer.slice() / TypedArray

为啥分片必须用 Blob? 因为 DOM 给你的源数据本身就是 Blob/File——<input type="file"> 和拖拽事件返回的就是 File(继承自 Blob),你压根没有 ArrayBuffer。要把 File 转成 ArrayBuffer 必须先 await file.arrayBuffer(),这步会把整个文件一次性加载进内存,5GB 直接 OOM。而 Blob.slice() 是引用语义,全程不读字节,切完直接传给 fetch / FormData 就能发。

// 典型链路:File(Blob)切片 → 读出 ArrayBuffer → 加工
const file = input.files[0];
const chunk = file.slice(0, 1024 * 1024); // 1️⃣ 搬运:零拷贝
const buffer = await chunk.arrayBuffer(); // 2️⃣ 读出:按需复制
const view = new Uint8Array(buffer); // 3️⃣ 加工:同步读写

stream() 是分片上传的现代写法,slice 循环更多见于兼容老浏览器或需要精确控制每片大小的场景(比如按固定 1MB 分片做断点续传)。

2.3 Object URL (用于预览和下载)

URL.createObjectURL(blob) 可以把一个 Blob 映射为一个短链接(blob:http://...)。这个链接可以直接赋值给 img.srca.href,常用于前端本地预览或生成下载链接。

const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "hello.html";
a.click();

// 极其重要:使用完毕后必须手动释放内存!
URL.revokeObjectURL(url);

3. File (特殊的文件 Blob)

File 接口基于 Blob,并在其基础上增加了用户文件系统相关的元数据(文件名、最后修改时间)。 前端通常通过 <input type="file"> 标签或拖拽事件(Drag & Drop)获取 File 对象。

const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0]; // 这是一个 File 对象
console.log(file.name); // 文件名,如 'avatar.png'
console.log(file.lastModified); // 最后修改时间戳
console.log(file instanceof Blob); // true,File 是 Blob 的子类
});

4. 各种格式的相互转换 (面试常考)

在处理文件上传、Canvas 海报导出、图片裁剪等业务时,经常需要在这几种类型间反复横跳。

4.1 Blob / File 转 ArrayBuffer 或 文本

现代浏览器推荐使用 Blob 实例上的异步方法(基于 Promise)。

const blob = new Blob(["hello world"]);

// 1. 转为 ArrayBuffer
const buffer = await blob.arrayBuffer();

// 2. 转为纯文本字符串
const text = await blob.text();

4.2 ArrayBuffer 转 Blob

直接作为数组元素传入 Blob 构造函数即可。

const buffer = new ArrayBuffer(16);
const blob = new Blob([buffer], { type: "application/octet-stream" });

4.3 Canvas 导出为 Blob (生成海报)

const canvas = document.getElementById("myCanvas");
canvas.toBlob(
(blob) => {
// 得到图片的 Blob 对象,可用于上传或创建本地 URL
},
"image/jpeg",
0.9,
);

4.4 Blob / File 转 Base64 (DataURL)

当需要把图片转成 Base64 字符串以便随 JSON 接口一起发送时,可以使用 FileReader

function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result); // result 即为 Base64 字符串
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}