二进制知识及相关API的使用

421 阅读33分钟

在前端开发中,图片处理是高频需求之一,而理解进制知识和二进制数据操作是掌握图片处理的基石。无论是图片上传、格式转换,还是复杂的美化与压缩,都离不开对二进制数据的操作。然而,许多开发者可能对这方面的知识了解不深,或者由于长期未接触而逐渐遗忘。本文将深入讲解进制知识及前端处理二进制数据的核心 API,帮助大家更高效地应对开发中的二进制数据处理需求。

进制与数据存储单位

进制

进制是一种计数系统,它规定了用来表示数值的符号个数。我们日常使用的是十进制系统,其基数为10,使用 0~9 共十个符号;而计算机内部使用的二进制系统,基数为2,只使用 0 和 1 两个符号。此外,还有八进制(基数8)和十六进制(基数16),在前端开发中也经常出现,例如颜色表示中使用的十六进制代码。

1.1 数字的表示方法

  • 十进制(Decimal)
    • 使用 0~9 共 10 个符号表示,例:18、255、1001 等。
    • 直接使用数字表示,无需前缀。
  • 二进制(Binary)
    • 二进制表示的规则是逢二进一,仅使用数字 0 和 1,它是计算机数据表示的基础。
    • 如:十进制的 10 在计算机内保存为二进制的 1010。
    • 前缀为0b 或 0B。
  • 八进制(Octal)
    • 使用数字0到7表示数值,逢八进一。
    • 如:八进制数123,转换为十进制:1 * 8^2 + 2 * 8^1 + 3 * 8^0 = 83。
    • 前缀为 0(现代 JavaScript 开发中,尤其是严格模式下,八进制字面量应以 0o 或 0O 开头,而不是单独的 0)。
  • 十六进制(Hex)
    • 使用0到9以及A到F(分别表示10到15)这16个数字表示数值。
    • 如:十六进制的数值 2F,转换为十进制:2 * 16^1 + F * 16^0 = 47。
    • 前缀为0x 或 0X。

1.2 进制的转换

1.2.1 十进制转其他进制

将一个十进制数转换为目标进制的常用方法是“除基取余法”。以十进制转二进制为例:

  • 将十进制数不断除以 2,记录每次的余数;
  • 当商为 0 时停止;
  • 将余数按逆序排列,即得二进制表示。

示例: 将 18 转换为二进制

18 ÷ 2 = 9,余 0
9 ÷ 2 = 4,余 1
4 ÷ 2 = 2,余 0
2 ÷ 2 = 1,余 0
1 ÷ 2 = 0,余 1

逆序排列余数:1 0 0 1 0,即 18 的二进制为 10010

对于小数部分,则采用“乘2取整法”:不断将小数部分乘以 2,取乘积的整数部分作为二进制位,重复直到小数部分为 0 或达到精度要求。

1.2.2 其他进制转十进制

将任一进制数转换为十进制时,可以将该数拆解为各位数字与对应权重(基数的幂)的乘积之和。例如,将二进制数 1101 转为十进制:

1×2^3 + 1×2^2 + 0×2^1 + 1×2^0 = 8 + 4 + 0 + 1 = 13

同理,八进制和十六进制也遵循类似的规则,只不过基数分别为 8 和 16。

存储单位

2.1 位(Bit):

计算机在进行数据处理时,无论处理的是何种数据,最终都会将其转换成由“0”和“1”组成的二进制数据。

在计算机中,每个二进制位(bit,比特)只能表示一个0或1,是数据存储的最小单位。例如,“01010001”是由8个二进制位组成的数字,也就是一个8位的二进制数。

2.2 字节(Byte):

计算机中常用的存储单位通常是字节(byte),1 byte 等于 8 bits,即8个二进制位,那就能表示 2^8 = 256 种状态,取值从 00000000 到 11111111。所以,“01010001”这个8位的二进制数,占用了1个字节的存储空间。

字节序

计算机内存是以字节为单位进行寻址的,每个字节对应一个唯一的地址。一个字节存储8位二进制,即0~255之间,当需要存储大于255的数值的时,如16位、32位或64位的数值,就需要使用多个字节来存储。

这时,如何在内存中排列这些字节,即字节的顺序,就成为了一个问题。

字节序(Byte Order) 定义了多字节数据在内存中的存储顺序,主要有两种方式:

  • 大端序(Big Endian) :高位字节存储在低地址,低位字节存储在高地址。
  • 小端序(Little Endian) :低位字节存储在低地址,高位字节存储在高地址。

以存储十六进制数 0x12345678 为例,其二进制表示为:

0001 0010 0011 0100 0101 0110 0111 1000

占用4个字节,每个字节8位。

在大端序中,高位字节存储在低地址,低位字节存储在高地址。因此,0x12345678 在内存中的存储顺序如下:

地址    数据
0x00 -> 0x12
0x01 -> 0x34
0x02 -> 0x56
0x03 -> 0x78

在小端序中,低位字节存储在低地址,高位字节存储在高地址。因此,0x12345678 在内存中的存储顺序如下:

地址    数据
0x00 -> 0x78
0x01 -> 0x56
0x02 -> 0x34
0x03 -> 0x12

从视觉习惯上看,大端存储(Big Endian)似乎更符合我们从左到右的读数习惯,尤其是在处理数字时,似乎更直观地将最高位放在前面。无论使用大端存储还是小端存储,计算的结果最终都是一致的。

主要的区别在于字节的存储顺序,这会影响计算机内部如何处理这些数据。在不同的硬件架构或文件结构中,可能会采用不同的字节序,需要特别注意字节序的转换。

图片格式的字节序

不同格式的图片在字节序的存储上有不同的要求,有些格式严格要求字节序,而有些格式则没有严格规定字节序,或者不依赖于字节序进行数据解析

  • 大端存储:PNG、HEIF
  • 小端存储:GIF

对于有字节序要求的格式,在解析和数据提取过程中必须遵循图片格式规范中的字节序规则,否则可能导致错误的解析结果。

关于图片格式的解析与识别相关知识,将在后续章节中进行详细介绍。

2.3 字(Word):

“字”(word)是计算机数据存储处理运算的单位,但是一个字到底占多数个字节,不同的计算机架构方案,字的长度是不同的。在32位系统中,通常1字=4字节=32位;而在64位系统中,1字=8字节=64位。

32位系统一次可以处理4字节数据,而64位系统可以处理8字节数据,这也是为什么64位系统在处理大数据或高精度运算时通常具有更好的性能。

2.4 其他存储单位

通常我们更常用的是 KB、MB、GB、TB,它们之间的换算是:

  • 1KB=1024Byte
  • 1MB=1024KB
  • 1GB=1024MB
  • 1TB=1024GB
  • 1PB = 1024 TB
  • ... ...

Base64编码的原理与使用场景

Base64 编码的目的是将任意的二进制数据转换成一种由 64 个可打印字符组成的文本格式。这 64 个字符包括大写字母 A-Z、小写字母 a-z、数字 0-9,以及两个额外的字符 '+' 和 '/',有时还会用 '=' 来作为填充字符

Base64编码的过程

  • 将原始数据转换为二进制字节流

假设我们有一段原始数据(可以是文本、图片、音频等二进制数据),首先需要将其转换为二进制表示。例如,对于文本数据,每个字符通常用 8 位(1 个字节)的 ASCII 码或 Unicode 编码表示。

  • 按 6 位一组对二进制流进行分组

Base64 编码是将二进制数据按每 6 位一组进行分组,而计算机中通常以 8 位为一个字节来存储和处理数据。3 个字节正好是 24 位,可以完整地划分为 4 个 6 位的组,这种分组方式能够高效地利用数据位,实现二进制数据到 Base64 字符( 64 个字符)的准确转换。

如果数据的字节数不是 3 的整数倍,就会出现余数情况,需要特殊处理。

  • 处理不足 6 位的情况(补位)

如果最后一组不足 6 位,需要在右边补 0 使其成为 6 位。

  • 将每组 6 位二进制数转换为十进制数

将分组后的每组 6 位二进制数转换为对应的十进制数,这个十进制数的范围是 0 ~ 63。

  • 将十进制数映射到 Base64 字符表

Base64 字符表包含 A - Z(0 - 25)、a - z(26 - 51)、0 - 9(52 - 61)、+(62)、/(63)。

  • 处理填充

如果原始数据的字节数不是 3 的整数倍,编码后可能会出现余数。在这种情况下,编码结果通常会用 = 作为填充字符来表示余数的位置和数量。具体规则是:

    • 如果最后剩余 1 个字节,即 8 位二进制,先在后面补 4 个 0,形成两个 6 位组,其中后一组全为 0,编码后在末尾添加 2 个 =;
    • 如果最后剩余 2 个字节,即 16 位二进制,先在后面补 2 个 0,形成三个 6 位组,其中最后一组为 2 个 0,编码后在末尾添加 1 个 =。

示例

以 Frontend 这个字符串为例,对其进行 Base64 编码的过程如下:

  • 将字符串转换为 ASCII 码对应的二进制表示

每个字符都有对应的 ASCII 码值,我们将 "Frontend" 中每个字符转换为对应的 ASCII 码,再将这些 ASCII 码转换为 8 位二进制数。

字符ASCII 码(十进制)ASCII 码(二进制)
F7001000110
r11401110010
o11101101111
n11001101110
t11601110100
e10101100101
n11001101110
d10001100100
  • 将这些二进制数连接起来得到一个长的二进制串
01000110 01110010 01101111 01101110 01110100 01100101 01101110 01100100
  • 按 6 位一组进行划分,每6位切分一次
010001 100111 001001 101111 011011 100111 010001 100101 011011 100110 0100
  • 补位

由于最后一组不足 6 位,我们需要在后面补 0 使其成为 6 位:

010001 100111 001001 101111 011011 100111 010001 100101 011011 100110 010000
  • 将每组 6 位二进制数转换为十进制数
010001 (17)
100111 (39)
001001 (9)
101111 (47)
011011 (27)
100111 (39)
010001 (17)
100101 (37)
011011 (27)
100110 (38)
010000 (16)
  • 将十进制数映射到 Base64 字符表

Base64 字符表包含 A - Z(0 - 25)、a - z(26 - 51)、0 - 9(52 - 61)、+(62)、/(63)。根据这个映射关系:

十进制数Base64 字符
17R
39n
9J
47v
27b
39n
17R
37l
27b
38m
16Q
  • 填充

“Frontend” 这个字符串包含 8 个字符,每个字符在 ASCII 编码里占 1 个字节,所以总共有 8 个字节。

按照每 3 个字节一组来划分,8除以3 = 2(组)......2(个字节),也就是能完整分成 2 组 3 字节,还剩下 2 个字节。所以需要在编码结果末尾添加 1 个 =

  • 得到最终的 Base64 编码结果

最终 "Frontend" 的 Base64 编码结果为 "RnJvbnRlbmQ="。

编码后数据量增加

因Base64 编码使用 64 个特定字符(A-Z、a-z、0-9、+、/)和一个填充字符(=)来表示数据。由于原始二进制数据需映射到这 64 个字符,每 3 个字节(24 位)的二进制数据会被重新划分为 4 组,每组 6 位,对应一个 Base64 字符。若原始数据不足 3 字节,会以 0 填充至 3 字节。这一过程导致编码后字符数增多,数据量增加。如 3 字节二进制数据 00000001 00000010 00000011,经 Base64 编码会变为 4 个字符,数据量自然增加。Base64 编码会使数据量增加约 33%。

二进制数据处理相关的 API

ArrayBuffer

ArrayBuffer 是用于表示固定长度的原始二进制数据的对象,它是一个字节数组。ArrayBuffer 本身无法直接读写其内容,需要通过视图(如类型化数组 TypedArray 或 DataView)来操作其中的数据。

可以使用 ArrayBuffer 构造函数创建一个指定字节长度的缓冲区:

// 创建一个长度为 8 字节的 ArrayBuffer
const buffer = new ArrayBuffer(8);

打印结果:

从打印结果可以看到 ArrayBuffer 实例的属性和方法。

  • 实例属性 byteLength:返回 ArrayBuffer 所分配的内存区域的字节长度,该值在创建时确定,并且不可更改。
const buffer = new ArrayBuffer(8);
console.log(buffer.byteLength); // 输出: 8
  • 实例方法 slice(begin, end) :返回一个新的 ArrayBuffer,其内容是从原始 ArrayBuffer 的 begin(包含)到 end(不包含)之间的字节,可以分割ArrayBuffer。如果未指定 end,则默认到原 ArrayBuffer 的末尾。
const buffer = new ArrayBuffer(8);
const newBuffer = buffer.slice(0, 3);
console.log(newBuffer.byteLength); // 输出: 3

ArrayBuffer还有一些静态属性和方法:

  • ArrayBuffer.length:该属性表示 ArrayBuffer 构造函数的 length 属性,其值为 1,表示构造函数期望的参数个数。
  • ArrayBuffer.isView(arg):这是一个静态方法,用于判断给定的参数是否为 ArrayBuffer 的视图实例(如 TypedArray 或 DataView)。如果是,返回 true;否则,返回 false。
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
console.log(ArrayBuffer.isView(view)); // 输出: true
console.log(ArrayBuffer.isView(buffer)); // 输出: false

TypedArray

TypedArray 并不是一个具体的类,而是一组类型化数组的统称。它提供了一种机制,允许以特定的数据类型(如整数、浮点数等)来访问和操作存储在 ArrayBuffer 中的二进制数据

每个 TypedArray 都是一个视图(view),它指向底层的 ArrayBuffer,并按照指定的数据类型解释其中的字节。

2.1 常见的 TypedArray 类型

以下是 JavaScript 中常见的 TypedArray 类型及其对应的存储格式:

类型大小(字节单位)描述
Int8Array18位二进制带符号整数 -2^7~(2^7) - 1
Uint8Array18位无符号整数 0~(2^8) - 1
Uint8ClampedArray10 到 255。当赋值超出这个范围时,小于 0 的值会被钳制为 0,大于 255 的值会被钳制为 255。
Int16Array216位二进制带符号整数 -2^15~(2^15)-1
Uint16Array216位无符号整数 0~(2^16) - 1
Int32Array432位二进制带符号整数 -2^31~(2^31)-1
Uint32Array432位无符号整数 0~(2^32) - 1
Float32Array432位IEEE浮点数
Float64Array864位IEEE浮点数

2.2 TypedArray 的基本用法

  • 创建 TypedArray:使用 ArrayBuffer 创建一个固定大小的内存区域,然后基于此缓冲区创建一个 TypedArray。
// 创建一个 ArrayBuffer
const buffer = new ArrayBuffer(16); // 分配 16 字节的缓冲区

// 基于 ArrayBuffer 创建 TypedArray
const int32View = new Int32Array(buffer); // 每个元素占 4 字节,因此可以存储 4 个整数
int32View[0] = 1;
int32View[1] = 2;

console.log(int32View); // 输出: Int32Array [1, 2, 0, 0]
  • 直接初始化 TypedArray:也可以直接初始化一个 TypedArray,它会自动分配一个 ArrayBuffer
const uint8Array = new Uint8Array([10, 20, 30, 40]);
console.log(uint8Array); // 输出: Uint8Array [10, 20, 30, 40]
  • 使用 TypedArray 操作二进制数据

假设我们需要从一个文件中读取二进制数据,并将其解析为整数或浮点数:

// 假设我们有一个包含二进制数据的 ArrayBuffer
const buffer = new ArrayBuffer(8);
const dataView = new DataView(buffer);

// 写入数据
dataView.setInt32(0, 12345);    // 在偏移量 0 处写入一个 32 位整数
dataView.setFloat32(4, 3.14);   // 在偏移量 4 处写入一个 32 位浮点数

// 读取数据
const intVal = dataView.getInt32(0);    // 读取整数
const floatVal = dataView.getFloat32(4); // 读取浮点数

console.log(intVal, floatVal); // 输出: 12345, 3.14

2.3 与普通数组的区别

TypedArray 提供了与常规数组类似的方法和属性,如 slice、subarray、set 等,但不支持改变数组长度的方法,如 push 和 pop。此外,TypedArray 的长度是固定的,一旦创建就无法改变。

2.4 不支持指定字节序

TypedArray 不支持指定字节序,主要是为了简单和高效 。

  • 简单 :TypedArray 默认按主机系统的字节序处理数据,开发者不用额外考虑字节序,用起来更省心。
  • 高效 :如果每次读写数据都要调整或转换字节序,会拖慢速度。TypedArray 直接使用主机字节序,性能更高。

2.5 简单示例-图像数据处理

在 HTML5 的 API 中,使用 ImageData 对象获取的像素数据就是以 Uint8ClampedArray 的形式存储的。

每个像素由 4 个值表示(RGBA,红、绿、蓝、透明度),每个值的范围是 0 到 255。如果直接操作这些数据,可能会不小心写入超出范围的值。使用 Uint8ClampedArray 可以确保所有值都被限制在合法范围内,避免意外错误。

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

// 假设已经绘制了一些内容到 canvas 上
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // 这是一个 Uint8ClampedArray

// 修改像素值
for (let i = 0; i < data.length; i += 4) {
  data[i] = -10;     // 红色通道,被钳制为 0
  data[i + 1] = 300; // 绿色通道,被钳制为 255
  data[i + 2] = 128; // 蓝色通道,保持不变
  data[i + 3] = 255; // 透明度通道,保持不变
}

ctx.putImageData(imageData, 0, 0);

更多实践和使用方式,会在后面的章节中详细介绍。关于 TypedArray 更多实例属性和方法,参考:TypedArray - JavaScript | MDN

DataView

DataView 是另一个用于操作 ArrayBuffer 的接口对象,它可以从一块二进制数据(ArrayBuffer)中读取或写入不同类型的数据,比如整数、浮点数等。而且,它还允许指定数据的字节顺序 (大端或小端)。

3.1 DataView 的基本用法

  • 创建 DataView

DataView 必须基于一个 ArrayBuffer 来创建:

// 创建一个 ArrayBuffer
const buffer = new ArrayBuffer(16); // 分配 16 字节的缓冲区

// 基于 ArrayBuffer 创建 DataView
const dataView = new DataView(buffer);

console.log(dataView.byteLength); // 输出: 16

DataView 提供了一系列以 get- 和 set- 开头的方法,用来读取和写入不同类型的数据。这些方法非常直观,只需要指定偏移量和数据类型即可。

  • 写入数据

通过 set- 方法,可以将不同类型的数据写入到指定位置:

// 在偏移量 0 处写入一个 32 位有符号整数
dataView.setInt32(0, 12345);

// 在偏移量 4 处写入一个 32 位浮点数
dataView.setFloat32(4, 3.14);
  • 读取数据

通过 get- 方法,你可以从指定位置读取不同类型的数据:

// 从偏移量 0 处读取一个 32 位有符号整数
const intVal = dataView.getInt32(0);

// 从偏移量 4 处读取一个 32 位浮点数
const floatVal = dataView.getFloat32(4);

console.log(intVal, floatVal); // 输出: 12345, 3.14

3.2 DataView 的方法

DataView 提供了多种方法来读取和写入不同类型的数值。以下是一些常用的方法:

  • 写入方法
    • setInt8(byteOffset, value):在指定偏移量写入一个 8 位有符号整数。
    • setUint8(byteOffset, value):在指定偏移量写入一个 8 位无符号整数。
    • setInt16(byteOffset, value, littleEndian):在指定偏移量写入一个 16 位有符号整数,可以选择字节序。
    • setUint16(byteOffset, value, littleEndian):在指定偏移量写入一个 16 位无符号整数,可以选择字节序。
    • setInt32(byteOffset, value, littleEndian):在指定偏移量写入一个 32 位有符号整数,可以选择字节序。
    • setUint32(byteOffset, value, littleEndian):在指定偏移量写入一个 32 位无符号整数,可以选择字节序。
    • setFloat32(byteOffset, value, littleEndian):在指定偏移量写入一个 32 位浮点数,可以选择字节序。
    • setFloat64(byteOffset, value, littleEndian):在指定偏移量写入一个 64 位浮点数,可以选择字节序。
  • 读取方法
    • getInt8(byteOffset):从指定偏移量读取一个 8 位有符号整数。
    • getUint8(byteOffset):从指定偏移量读取一个 8 位无符号整数。
    • getInt16(byteOffset, littleEndian):从指定偏移量读取一个 16 位有符号整数,可以选择字节序。
    • getUint16(byteOffset, littleEndian):从指定偏移量读取一个 16 位无符号整数,可以选择字节序。
    • getInt32(byteOffset, littleEndian):从指定偏移量读取一个 32 位有符号整数,可以选择字节序。
    • getUint32(byteOffset, littleEndian):从指定偏移量读取一个 32 位无符号整数,可以选择字节序。
    • getFloat32(byteOffset, littleEndian):从指定偏移量读取一个 32 位浮点数,可以选择字节序。
    • getFloat64(byteOffset, littleEndian):从指定偏移量读取一个 64 位浮点数,可以选择字节序。

3.3 指定字节序

默认情况下,DataView 使用大端字节序。如果需要使用小端字节序,可以通过将 littleEndian 参数设置为 true 来指定。

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

// 写入 32 位整数,使用小端字节序
dataView.setInt32(0, 0x12345678, true);

// 读取 32 位整数,使用小端字节序
const value = dataView.getInt32(0, true);

console.log(value.toString(16)); // 输出: 12345678

更多实践和使用方式,会在后面的章节中详细介绍。关于 DataView 更多实例属性和方法,参考:DataView - JavaScript | MDN

Blob

Blob(Binary Large Object)是 JavaScript 中用于表示二进制数据的对象。它是一种不可变的、原始数据的类文件对象,通常用于处理文件或大块的二进制数据。Blob 可以看作是一个容器,用来存储二进制数据,并且可以通过多种方式操作这些数据。

Blob 的主要特点包括:

  • 不可变性 :一旦创建,Blob 对象的内容不能被修改。
  • 支持多种数据类型 :Blob 可以包含文本、图片、音频、视频等任何形式的二进制数据。
  • 分片操作 :可以通过 slice() 方法对 Blob 进行分割,生成新的 Blob 对象。

4.1 创建 Blob 对象

Blob 对象可以通过 Blob 构造函数创建,语法如下:

new Blob(array, options);
  • array :一个数组,包含了要放入 Blob 中的数据。数组中的元素可以是 ArrayBuffer、TypedArray、DataView、Blob 或字符串。
  • options (可选):一个配置对象,包含以下属性:
    • type:指定 Blob 的 MIME 类型(如 'image/png'、'text/plain' 等),默认为空字符串。
    • endings:指定如何处理换行符,默认为 'transparent',即不进行任何转换。

示例:

// 创建一个包含文本数据的 Blob 对象
const text = 'Hello, Blob!';
const blob = new Blob([text], { type: 'text/plain' });
console.log(blob); 

4.2 Blob 对象的属性和方法

属性

  • size:返回 Blob 对象中所包含数据的字节数。
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
console.log(blob.size); // 输出:13
  • type:返回 Blob 对象的 MIME 类型,如果类型未知,则返回空字符串。
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
console.log(blob.type); // 输出:'text/plain'

方法

  • slice(start, end, contentType):返回一个新的 Blob 对象,包含从原 Blob 中提取的部分数据。类似于数组的 slice() 方法。
    • start:可选,指定开始的字节偏移量,默认为 0。
    • end:可选,指定结束的字节偏移量,默认为 Blob 的大小。
    • contentType:可选,指定新 Blob 对象的 MIME 类型,默认为原 Blob 的类型。
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
const slicedBlob = blob.slice(0, 5, 'text/plain');
console.log(slicedBlob.size); // 输出:5
console.log(slicedBlob.type); // 输出:'text/plain'
  • stream():返回一个ReadableStream,可以用它来逐步读取Blob数据,适用于处理大文件或流式数据。

Stream(流)是一种抽象的概念,用于表示数据的流动。它允许程序以一种连续的方式处理数据,而不需要一次性将所有数据加载到内存中,从而节省内存资源。特别是 Node.js 环境下,Stream 是一个非常重要的模块,用于处理 I/O 操作(如文件读写、网络通信等)。

虽然 Blob 本身不直接提供 stream() 方法,但可以使用 ReadableStream 来处理 Blob 数据。

const blob = new Blob(['This is a very large text file with lots of data...'], { type: 'text/plain' });

// 将 Blob 转换为 ReadableStream
const readableStream = blob.stream();
const reader = readableStream.getReader();
const decoder = new TextDecoder();

function read() {
  reader.read().then(({ done, value }) => {
    if (done) {
      console.log('Stream complete');
      return;
    }

    // 模拟处理每个数据块
    console.log(`Received chunk of size: ${value.length} bytes`);
    console.log(`Chunk content: ${decoder.decode(value)}`); // 解码并输出内容

    read(); // 继续读取下一个数据块
  });
}

read();

4.3 Blob 对象的应用场景

  • 文件上传和下载
    • 在用户选择文件后,可以将文件转换为Blob对象,利用FormData对象将其发送到服务器。对于大文件,可以将其分割为多个Blob对象,逐个上传到服务器,实现大文件的分片上传。
// 对于大文件上传,Blob 的 slice() 方法可以将文件分割成多个小块,分别上传到服务器。
// 每个分块 1MB
function uploadFileInChunks(file, chunkSize = 1024 * 1024) {
  let start = 0;
  while (start < file.size) {
    const chunk = file.slice(start, start + chunkSize);
    uploadChunk(chunk); // 假设有一个 uploadChunk 函数负责上传每个分片
    start += chunkSize;
  }
}

function uploadChunk(chunk) {
  // 实现上传逻辑
  console.log('Uploading chunk:', chunk);
}
    • 通过 Blob 对象,可以生成文件并触发浏览器的下载行为:
const blob = new Blob(['Hello, world!'], { type: 'text/plain' });
const url = URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = 'hello.txt';
a.click();

URL.revokeObjectURL(url); // 释放 URL 对象
  • 图片预览

用户上传图片后,可以通过 FileReader 将图片文件读取为 Blob,并将其显示在页面上。

<!DOCTYPE html>
<html>
  <body>
    <input type="file" id="fileInput">
    <img id="imagePreview" src="#" alt="Preview">
    <script>
      const fileInput = document.getElementById('fileInput');
      const imagePreview = document.getElementById('imagePreview');
      fileInput.addEventListener('change', function () {
        const file = this.files[0];
        if (file) {
          const blob = new Blob([file], { type: file.type });
          const url = URL.createObjectURL(blob);
          imagePreview.src = url;
          imagePreview.onload = function () {
            URL.revokeObjectURL(url);
          };
        }
      });
    </script>
  </body>
</html>
  • IndexedDB 和 Blob 结合

以将大文件(如图片、视频、PDF 等)存储在本地,支持离线访问。

File

File 对象是 JavaScript 中用于表示文件的对象,它是 Blob 的子类。File 对象不仅继承了 Blob 的所有属性和方法,还增加了一些与文件相关的特性。它通常用于处理用户上传的文件(例如通过 获取的文件)。

File 对象的主要特点包括:

  • 继承自 Blob :File 对象继承了 Blob 的所有属性和方法,因此可以像操作 Blob 一样操作 File。
  • 文件元信息 :File 对象包含了文件的元信息,如文件名、文件类型、最后修改时间等。
  • 不可变性 :与 Blob 一样,File 对象的内容是不可变的。

5.1 File 对象的属性

File 对象除了继承 Blob 的属性外,还提供了以下特有的属性:

  • name:文件的名称(不包含路径),例如 'example.txt'。
const file = new File(['Hello, world!'], 'example.txt', { type: 'text/plain' });
console.log(file.name); // 输出:'example.txt'
  • lastModified:文件最后修改的时间戳(以毫秒为单位,表示自 Unix 时间纪元以来的时间)。
const file = new File(['Hello, world!'], 'example.txt', { type: 'text/plain' });
console.log(file.lastModified); // 输出:当前时间戳
  • type:文件的 MIME 类型,例如 'text/plain' 或 'image/png'。如果类型未知,则返回空字符串。
const file = new File(['Hello, world!'], 'example.txt', { type: 'text/plain' });
console.log(file.type); // 输出:'text/plain'
  • size:文件的大小(以字节为单位)。
const file = new File(['Hello, world!'], 'example.txt', { type: 'text/plain' });
console.log(file.size); // 输出:13

5.2 File 对象的使用方式

File 对象通常通过以下几种方式获取或创建:

  • 通过 获取:用户通过文件选择器上传文件时,可以通过 元素获取 File 对象。
<input type="file" id="fileInput">
<script>
  const input = document.getElementById('fileInput');
  input.addEventListener('change', (event) => {
    const files = event.target.files; // 返回一个 FileList 对象
    const file = files[0]; // 获取第一个文件
    console.log(file.name); // 输出文件名
    console.log(file.type); // 输出文件类型
    console.log(file.size); // 输出文件大小
  });
</script>
  • 通过拖放事件获取:用户可以通过拖放文件到页面上,触发 drop 事件,并从中获取 File 对象。
<div id="dropZone" style="width: 300px; height: 200px; border: 1px solid black;">
  Drop files here
</div>
<script>
  const dropZone = document.getElementById('dropZone');

  dropZone.addEventListener('dragover', (event) => {
    event.preventDefault();
  });

  dropZone.addEventListener('drop', (event) => {
    event.preventDefault();
    const files = event.dataTransfer.files; // 返回一个 FileList 对象
    const file = files[0]; // 获取第一个文件
    console.log(file.name); // 输出文件名
    console.log(file.type); // 输出文件类型
    console.log(file.size); // 输出文件大小
  });
</script>
  • 通过 Fetch API 获取:通过 fetch 请求获取文件时,响应体可以转换为 Blob 或 File 对象。
fetch('example.txt')
  .then(response => response.blob())
  .then(blob => {
    const file = new File([blob], 'example.txt', { type: 'text/plain' });
    console.log(file.name); // 输出:'example.txt'
    console.log(file.type); // 输出:'text/plain'
  });

5.3 File 对象的应用场景

同上面提到的 Blob 对象的应用场景。

FileReader

FileReader 是用于读取文件内容的 API,它允许开发者以异步的方式读取 Blob 或 File 对象中的数据。 FileReader 主要用于处理用户上传的文件(例如通过 获取的文件),并将文件内容转换为不同的格式(如文本、Data URL、ArrayBuffer 等)。

FileReader 的主要特点包括:

  • 异步操作 :FileReader 的读取操作是异步的,不会阻塞主线程,适合处理大文件。
  • 多种读取方式 :支持将文件内容读取为文本、二进制数据、Data URL 等多种形式。
  • 事件驱动 :FileReader 使用事件模型来处理读取过程中的不同状态(如读取完成、读取错误等)。

6.1 FileReader 的常用方法

FileReader 提供了多种方法来读取文件内容,以下是常用的几种方法:

  • readAsText(Blob|File, encoding) :将 Blob 或 File 对象的内容读取为文本字符串。
    • 参数 Blob|File: 要读取的文件或二进制数据对象。
    • 参数 encoding (可选): 指定文件的编码格式,默认为 'UTF-8'。
const file = new File(['Hello, world!'], 'example.txt', { type: 'text/plain' });
const reader = new FileReader();

reader.onload = function(event) {
  console.log(event.target.result); // 输出:'Hello, world!'
};

reader.readAsText(file);
  • readAsDataURL(Blob|File) :将 Blob 或 File 对象的内容读取为 Data URL(Base64 编码的字符串)。常用于图片预览。
    • 参数 Blob|File:要读取的文件或二进制数据对象。
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', (event) => {
  const file = event.target.files[0];
  const reader = new FileReader();

  reader.onload = function(event) {
    const img = document.createElement('img');
    img.src = event.target.result; // Data URL
    document.body.appendChild(img);
  };

  reader.readAsDataURL(file);
});
  • readAsArrayBuffer(Blob|File) :将 Blob 或 File 对象的内容读取为 ArrayBuffer,适合处理二进制数据。
const file = new File([new Uint8Array([72, 101, 108, 108, 111])], 'example.bin', { type: 'application/octet-stream' });
const reader = new FileReader();

reader.onload = function(event) {
  const arrayBuffer = event.target.result;
  console.log(new Uint8Array(arrayBuffer)); // 输出:Uint8Array [72, 101, 108, 108, 111]
};

reader.readAsArrayBuffer(file);
  • abort() :中止当前的文件读取操作。
const file = new File(['Hello, world!'], 'example.txt', { type: 'text/plain' });
const reader = new FileReader();

reader.onabort = function() {
  console.log('File reading aborted');
};

reader.readAsText(file);

// 手动中止读取
reader.abort();

6.2 FileReader 的事件

FileReader 使用事件模型来处理读取过程中的不同状态。以下是常见的事件:

  • onload:当文件读取成功完成时触发。
  • onerror:当文件读取过程中发生错误时触发。
  • onprogress:在文件读取过程中定期触发,可以用来显示读取进度。
  • onabort:当文件读取被取消时触发。
  • onloadstart:当文件读取开始时触发。
  • onloadend:当文件读取结束时触发,无论成功还是失败都会触发。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>FileReader Events Demo</title>
</head>
<body>
  <h1>FileReader Events Demo</h1>
  <input type="file" id="fileInput">
  <button id="abortButton" disabled>Abort Reading</button>
  <div id="output"></div>

  <script>
    const fileInput = document.getElementById('fileInput');
    const abortButton = document.getElementById('abortButton');
    const outputDiv = document.getElementById('output');

    let reader;

    fileInput.addEventListener('change', (event) => {
      const file = event.target.files[0];
      if (!file) return;

      // 创建 FileReader 实例
      reader = new FileReader();

      // 监听 onloadstart 事件(读取开始时触发)
      reader.onloadstart = function() {
        outputDiv.innerHTML += '<p>File reading started...</p>';
        abortButton.disabled = false; // 启用中止按钮
      };

      // 监听 onprogress 事件(读取过程中触发)
      reader.onprogress = function(event) {
        if (event.lengthComputable) {
          const percentLoaded = Math.round((event.loaded / event.total) * 100);
          outputDiv.innerHTML += `<p>Progress: ${percentLoaded}%</p>`;
        }
      };

      // 监听 onload 事件(读取成功完成时触发)
      reader.onload = function(event) {
        outputDiv.innerHTML += `<p>File read complete:</p><pre>${event.target.result}</pre>`;
        abortButton.disabled = true; // 禁用中止按钮
      };

      // 监听 onerror 事件(读取失败时触发)
      reader.onerror = function() {
        outputDiv.innerHTML += `<p>Error reading file: ${reader.error.message}</p>`;
        abortButton.disabled = true; // 禁用中止按钮
      };

      // 监听 onabort 事件(读取被中止时触发)
      reader.onabort = function() {
        outputDiv.innerHTML += '<p>File reading aborted.</p>';
        abortButton.disabled = true; // 禁用中止按钮
      };

      // 监听 onloadend 事件(读取结束时触发,无论成功还是失败)
      reader.onloadend = function() {
        outputDiv.innerHTML += '<p>File reading ended.</p>';
      };

      // 开始读取文件内容为文本
      reader.readAsText(file);
    });

    // 中止文件读取
    abortButton.addEventListener('click', () => {
      if (reader && reader.readyState === FileReader.LOADING) {
        reader.abort(); // 中止读取
      }
    });
  </script>
</body>
</html>

6.3 FileReader 的属性

  • readyState:当前状态:0(空)、1(加载中)、2(完成)。

6.4 应用场景

  • 文件读取进度条

通过 onprogress 事件,可以实现文件读取时的进度条功能。

const input = document.querySelector('input[type="file"]');
input.addEventListener('change', (event) => {
  const file = event.target.files[0];
  if (file) {
    const reader = new FileReader();

    reader.onprogress = function(event) {
      if (event.lengthComputable) {
        const percentLoaded = Math.round((event.loaded / event.total) * 100);
        console.log(`Progress: ${percentLoaded}%`);
      }
    };

    reader.onload = function(event) {
      console.log('File read complete:', event.target.result);
    };

    reader.readAsText(file);
  }
});
  • 模拟暂停和恢复的方式

FileReader 本身并不支持直接的暂停和恢复功能。它是一个一次性、异步的文件读取操作,一旦开始读取,就会一直进行直到完成,不能像流式读取那样进行暂停和恢复。

通过以下方式实现类似的功能:

    • 分块读取文件: 使用 slice() 方法将文件分割成多个块,逐步读取每个块。每次读取一部分,等待用户操作后再继续读取下一个部分。
    • 控制进度: 在 onprogress 事件中控制读取的节奏,比如通过设置定时器来延迟读取下一块数据,模拟暂停和恢复的效果。
const reader = new FileReader();
const chunkSize = 1024 * 1024; // 每次读取 1MB
let currentByte = 0; // 当前读取的位置

// 模拟暂停的状态
let isPaused = false;

// 读取文件的函数
function readFileChunk(file) {
  if (currentByte < file.size) {
    const chunk = file.slice(currentByte, currentByte + chunkSize);
    
    reader.onload = function(event) {
      // 处理当前读取的文件块
      console.log('Read chunk:', event.target.result);
      
      // 更新当前读取的位置
      currentByte += chunkSize;
      
      if (!isPaused) {
        // 继续读取下一个块
        readFileChunk(file);
      }
    };
    
    reader.readAsText(chunk); // 读取当前块
  } else {
    console.log('File reading completed');
  }
}

// 假设用户选择了一个文件
const file = document.getElementById('fileInput').files[0];

// 开始读取文件
readFileChunk(file);

// 用户点击暂停按钮时调用
function pauseReading() {
  isPaused = true;
  console.log('File reading paused');
}

// 用户点击恢复按钮时调用
function resumeReading() {
  isPaused = false;
  console.log('File reading resumed');
  readFileChunk(file); // 恢复从暂停的地方读取
}

// 用户点击暂停按钮
document.getElementById('pauseButton').addEventListener('click', pauseReading);

// 用户点击恢复按钮
document.getElementById('resumeButton').addEventListener('click', resumeReading);

URL.createObjectURL 和 URL.revokeObjectURL

这两个方法属于 URL 对象,它们用于生成和管理 Blob 或 File 对象的临时 URL。常见的应用场景包括在浏览器中预览文件(如图片、视频等)或创建可供下载的文件链接。

URL.createObjectURL 方法用于为 Blob 或 File 对象创建一个临时的 URL(通常称为对象 URL)。该 URL 是指向内存中的对象,而不是实际的文件路径,因此它并不持久,只能在当前浏览器会话中使用(刷新页面失效)。

URL.revokeObjectURL 方法用于释放通过 createObjectURL 创建的临时 URL。只要对象 URL 存在,浏览器就会保留与之关联的 Blob 或 File 对象在内存中,即使页面不再使用该对象。 因此开发者需要在不再使用时手动调用 revokeObjectURL 来释放内存。

图片预览场景示例:

const input = document.querySelector('input[type="file"]');
const img = document.createElement('img');

input.addEventListener('change', (event) => {
  const file = event.target.files[0];
  if (file) {
    const objectURL = URL.createObjectURL(file);
    img.src = objectURL; // 形式:blob:https://example.com/2c7a2834-85cd-468a-9d0e-6b85dba74180
    document.body.appendChild(img);

    // 手动释放 URL
    img.onload = () => {
      URL.revokeObjectURL(objectURL);
    };
  }
});

7.1 createObjectURL 的优点

在本地预览图片文件时,FileReader 是一种常见的方式。通过 FileReader 的 readAsDataURL() 方法,可以将图片文件转换为 Base64 编码的 Data URL。这种格式可以直接嵌入到 HTML 中(例如 标签的 src 属性),从而实现图片的本地预览。如下代码:

const fileInput = document.getElementById('fileInput');
const imgElement = document.getElementById('imgPreview');

fileInput.addEventListener('change', function(event) {
  const file = event.target.files[0]; // 获取上传的文件
  if (file) {
    const reader = new FileReader();
    reader.onload = function(e) {
      imgElement.src = e.target.result; // 设置 Base64 编码后的图像数据
    };

    // 当调用 reader.readAsDataURL(file) 时,
    // FileReader 会读取 file 对象的内容,并将其进行 Base64 编码。
    reader.readAsDataURL(file);  // 将文件转换为 Data URL
  }
});

对于小图像文件(如小图标、SVG 文件等),使用 Data URL 是非常方便且有效的。但由于 Base64 编码的效率较低,图像的大小通常会比原始文件大约增加 33%。这种冗余增加了浏览器的内存消耗,尤其当处理较大的图像时,会不太高效。另外,Data URL 是嵌入到 HTML 或 CSS 中的,浏览器无法单独缓存它,每次加载页面时都需要重新解析 Data URL

createObjectURL 生成的 URL 是指向浏览器内存中的数据,这个 URL 只是一个指向文件的引用,而不是将数据以字符串形式存储,因此不会增加数据体积。对于大文件或高分辨率图像,使用 createObjectURL 要比 Data URL 高效

对比总结:

特性Data URLcreateObjectURL
适用场景小文件(如图标、SVG)大文件或高分辨率图像
数据体积增加约 33%(Base64 编码)不增加数据体积
内存消耗较高(尤其是大文件)较低
缓存支持无法缓存支持缓存
实时性需要等待文件内容被读取并编码实时生成 URL
生命周期管理自动释放需手动调用revokeObjectURL

最后

下面用一个图示总结:

掌握上述这些基础知识,有助于更深入地理解前端图片处理过程中二进制数据的操作逻辑,从而更清晰地把握数据处理的内在机制,为高效解决实际开发问题打好基础。

然而,需要说明的是,本文所介绍的二进制处理 API 并未涵盖所有相关内容。

在接下来的文章中,我们将基于这些基础知识,逐步深入到更高级的图像处理技术,包括图片的加载与渲染优化、格式转换、压缩算法的实现、Canvas 和 WebGL 在图像处理中的应用,以及如何利用现代 Web 技术(如 WebAssembly 和 GPU 加速)提升性能。如果你对这些内容感兴趣,欢迎继续关注本专栏,一起探索前端图像处理的更多可能性!