二进制数据及文件

二进制数组

ArrayBuffer

对于二进制数据(如文件的创建、下载、上传),使用二进制性能会更高。

JavaScript 的二进制数据采用非标准方式实现

基本二进制对象:ArrayBuffer(对固定长度的连续内存空间引用)

// 分配一个 16 字节的连续内存空间,并用 0 进行预填充
let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
console.info(buffer.byteLength); // 16

注意

  • ArrayBuffer 只是一个连续空间的引用,储存着原始字节序列,此外什么也不是。
  • Array 要进行区别看待:长度固定,无法动态增加或减少长度。
  • 访问内部数据不能使用下标访问元素,需要使用视图对象

视图对象 TypedArray

想要查看 ArrayBuffer 内部数据,需要借助视图对象,统称为 TypedArray。其本身不存储任何数据,仅用来解释 ArrayBuffer

  • Uint8Array:将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(8 位无符号整数)。
  • Uint16Array:将每 2 个字节视为一个 0 到 65535 之间的整数(16 位无符号整数)。
  • Uint32Array:将每 4 个字节视为一个 0 到 4294967295 之间的整数(32 位无符号整数)。
  • Float64Array:将每 8 个字节视为一个约 ±2.2×10⁻³⁰⁸ 到 ±1.8×10³⁰⁸ 之间的浮点数(双精度 IEEE 754)。

因此,一个 16 字节长度的 ArrayBuffer 在不同视图对象中表示不同的数据:

Uint8Array0123456789101112131415
Uint16Array01234567
Uint32Array0123
Float64Array01

完整视图对象一览表:

类型名称位宽(bit)字节数(byte)取值范围(十进制)是否有符号特性说明
Uint8Array810 到 255标准无符号 8 位整数
Uint8ClampedArray810 到 255赋值时自动“钳制”:小于 0 → 0,大于 255 → 255,非整数四舍五入
Int8Array81-128 到 127有符号 8 位整数
Uint16Array1620 到 65,535无符号 16 位整数
Int16Array162-32,768 到 32,767有符号 16 位整数
Uint32Array3240 到 4,294,967,295无符号 32 位整数
Int32Array324-2,147,483,648 到 2,147,483,647有符号 32 位整数
Float32Array324约 ±1.2×10⁻³⁸ 到 ±3.4×10³⁸(含小数)单精度 IEEE 754 浮点数
Float64Array648约 ±2.2×10⁻³⁰⁸ 到 ±1.8×10³⁰⁸(含小数)双精度 IEEE 754 浮点数

  • 所有 TypedArray 都基于 ArrayBuffer,提供对底层二进制数据的高效访问。
  • Uint8ClampedArray 常用于图像处理(如 Canvas 的 ImageData),确保颜色值始终在 [0, 255] 范围内。

ArrayBuffer 的所有操作都需要借助于视图对象,只有基于视图对象才知道每次应该操作多少个字节空间。

let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer

let view = new Uint32Array(buffer); // 将 buffer 视为一个 32 位整数的序列

console.info(Uint32Array.BYTES_PER_ELEMENT); // 每个整数 4 个字节

console.info(view.length); // 4,它存储了 4 个整数
console.info(view.byteLength); // 16,字节中的大小

// 让我们写入一个值
view[0] = 123456;

// 遍历值
for(let num of view) {
  console.info(num); // 123456,然后 0,0,0(一共 4 个值)
}

所有的视图对象都有以下五种参数变体,除了第一种传入了 buffer,其余都会自动创建 buffer,因为 buffer 是基础。

如要访问底层的 ArrayBuffer,那么在 TypedArray 中有如下的属性:

  • typedArr.buffer —— 引用 ArrayBuffer

    因此可以将一种 TypedArray 转变为另一种 TypedArray

  • typedArr.byteLength —— ArrayBuffer 的长度。
// 会在 buffer 上创建视图,可以指定起始字节偏移量 byteOffset,以及长度 length
new TypedArray(buffer, [byteOffset], [length]);
// 给的是 Array 或者类数组对象,会创建一个相同长度的类型化数组,并复制其内容
new TypedArray(object);
// 给的是其他 TypedArray,创建一个相同长度的类型化数组,并复制其内容,
// 对于原始数据超出当前 TypedArray 类型所能表示的最大数据时会造成越界被截断
// 如:Uint8 表示范围 0-255(八位,全为 1:11111111),
// 转换前数据为 256(二进制为:100000000),则会被截断成后八位(00000000)即为 0
new TypedArray(typedArray);
// 数字参数 length —— 创建类型化数组以包含这么多元素。
// 它的字节长度将是 length 乘以单个 TypedArray.BYTES_PER_ELEMENT 中的字节数
new TypedArray(length);
// 创建长度为零的类型化数组
new TypedArray();

TypedArray 方法

  • set(arrayOrTypedArray[, offset])
    offset(默认为 0)位置开始,将另一个数组或类型化数组的所有元素复制到当前 TypedArray 中。

    const arr = new Uint8Array(5);
    arr.set([10, 20, 30], 1); // arr 变为 [0, 10, 20, 30, 0]
    
  • subarray([begin[, end]])
    返回一个相同类型的新 TypedArray 视图,覆盖从 beginend(不包括 end)的原始缓冲区区域;不复制数据,仅创建新视图。

    const arr = new Int16Array([100, 200, 300, 400]);
    const sub = arr.subarray(1, 3); // Int16Array [200, 300]
    sub[0] = 999;
    console.log(arr[1]); // 999(原数组也被修改)
    
  • slice([begin[, end]])
    返回一个新分配的、独立副本的 TypedArray,包含从 beginend 的元素(类似普通数组的 slice);会复制数据

    const arr = new Uint8Array([1, 2, 3, 4]);
    const copy = arr.slice(1, 3); // Uint8Array [2, 3]
    copy[0] = 99;
    console.log(arr[1]); // 2(原数组未变)
    

明确不支持的方法(与普通 Array 不同):

  • splice()
    不可用。TypedArray 基于固定大小的 ArrayBuffer,无法动态增删元素。若需“删除”,只能手动用 0 或其他值覆盖。

  • concat()
    不可用。不能直接拼接两个 TypedArray。可通过 set() 或新建更大的 TypedArray 手动实现合并。

支持的常见 Array 方法(部分列举):

TypedArray 支持大多数不改变长度的数组方法,例如:

  • map(), filter(), reduce(), find(), forEach()
  • indexOf(), includes(), every(), some()
  • reverse(), fill(), sort()
  • keys(), values(), entries()(用于迭代)

注意:filter()map() 等返回新 TypedArray(同类型),而非普通数组。

DataView 超灵活视图

允许以任何格式访问任何偏移量(offset)的数据。

创建语法,参数与常规 TypedArray 一致:

new DataView(buffer, [byteOffset], [byteLength])

类型化的数组,构造器已经决定了格式,数组是统一的,第 i 个元素就是 typedArr[i],而 DataView 允许在调用时选择格式,而非构造时。

// 4 个字节的二进制数组,每个都是最大值 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// 在偏移量为 0 处获取 8 位数字
console.info( dataView.getUint8(0) ); // 255

// 现在在偏移量为 0 处获取 16 位数字,它由 2 个字节组成,一起解析为 65535
console.info( dataView.getUint16(0) ); // 65535(最大的 16 位无符号整数)

// 在偏移量为 0 处获取 32 位数字
console.info( dataView.getUint32(0) ); // 4294967295(最大的 32 位无符号整数)

dataView.setUint32(0, 0); // 将 4 个字节的数字设为 0,即将所有字节都设为 0

二进制字符文本

TextDecoder

将给定缓冲区的二进制数据转为文本字符串。

创建语法:

let decoder = new TextDecoder([label], [options]);
  • label:编码格式,默认 utf-8,支持 big5windows-1251 等许多其他编码格式。
  • options:可选对象配置
    • fatal:布尔类型
      • true:为无效(不可解码)字符时抛出异常
      • false:(默认)用字符 \uFFFD 替换无效字符
    • ignoreBOM:布尔类型
      • true:忽略 BOM(可选的字节顺序 Unicode 标记),很少需要使用

解码语法:

let str = decoder.decode([input], [options]);
  • input:要被解码的 BufferSource
  • options:可选对象配置
    • stream:布尔类型
      • true:将传入的数据块(chunk)作为参数重复调用 decoder。这种情况下,多字节的字符可能偶尔会在块与块之间被分割。这个选项告诉 TextDecoder 记住“未完成”的字符,并在下一个数据块来的时候进行解码。
let uint8Array = new Uint8Array([228, 189, 160, 229, 165, 189]);

console.info( new TextDecoder().decode(uint8Array) ); // 你好

TextEncoder

将字符串转换为字节。

创建语法:

let encoder = new TextEncoder();

注意:只支持 UTF-8 编码。

两个方法:

  • encode(str)
    将字符串 str 编码为新的 Uint8Array(返回新数组,不修改原数据)。

    const encoder = new TextEncoder();
    const uint8 = encoder.encode("Hello"); // Uint8Array [72, 101, 108, 108, 111]
    
  • encodeInto(str, destination)
    将字符串 str 编码到预分配的 destination(必须是 Uint8Array)中,不返回新数组,而是返回对象 { read: number, written: number }(表示读取的字符数和写入的字节数)。

    const encoder = new TextEncoder();
    const destination = new Uint8Array(10); // 预分配缓冲区
    const result = encoder.encodeInto("Hello", destination);
    console.log(result); // { read: 5, written: 5 }
    console.log(destination); // Uint8Array [72, 101, 108, 108, 111, 0, 0, 0, 0, 0]
    

本质区别

方法返回值是否创建新数组适用场景
encode(str)Uint8Array✅ 是简单转换,无需复用缓冲区
encodeInto(str, destination)对象 { read, written }❌ 否高性能场景:复用现有缓冲区,避免内存分配(如流处理、网络传输)

为什么 encodeInto 不返回 Uint8Array
因为它的设计目标是直接操作预分配的缓冲区destination),而非创建新对象。这能显著减少内存分配开销(尤其在循环或大数据处理中),是 Web API 为性能优化的关键设计。

Blob

由一个可选的字符串 type(通常是 MIME 类型)和 blobParts 组成 —— 一系列其他 Blob 对象、字符串和 BufferSource

Blob = image/png(type) + blob1blob2blob3blob4...strbuffer(blobParts)

Blob 对象不允许修改,但可以自行对 blob 部分进行拼接。

创建语法:

new Blob(blobParts, options);

使用示例:

// 从类型化数组(typed array)和字符串创建 Blob
let hello = new Uint8Array([72, 101, 108, 108, 111]); // 二进制格式的 "hello"

let blob = new Blob([hello, ' ', 'world'], {type: 'text/plain'});
  • blobPartsBlob/BufferSource/String 类型的值的数组。
  • options 可选对象:
    • type —— Blob 类型,通常是 MIME 类型,例如 image/png
    • endings —— 是否转换换行符,使 Blob 对应于当前操作系统的换行符(\r\n\n)。默认为 "transparent"(啥也不做),不过也可以是 "native"(转换)。

可以用 slice 方法来提取 Blob 片段:

blob.slice([byteStart], [byteEnd], [contentType]);

允许传负数

  • byteStart —— 起始字节,默认为 0。
  • byteEnd —— 最后一个字节(不包括,默认为最后)。
  • contentType —— 新 blob 的 type,默认与源 blob 相同。

Blob 作 URL

let link = document.createElement('a');
link.download = 'hello.txt';

let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
// URL.createObjectURL 取一个 Blob,并为其创建一个唯一的 URL,形式为 blob:<origin>/<uuid>
// 类似于 blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273
link.href = URL.createObjectURL(blob);

link.click();

URL.revokeObjectURL(link.href);

对于创建的 Blob 链接,如果不再需要,应当手动移除引用,否则将会一直存在于内存中,无法释放。

  • URL.revokeObjectURL(url):从内部映射中移除引用,因此允许 Blob 被删除(如果没有其他引用的话),并释放内存。

Blob 转为 base64

除了使用 URL 创建链接外,也可将其转为 base64 编码的字符串作为链接:

<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">

实际效果:

通过 base64 进行下载:

let link = document.createElement('a');
link.download = 'hello.txt';

let blob = new Blob(['Hello, world!'], {type: 'text/plain'});

let reader = new FileReader();
reader.readAsDataURL(blob); // 将 Blob 转换为 base64 并调用 onload

reader.onload = function() {
  link.href = reader.result; // data url
  link.click();
};

对比一览表:

特性URL.createObjectURL(blob)Blob 转换为 Data URL (FileReader / blob.text() 等)
使用方式const url = URL.createObjectURL(blob);const reader = new FileReader(); reader.readAsDataURL(blob);
返回值类型短字符串(如 blob:https://example.com/abc-123长字符串(如 data:image/png;base64,iVBORw0KG...
内存占用低(仅创建引用,不复制数据)高(将整个 Blob 编码为 Base64 字符串,体积增大约 33%)
性能快速(O(1) 操作)较慢(需完整读取并编码 Blob,O(n) 时间和内存)
是否需要手动释放✅ 是,应调用 URL.revokeObjectURL(url) 避免内存泄漏❌ 否,Data URL 是普通字符串,随作用域自动回收
适用场景大文件预览、视频/音频播放、临时资源引用小文件嵌入(如图标、小图)、需要内联数据的场景
浏览器兼容性广泛支持(IE10+)广泛支持(IE10+,但需配合 FileReader)
可缓存性不可被 Service Worker 或 HTTP 缓存可直接嵌入 HTML/CSS,但无法被外部缓存
安全性同源策略保护,仅当前 origin 可访问无特殊限制,但大字符串可能影响页面性能
  • 优先使用 URL.createObjectURL:简单、高效,适合绝大多数场景。
  • 记得调用 revokeObjectURL:在确定不再需要该 URL 时(如元素卸载后)释放资源。
  • 仅对小 Blob 使用 Data URL:例如小于 100KB 的图像,用于 CSS background 或 <img src> 内联。
// 推荐用法示例
const url = URL.createObjectURL(blob);
img.src = url;
img.onload = () => URL.revokeObjectURL(url); // 加载完成后释放

Image 转 Blob

图像操作是通过 <canvas> 元素来实现的:

  1. 使用 canvas.drawImage 在 canvas 上绘制图像(或图像的一部分)。
  2. 调用 canvas 方法 .toBlob(callback, format, quality) 创建一个 Blob,并在创建完成后使用其运行 callback。
// 获取任何图像
let img = document.querySelector('img');

// 生成同尺寸的 <canvas>
let canvas = document.createElement('canvas');
canvas.width = img.clientWidth;
canvas.height = img.clientHeight;

let context = canvas.getContext('2d');

// 向其中复制图像(此方法允许剪裁图像)
context.drawImage(img, 0, 0);
// 我们 context.rotate(),并在 canvas 上做很多其他事情

// toBlob 是异步操作,结束后会调用 callback
canvas.toBlob(function(blob) {
  // blob 创建完成,下载它
  let link = document.createElement('a');
  link.download = 'example.png';

  link.href = URL.createObjectURL(blob);
  link.click();

  // 删除内部 blob 引用,这样浏览器可以从内存中将其清除
  URL.revokeObjectURL(link.href);
}, 'image/png');

或者使用 async/await:

let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));

Blob 转换为 ArrayBuffer

可以从 blob.arrayBuffer() 中获取最低级别的 ArrayBuffer

// 从 blob 获取 arrayBuffer
const bufferPromise = await blob.arrayBuffer();

// 或
blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer */);

Blob 转换为 Stream

读取和写入超过 2 GB 的 blob 时,将其转换为 arrayBuffer 的使用来说会更加占用内存。这种情况下,可以直接将 blob 转换为 stream 进行处理。

stream 是一种特殊的对象,可以从它那里逐部分地读取(或写入)

Blob 接口里的 stream() 方法返回一个 ReadableStream,在被读取时可以返回 Blob 中包含的数据。

// 从 blob 获取可读流(readableStream)
const readableStream = blob.stream();
const stream = readableStream.getReader();

while (true) {
  // 对于每次迭代:value 是下一个 blob 数据片段
  let { done, value } = await stream.read();
  if (done) {
    // 读取完毕,stream 里已经没有数据了
    console.log('all blob processed.');
    break;
  }

  // 对刚从 blob 中读取的数据片段做一些处理
  console.log(value);
}

File 和 FileReader

File

File 对象继承自 Blob,并扩展了与文件系统相关的属性和功能,主要用于表示用户设备上的文件。

获取方式

  1. 通过构造函数创建(较少使用):

    new File(fileParts, fileName, [options])
    
    • filePartsBlob / BufferSource / String 类型值的数组。
    • fileName:文件名字符串。
    • options:可选对象,支持:
      • lastModified:最后一次修改的时间戳(整数,毫秒)。
  2. 从浏览器接口获取(最常见):

    • <input type="file">
    • 拖放(Drag and Drop)
    • 剪贴板 API 等
      此时,File 对象会从操作系统获取真实的文件元信息。

属性(继承自 Blob 并扩展)

  • name
    文件名(只读)。

    console.log(file.name); // "report.pdf"
    
  • lastModified
    文件最后修改时间的时间戳(毫秒,自 Unix Epoch 起),只读。

    console.log(new Date(file.lastModified)); // Mon Mar 10 2025 14:30:00 GMT+0800
    

⚠️ 注意:<input type="file">files 是一个 FileList(类数组),即使只选一个文件也需通过 input.files[0] 访问。

FileReader

FileReader 是一个用于异步读取 BlobFile 内容的对象。由于读取可能涉及磁盘 I/O,因此采用事件驱动模型。

构造函数

  • new FileReader()
    创建一个新的 FileReader 实例,无参数。
    const reader = new FileReader();
    

主要方法

  • readAsArrayBuffer(blob)
    Blob 读取为二进制 ArrayBuffer,适用于处理原始二进制数据(如图像、音频解析)。

    reader.readAsArrayBuffer(file);
    
  • readAsText(blob, [encoding])
    Blob 读取为文本字符串;可指定编码(默认 'utf-8')。

    reader.readAsText(file, 'utf-8');
    
  • readAsDataURL(blob)
    Blob 读取并编码为 Base64 Data URL(如 data:image/png;base64,...),常用于 <img> 标签预览。

    reader.readAsDataURL(imageFile);
    
  • abort()
    中止当前读取操作;触发 abortloadend 事件。

    reader.abort(); // 取消读取
    

事件(按执行顺序)

事件触发时机
loadstart开始读取
progress读取过程中(可用于显示进度)
load成功完成读取
error读取出错
abort调用了 abort()
loadend读取结束(无论成功、失败或中止)

读取结果

  • reader.result
    读取成功后的结果,类型取决于所用的 readAs* 方法:

    • readAsArrayBufferArrayBuffer
    • readAsTextstring
    • readAsDataURLstring(Data URL)
  • reader.error
    读取失败时的 DOMException 对象。

基本使用示例

<input type="file" onchange="handleFile(this)">

<script>
function handleFile(input) {
  const file = input.files[0];
  if (!file) return;

  const reader = new FileReader();

  reader.onload = function(e) {
    console.log('内容:', reader.result);
    // e.target.result 等价于 reader.result
  };

  reader.onerror = function() {
    console.error('读取失败:', reader.error);
  };

  // 根据需求选择读取方式
  if (file.type.startsWith('text/')) {
    reader.readAsText(file);
  } else if (file.type.startsWith('image/')) {
    reader.readAsDataURL(file);
  }
}
</script>

补充说明

  • 不仅限于 FileFileReader 可读取任意 Blob 对象。
  • Web Workers 中的同步版本
    在 Web Worker 中可使用 FileReaderSync,其方法(如 readAsText()同步返回结果,不会触发事件:
    // 仅在 Worker 中可用
    const reader = new FileReaderSync();
    const text = reader.readAsText(blob);
    

提示:对于图片预览等场景,优先考虑 URL.createObjectURL(file)(性能更好),仅在需要字符串形式(如上传前校验文本内容)时使用 FileReader