一份 JavaScript 中“二进制生态”极简内功心法
诙谐版 · 但保证概念不缩水


0. 前言:为什么我们要跟二进制“死磕”?

前端的世界看似是字符串、JSON 和 DOM 的乐园,但当你遇到:

  • 上传一张图片要预览、要切片
  • 下载一个文件要命名、要触发保存
  • WebSocket 收到一段二进制协议(比如游戏对战数据)
  • 从 Canvas 抠出像素自己加工
  • 处理一个 ZIP 文件、解析一个 PDF、做 Web Audio 可视化

……你就会发现:二进制数据才是幕后真正的大佬

JavaScript 为我们准备了一整套二进制“兵器库” —— 从最底层的 ArrayBuffer,到最上手的 Blob,再到流、文件、Base64……
它们听起来像一群近亲,很容易搞混。今天我们就从底层到顶层,把整个生态撸一遍。

读完你会明白:为什么有这么多概念?它们各自解决什么问题?以及——到底什么时候用哪个?


1. 一切的基础:ArrayBuffer —— “一块干净的内存荒地”

想象你向操作系统申请了一块 连续的内存,大小是 16 字节。这块内存里全是 0 和 1,没有类型,没有结构。

const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16

重要特征

  • 长度固定,不能直接扩容(但可以拷贝生成新的)。
  • 你不能直接读写它 —— 就像一个没有门牌号的荒地区域,JS 不让你直接往里踩。

为什么这么设计?
因为原始二进制数据没有“类型”概念,如果直接操作很容易出现字节序错乱、类型误用。JavaScript 作为动态语言,选择强制你通过“视图”来操作,既安全又灵活。

于是,我们需要 视图(View) 来给这片荒地铺上“地板砖”。


2. 视图一:TypedArray —— “铺上统一尺寸的地砖”

TypedArray 不是单个类,而是一族类:Uint8ArrayInt16ArrayFloat32Array …… 它们把 ArrayBuffer 当作一个 数字数组 来读写,每个元素的大小是固定的。

const buffer = new ArrayBuffer(8);
const uint8 = new Uint8Array(buffer);   // 每块砖 1 字节,共 8 块
const int32 = new Int32Array(buffer);   // 每块砖 4 字节,共 2 块

uint8[0] = 0xFF;
uint8[1] = 0x12;
console.log(int32[0]); // 猜猜输出?小端序环境下是 0x12FF(即 4863)

不同视图可以 共享同一块内存,这非常强大 —— 你可以把同一段二进制同时当作字节流和整数数组来操作。

常用 TypedArray 一览

类型 元素大小 用途举例
Uint8Array 1 原始字节、图片数据、文件分片
Int8Array 1 有符号小整数
Uint16Array 2 像素(RGBA 中的 RGB 常见 16 位)
Int32Array 4 常规整数运算
Float32Array 4 WebGL 顶点、音频样本

直接创建方式(自动分配 buffer):

const arr = new Uint8Array([0, 1, 2, 3]);
console.log(arr.buffer); // 底层 ArrayBuffer

灵魂拷问:TypedArray 和普通数组有什么区别?

  • 普通数组可以存不同类型,动态扩容;TypedArray 是固定类型、固定长度、内存连续,性能极高。
  • TypedArray 没有 pushpop 等会改变长度的方法。

3. 视图二:DataView —— “万能遥控器,还能切换字节序”

如果你觉得 TypedArray 太“死板”(只能统一类型),或者你需要处理 不同字节序(大端/小端)的数据,请找 DataView

DataView 可以让你在同一个 ArrayBuffer 上,随意读写任意类型的数据,并且 显式指定字节序

const buffer = new ArrayBuffer(4);
const dv = new DataView(buffer);

// 大端序写入 0x12345678
dv.setUint32(0, 0x12345678, false);

// 小端序读取前 2 字节作为 Uint16
console.log(dv.getUint16(0, true)); // 小端序 -> 0x5678

字节序(Endianness)是啥?
想象数字 0x12345678 在内存中摆放:大端序是 12 34 56 78(网络传输常用),小端序是 78 56 34 12(x86 常用)。DataView 让你随心所欲。

什么时候用 DataView?

  • 解析二进制文件格式(PNG、MP4、ZIP 等),它们往往混用不同大小的整数。
  • 处理网络协议(TCP、WebSocket 自定义协议)。
  • 任何需要跨平台交换二进制数据的场景。

4. 进阶:SharedArrayBuffer —— “两个线程抢同一块荒地”

普通 ArrayBuffer 是线程隔离的。如果你在主线程创建一个 buffer,传给 Web Worker,Worker 会得到它的 副本 —— 内存被复制,效率低,且无法同步修改。

SharedArrayBuffer 则允许多个线程 共享同一块内存,配合 Atomics 对象实现原子操作,避免数据竞争。

// 主线程
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
worker.postMessage(sharedBuffer); // 传的是引用,不是拷贝

// Worker 线程
self.onmessage = (e) => {
  const arr = new Int32Array(e.data);
  Atomics.add(arr, 0, 1);   // 原子加 1
  Atomics.notify(arr, 0, 1); // 通知主线程
};

注意:SharedArrayBuffer 需要网站开启 跨域隔离(设置 COOP/COEP 响应头),否则会被浏览器禁用(因为 Spectre 漏洞)。
所以普通业务里用得少,但高性能计算、多线程渲染会用。


5. 把二进制“穿件衣服”:Blob 和 File

ArrayBuffer 太底层了,它不知道什么是“图片”、什么是“文本文件”。
Blob 在它之上加了一层 元信息:MIME 类型(type),并且 不可变(immutable)。

const blob = new Blob(['<h1>Hello</h1>'], { type: 'text/html' });
console.log(blob.size, blob.type); // 16, "text/html"

Blob 的妙用

  • 生成临时 URL:URL.createObjectURL(blob) 用于 <img><video><a download> 等。
  • 切片上传:blob.slice() 实现大文件分片。
  • 转成 ArrayBuffer 或文本:await blob.arrayBuffer()await blob.text()

File 是 Blob 的子类,多了 namelastModified 属性,通常来自 <input type="file">

const file = fileInput.files[0];
console.log(file.name, file instanceof Blob); // true

为什么需要 Blob?
因为浏览器需要一种能代表“文件”的对象,并且可以安全地在主线程和 Worker 间传递(结构化克隆算法支持 Blob,而 ArrayBuffer 也可以传递但会转移所有权)。Blob 还能利用浏览器缓存、磁盘存储等。


6. 字符串与二进制之间的摆渡人:TextEncoder / TextDecoder

你肯定遇到过这种需求:把一个字符串变成二进制(比如发送到 WebSocket),或者反过来。
TextEncoderTextDecoder,它们专门处理 UTF-8 编码。

const encoder = new TextEncoder();
const uint8 = encoder.encode('你好🌍'); // Uint8Array(12)

const decoder = new TextDecoder('utf-8');
const backToString = decoder.decode(uint8); // "你好🌍"

为什么不用 unescape/encodeURIComponent 那些老古董?
那些是历史包袱,性能差且不支持全部 Unicode。TextEncoder 是标准现代 API,浏览器和 Node.js 都支持。

注意TextEncoder 只支持 UTF-8,而 TextDecoder 可以指定其他编码(如 gbk,但需要浏览器支持)。


7. Node.js 中的特殊存在:Buffer

在 Node.js 里,Buffer 是二进制数据的“亲儿子”。它类似于 Uint8Array,但多了许多便利方法:toStringwritefrom 等。

const buf = Buffer.from('Hello', 'utf8'); // <Buffer 48 65 6c 6c 6f>
buf.write('Hi', 0);
console.log(buf.toString()); // "Hillo"

// Buffer 和 Uint8Array 可以无缝互转
const uint8 = new Uint8Array(buf);
const buf2 = Buffer.from(uint8);

区别

  • Buffer 是 Node.js 独有的,浏览器没有。
  • Buffer 的方法更贴近文件/网络操作(比如 buf.slice() 的行为不同——它返回新 Buffer 但指向同一段内存,而 TypedArray slice 会复制)。
  • 现代 Node.js 也完全支持 Uint8Array,但 Buffer 依然更常用。

设计原因:Node.js 早期就引入了 Buffer,后来浏览器有了 Uint8Array,Node 为了兼容和性能保留了 Buffer,并且让两者共享内存。


8. 当二进制遇到“大块头”:Streams API

ArrayBufferBlob 都是一次性加载到内存。如果文件有几百 MB 甚至 GB,页面会卡死。
Streams API 让你像流水一样处理数据:边读边处理,内存占用极低。

// 从 Blob 获取流
const blob = new Blob(['a'.repeat(1024 * 1024)]);
const readableStream = blob.stream();
const reader = readableStream.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log('chunk size:', value.length); // value 是 Uint8Array
}

更有用的是 fetch 响应流:

const response = await fetch('https://example.com/bigfile.bin');
const reader = response.body.getReader(); // ReadableStream<Uint8Array>
// 逐步读取...

流还可以 管道 到 WritableStream(如文件写入)、TransformStream(如实时压缩/解压)。

何时用流?

  • 大文件上传/下载进度条
  • 实时音视频处理
  • 解析超大的 JSON/CSV(配合 JSON.parse 的流式库)

9. 表单上传专用选手:FormData

当你要上传文件(+ 其他字段)时,FormData 是最省心的方式。它会自动构建 multipart/form-data 请求体,并且可以添加 BlobFile

const formData = new FormData();
formData.append('username', 'alice');
formData.append('avatar', fileBlob, 'avatar.png');

fetch('/upload', { method: 'POST', body: formData });

注意:FormData 内部不会把文件读成字符串或 Buffer,而是直接以二进制分块形式发送,非常高效。


10. 文本化的二进制:Base64

Base64 是用 ASCII 字符表示二进制数据的方式,它把每 3 个字节变成 4 个可打印字符。常用于:

  • DataURL:data:image/png;base64,xxxx
  • 在 JSON 里传输小文件(比如缩略图)
  • 某些旧 API 只接受字符串

浏览器内置函数btoa(binary to ASCII)和 atob(反向)。
但是它们只接受“二进制字符串”(每个字符的码点 0-255),所以需要先转换:

function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

function base64ToArrayBuffer(base64) {
  const binary = atob(base64);
  const buffer = new ArrayBuffer(binary.length);
  const view = new Uint8Array(buffer);
  for (let i = 0; i < binary.length; i++) {
    view[i] = binary.charCodeAt(i);
  }
  return buffer;
}

注意:Base64 会使体积膨胀约 33%,不适合大文件。


11. 一张图理清所有关系(灵魂手绘版)

                       ┌─────────────────┐
                       │   二进制数据宇宙   │
                       └────────┬────────┘
                                │
          ┌─────────────────────┼─────────────────────┐
          │                     │                     │
          ▼                     ▼                     ▼
    ┌──────────┐        ┌────────────┐       ┌────────────┐
    │ArrayBuffer│        │SharedArrayBuffer│    │   Blob    │
    │(原始内存块)│        │(多线程共享内存)│       │(带MIME+不可变)│
    └────┬─────┘        └────────────┘       └─────┬──────┘
         │                                          │
    ┌────┴────┐                                   子类│
    ▼         ▼                                       ▼
TypedArray DataView                              ┌──────┐
(固定类型)  (灵活+字节序)                          │ File │
    │                                            └──────┘
    │  Node.js 版
    ▼
  Buffer

  字符串 ↔ 二进制:TextEncoder / TextDecoder
  流式处理:ReadableStream / WritableStream
  表单上传:FormData
  文本化:Base64 (btoa/atob)

12. 实战决策指南:我到底该用哪个?

你的需求 推荐工具 为什么
需要逐字节修改二进制数据(如加密、像素处理) Uint8ArrayDataView 直接操作底层内存,性能好
需要控制字节序(大端/小端) DataView TypedArray 固定使用 CPU 字节序
多线程共享大块数据并同步 SharedArrayBuffer + Atomics 避免拷贝,但要注意安全头
预览图片、视频,触发下载 Blob + URL.createObjectURL 生成本地 URL,浏览器原生支持
大文件分片上传 Blob.slice() + FormData 切片再组包,不卡主线程
处理用户选中的文件 File (继承自 Blob) 自带文件名等信息
字符串 ↔ UTF-8 二进制 TextEncoder / TextDecoder 标准、简洁、全覆盖 Unicode
Node.js 中读写文件或 socket Buffer 功能丰富,历史正统
超大文件处理(几百 MB+) ReadableStream 内存可控,支持背压
传统表单 + 文件上传 FormData 浏览器自动处理 multipart
在 JSON 或 CSS 里嵌入小图片 Base64(配合 Uint8Array 虽膨胀但方便

13. 最后一个段子:它们之间的关系就像……

  • ArrayBuffer:买了一块空地(内存),上面啥也没有。
  • TypedArray:铺上统一规格的地砖(比如 1m×1m 的方砖),可以走来走去(读写)。
  • DataView:一个能随时切换地板材质和形状的万能工具,还能让你决定“瓷砖缝隙朝哪边”(字节序)。
  • Blob:给空地拍了一张照片,还标注了“这是花园”还是“这是停车场”(MIME 类型),照片不可修改,但可以复制裁剪。
  • File:Blob 照片加上了一个相框,上面写着“我家后院.jpg”和拍摄时间。
  • TextEncoder/Decoder:空地和人类语言之间的翻译官。
  • Streams:在空地和目的地之间修了一条传送带,一边扔砖头一边在另一头收货。
  • Base64:把空地拍成照片后,又把照片印在了一件 T 恤上,虽然 T 恤变大了,但可以贴在任何地方(比如 JSON)。

结尾

JavaScript 的二进制生态看似庞杂,实则每个角色都对应一个 特定的抽象层次和场景。从最底层的 ArrayBuffer 到最上层的 BlobFile,再到流和文本编码,它们共同构成了前端处理“非文本数据”的完整能力。

希望这份文档能帮你和你的同事们 少一点“怎么又是个新对象”的困惑,多一点“原来如此”的会心一笑


最后,一句记忆口诀

内存裸地 ArrayBuffer,铺砖 TypedArray 和 DataView;
拍照穿 MIME 是 Blob,文件相册 File 承;
字符串过河找 Encoder,大块头走 Stream;
Node 亲儿叫 Buffer,表单上传 FormData;
文本化装 Base64,多线程共享 Shared。

祝你在二进制的海洋里,乘风破浪,永不 leak memory