在前端开发中,图片处理是高频需求之一,而理解进制知识和二进制数据操作是掌握图片处理的基石。无论是图片上传、格式转换,还是复杂的美化与压缩,都离不开对二进制数据的操作。然而,许多开发者可能对这方面的知识了解不深,或者由于长期未接触而逐渐遗忘。本文将深入讲解进制知识及前端处理二进制数据的核心 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 码(二进制) |
|---|---|---|
| F | 70 | 01000110 |
| r | 114 | 01110010 |
| o | 111 | 01101111 |
| n | 110 | 01101110 |
| t | 116 | 01110100 |
| e | 101 | 01100101 |
| n | 110 | 01101110 |
| d | 100 | 01100100 |
- 将这些二进制数连接起来得到一个长的二进制串
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 字符 |
|---|---|
| 17 | R |
| 39 | n |
| 9 | J |
| 47 | v |
| 27 | b |
| 39 | n |
| 17 | R |
| 37 | l |
| 27 | b |
| 38 | m |
| 16 | Q |
- 填充
“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 类型及其对应的存储格式:
| 类型 | 大小(字节单位) | 描述 |
|---|---|---|
| Int8Array | 1 | 8位二进制带符号整数 -2^7~(2^7) - 1 |
| Uint8Array | 1 | 8位无符号整数 0~(2^8) - 1 |
| Uint8ClampedArray | 1 | 0 到 255。当赋值超出这个范围时,小于 0 的值会被钳制为 0,大于 255 的值会被钳制为 255。 |
| Int16Array | 2 | 16位二进制带符号整数 -2^15~(2^15)-1 |
| Uint16Array | 2 | 16位无符号整数 0~(2^16) - 1 |
| Int32Array | 4 | 32位二进制带符号整数 -2^31~(2^31)-1 |
| Uint32Array | 4 | 32位无符号整数 0~(2^32) - 1 |
| Float32Array | 4 | 32位IEEE浮点数 |
| Float64Array | 8 | 64位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 URL | createObjectURL |
|---|---|---|
| 适用场景 | 小文件(如图标、SVG) | 大文件或高分辨率图像 |
| 数据体积 | 增加约 33%(Base64 编码) | 不增加数据体积 |
| 内存消耗 | 较高(尤其是大文件) | 较低 |
| 缓存支持 | 无法缓存 | 支持缓存 |
| 实时性 | 需要等待文件内容被读取并编码 | 实时生成 URL |
| 生命周期管理 | 自动释放 | 需手动调用revokeObjectURL |
最后
下面用一个图示总结:
掌握上述这些基础知识,有助于更深入地理解前端图片处理过程中二进制数据的操作逻辑,从而更清晰地把握数据处理的内在机制,为高效解决实际开发问题打好基础。
然而,需要说明的是,本文所介绍的二进制处理 API 并未涵盖所有相关内容。
在接下来的文章中,我们将基于这些基础知识,逐步深入到更高级的图像处理技术,包括图片的加载与渲染优化、格式转换、压缩算法的实现、Canvas 和 WebGL 在图像处理中的应用,以及如何利用现代 Web 技术(如 WebAssembly 和 GPU 加速)提升性能。如果你对这些内容感兴趣,欢迎继续关注本专栏,一起探索前端图像处理的更多可能性!