前端必须会的 TypedArray:一文吃透

前言

  • 在 JavaScript 中,TypedArray 是一种特殊的数组类型,它提供了一种用于在内存缓冲中访问原始二进制数据的机制。与普通的 JavaScript 数组不同,TypedArray 可以存储多种数据类型,如整数、浮点数和布尔值等。
  • TypedArray 对象描述了底层二进制数据缓冲区的类数组视图。没有称为 TypedArray 的全局属性,也没有直接可用的 TypedArray 构造函数。但是,有很多不同的全局属性,其值是指定元素类型的类型化数组构造函数。

1)家族成员 & 核心概念

  • ArrayBuffer:原始内存块(字节数组),不含读写方法。

  • 视图(View) :基于同一 ArrayBuffer 的不同"读写方式":

    • TypedArray 视图(按固定类型顺序读写)
      Int8Array / Uint8Array / Uint8ClampedArray
      Int16Array / Uint16Array
      Int32Array / Uint32Array
      BigInt64Array / BigUint64Array
      Float32Array / Float64Array
    • DataView(按字节偏移随取随用,支持大小端)

你可以在同一个 ArrayBuffer 上创建多个视图(甚至重叠),实现零拷贝地读取不同字段。

javascript 复制代码
const buf = new ArrayBuffer(16);
const u8  = new Uint8Array(buf);        // 按字节
const f64 = new Float64Array(buf, 8, 1); // 从第8字节起读一个 Float64

2)什么时候选 TypedArray,什么时候用 DataView?

  • 已知数据全是同一类型的连续块 (像像素、PCM、顶点数据):用 TypedArray,高效、简单。
  • 需要按"结构体"字段解析 (混合类型、含大小端控制):用 DataView
ini 复制代码
// DataView 解析一个小端头部:magic(4B) + version(Uint16) + flags(Uint8)
function parseHeader(buf) {
  const dv = new DataView(buf);
  const magic   = dv.getUint32(0, true);      // true = little-endian
  const version = dv.getUint16(4, true);
  const flags   = dv.getUint8(6);
  return { magic, version, flags };
}

3)构造/切片/拷贝的必会用法

javascript 复制代码
const u8 = new Uint8Array(4);            // 4 个字节,默认 0
u8.set([1,2,3], 1);                      // 从索引 1 开始写入
const part = u8.subarray(1, 3);          // 视图(零拷贝)
const copy = u8.slice(1, 3);             // 拷贝出新 Buffer(有拷贝)

// typed ↔ normal array
const arr = Array.from(u8);              // [0,1,2,3]
const u16 = new Uint16Array([500, 1000]); // 溢出会按类型截断/取模

// 注意:Uint8ClampedArray 会将值夹在 [0, 255]
new Uint8ClampedArray([-20, 260]);       // -> [0, 255]

口诀:subarray 不拷贝、slice 会拷贝。大文件/流式处理优先 subarray。


4)文本编码/解码(UTF-8 安全方案)

不要用 atob/btoa 搞 UTF-8 文本,它们是 Latin-1。请用标准 API:

ini 复制代码
const enc = new TextEncoder();               // UTF-8
const dec = new TextDecoder('utf-8');

const u8  = enc.encode('你好 👋');           // Uint8Array
const str = dec.decode(u8);                  // '你好 👋'

5)与 Web API 的高频搭配

Fetch 流式下载

ini 复制代码
const res = await fetch(url);
const reader = res.body.getReader();          // chunks: Uint8Array
let received = 0;
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  received += value.byteLength;               // 直接处理二进制块
}

Blob / File / 下载

ini 复制代码
const u8 = new Uint8Array([0xFF, 0xD8, /* ... */]); // JPEG 片段
const blob = new Blob([u8], { type: 'image/jpeg' });
const url  = URL.createObjectURL(blob);

Canvas / WebGL / WebAudio / WASM

  • Canvas:ctx.putImageData() / ImageData.dataUint8ClampedArray
  • WebGL:gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)
  • WebAudio:AudioBuffer.getChannelData()Float32Array
  • WASM:与模块共享其 Linear Memory (ArrayBuffer) ,在 JS 用 new Uint8Array(wasmMemory.buffer) 读写

6)与 Node.js:Buffer 互操作

在 Node 里,BufferUint8Array 的子类,可直接互转:

ini 复制代码
const buf = Buffer.from([1,2,3]);        // Node Buffer
const u8  = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);

大多数库都接受 Uint8Array;在浏览器用 TypedArray,在 Node 用 Buffer/Uint8Array 都 OK。


7)跨线程传输与共享(性能关键)

结构化克隆 & 可转移(postMessage)

scss 复制代码
// 主线程 → Worker
const ab = new ArrayBuffer(1024);
postMessage(ab, [ab]); // 转移所有权:零拷贝
// 注意:转移后 ab.byteLength 变为 0(已移交)

共享内存(SharedArrayBuffer + Atomics)

适合生产者/消费者模型、无锁并发计数等:

javascript 复制代码
// 共享计数器
const sab = new SharedArrayBuffer(4);
const i32 = new Int32Array(sab);
Atomics.add(i32, 0, 1);                // 线程安全 + 可唤醒
const v = Atomics.load(i32, 0);

开启 SAB 需站点隔离头(COOP/COEP)等安全前置,生产环境再用。


8)大小端(Endianness)不会?就记这条

  • TypedArray 读写使用宿主平台的端序,但你几乎感知不到(因为它以"元素"为单位)。
  • DataViewgetUint32(offset, littleEndian) 必须显式传端序 来匹配协议规范。多数网络/文件协议是小端(比如 protobuf、WAV 等常见格式),也有大端的(例如某些老协议)。

9)性能与内存小贴士

  • 避免不必要的拷贝 :优先使用 subarray/多视图共享一个 ArrayBuffer
  • 批量写入typed.set(otherTyped, offset) 一次性写,比循环快。
  • 复用缓冲区:频繁分配大 ArrayBuffer 会触发 GC;复用池更稳。
  • 边界与对齐Int16Array/Float32Array 等建议在对齐地址上读写(大多数 JS 引擎已优化,但跨平台解析二进制文件时自己保证结构体布局更稳)。
  • BigInt 系列BigInt64Array/BigUint64Array 只能与 BigInt 值互操作,不能与普通 number 混用。

10)典型实战片段

A. 解析"结构体"头 + 后续 payload

ini 复制代码
function parsePacket(buf) {
  const dv = new DataView(buf);
  const len   = dv.getUint32(0, true);     // payload 长度 (LE)
  const type  = dv.getUint16(4, true);
  const flags = dv.getUint8(6);
  const payload = new Uint8Array(buf, 7, len); // 零拷贝视图
  return { len, type, flags, payload };
}

B. 拼接多个分片(零拷贝合并并不现实,需新缓冲)

csharp 复制代码
function concat(chunks) {
  const total = chunks.reduce((s, c) => s + c.byteLength, 0);
  const out = new Uint8Array(total);
  let off = 0;
  for (const c of chunks) { out.set(c, off); off += c.byteLength; }
  return out;
}

C. Base64 ↔ Uint8Array(浏览器现代安全做法)

javascript 复制代码
async function base64ToBytes(b64) {
  const res = await fetch(`data:application/octet-stream;base64,${b64}`);
  return new Uint8Array(await res.arrayBuffer());
}
function bytesToBase64(u8) {
  return btoa(String.fromCharCode(...u8)); // 小数据适用;大数据用分块
}

11)常见坑

  • 把普通 Array 当成二进制容器 :会慢且占内存;用 Uint8Array
  • 误用 atob/btoa 处理 UTF-8 :请用 TextEncoder/Decoder
  • 忘了端序DataView 一律明确 littleEndian 参数。
  • 滥用 slice 拷贝 :先问自己能否 subarray
  • bigint 与 number 混用:在 BigInt TypedArray 中只能用 BigInt 值。
  • 跨线程大量拷贝 :使用 可转移对象(第二参数)或 SAB。

结语

记住三件事:ArrayBuffer 是底座,TypedArray 读写同类数据,DataView 解析结构体与端序。把上面的模式背下来,你在前端处理任何二进制任务都会顺手很多。需要我把你项目里的某份二进制协议/文件格式(例如某设备上报、WAV/PNG/自定义包)用 TypedArray+DataView 写成解析器模板吗?

相关推荐
Mintopia5 小时前
扩散模型在 Web 图像生成中的技术演进:从“随机噪声”到“浏览器里的画家”
前端·javascript·aigc
跟橙姐学代码5 小时前
Python学习笔记:正则表达式一文通——从入门到精通
前端·python·ipython
召摇5 小时前
简洁语法的逻辑赋值操作符
前端·javascript
Watermelo6175 小时前
复杂计算任务的智能轮询优化实战
大数据·前端·javascript·性能优化·数据分析·云计算·用户体验
龙在天5 小时前
上线还好好的,第二天凌晨白屏,微信全屏艾特我...
前端
qczg_wxg5 小时前
React Native系统组件(二)
javascript·react native·react.js
芝士加5 小时前
月下载超2亿次的npm包又遭投毒,我学会了搭建私有 npm 仓库!
前端·javascript·开源
千汇数据的老司机5 小时前
交互体验升级:Three.js在设备孪生体中的实时数据响应方案
开发语言·javascript·交互
前端世界5 小时前
前端必看:为什么同一段 CSS 在不同浏览器显示不一样?附解决方案和实战代码
前端·css