
随着用户设备的升级和浏览器的版本更新,许多二进制的操作都可以放到浏览器中进行了,如 PDF 的生成、对多个文件打包下载和音视频的编解码等,以便于降低服务端压力和节省资源。
到目前为止,浏览器已经提供了诸多操作二进制的数据类型,并且支持各种数据类型进行相互转换,接下来就来了解下。
ArrayBuffer
ArrayBuffer 是一个字节数组,通常在其他语言中称为“byte array”,该对象用来表示通用的、固定长度的原始二进制数据缓冲区。
你不能直接操作 ArrayBuffer 的内容,而是要通过 TypedArray 或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
基础使用
通过 ArrayBuffer 构造函数可以创建一个指定字节长度的 ArrayBuffer 对象。
它接受一个参数用于指定 ArrayBuffer 的大小(单位为字节),然后返回一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。
如果指定的 length
大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。
以下是一些常见的属性和方法:
属性/方法 | 说明 |
---|---|
ArrayBuffer.length | ArrayBuffer 构造函数的 length 属性,其值为 1 |
ArrayBuffer.isView(arg) | 判断是否是一种 ArrayBuffer 视图 |
ArrayBuffer.slice(begin[, end]) | 和 ArrayBuffer.prototype.slice() 功能相同 |
ArrayBuffer.transfer(oldBuffer [, newByteLength]) | 返回一个新的 ArrayBuffer 对象,其内容取自 oldBuffer 中的数据 |
ArrayBuffer.prototype.slice(begin[, end]) | 返回一个新的 ArrayBuffer ,它的内容是这个 ArrayBuffer 的字节副本 |
ArrayBuffer.prototype.byteLength | 只读属性,表示 ArrayBuffer 的 byte 的大小 |
和数组的区别
需要注意的是字节数组其实和我们平时使用的普通数组没有共性,相反存在着明显的异同点:
- 它正好占用了内存中的那么多空间。
- 它的长度是固定的,我们无法增加或减少它的长度。
- 要访问单个字节,需要另一个“视图”对象,而不是直接使用索引。
事实上,它就是一个内存区域。它里面存储了什么?无从判断。只是一个原始的字节序列。
const buffer = new ArrayBuffer(4);
console.log(buffer.byteLength); // 4
const buffer = new ArrayBuffer(4);
console.log(buffer.byteLength); // 4
如上所示,它会分配一个长度为 4 字节的连续内存空间,并用 0 进行预填充。
当需要对字节数组进行操作时,通常我们会使用类型数组对象。类型数组对象就像是一副“眼镜”,其本身并不存储任何东西。
TypedArray
类型化数组(TypedArray)对象描述了一个底层的二进制数据缓冲区的一个类数组视图(view),借此我们操作 ArrayBuffer 的内容。
基础介绍
事实上,没有名为 TypedArray 的全局属性,也没有一个名为 TypedArray 的构造函数。相反,有许多不同的全局属性,它们的值是特定元素类型的类型化数组构造函数:
类型 | 单个元素值的范围 | 大小(bytes) | 描述 | Web IDL 类型 | C 语言中的等价类型 |
---|---|---|---|---|---|
Int8Array | -128 to 127 | 1 | 8 位二进制有符号整数 | byte | int8_t |
Uint8Array | 0 to 255 | 1 | 8 位无符号整数(超出范围后从另一边界循环) | octet | uint8_t |
Uint8ClampedArray | 0 to 255 | 1 | 8 位无符号整数(超出范围后为边界值) | octet | uint8_t |
Int16Array | -32768 to 32767 | 2 | 16 位二进制有符号整数 | short | int16_t |
Uint16Array | 0 to 65535 | 2 | 16 位无符号整数 unsigned | short | uint16_t |
Int32Array | -2147483648 to 2147483647 | 4 | 32 位二进制有符号整数 | long | int32_t |
Uint32Array | 0 to 4294967295 | 4 | 32 位无符号整数 unsigned | long | uint32_t |
Float32Array | 1.2×10-38 to 3.4×1038 | 4 | 32 位 IEEE 浮点数(7 位有效数字,如 1.1234567) unrestricted | float | float |
Float64Array | 5.0×10-324 to 1.8×10308 | 8 | 64 位 IEEE 浮点数(16 有效数字,如 1.123...15) unrestricted | double | double |
BigInt64Array | -263 to 263-1 | 8 | 64 位二进制有符号整数 | bigint | int64_t (signed long long) |
BigUint64Array | 0 to 264-1 | 8 | 64 位无符号整数 | bigint | uint64_t (unsigned long long) |
如上的构造函数都可以接受多种参数,比如传入 length
参数时,一个内部的数组缓冲区会被创建在内存中,该缓存区的大小是传入的 length
乘以数组中每个元素的字节数(BYTES_PER_ELEMENT),每个元素的值都为 0。
操作字节数组
除了上面说的一种参数外,此处主要说的是:当传入一个 buffer
参数,或者再另外加上可选参数 byteOffset
和 length
时,一个新的类型化数组视图将会被创建,并可用于呈现传入的 ArrayBuffer 实例。
其中,byteOffset
和 length
参数指定了类型化数组视图将要暴露的内存范围。如果两者都未传入,那么整个 buffer
都会被呈现;如果仅仅忽略 length
,那么 buffer
中偏移了 byteOffset
后剩下的 buffer
将会被呈现。
比如我们先创建一个 16 字节的字节数组,然后分别将其传递给 Uint8Array、Uint16Array、Uint32Array 和 Float64Array 等类型化数组:
然后通过视图,我们就可以写入值或遍历字节数组:
const buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
const view = new Uint32Array(buffer); // 将 buffer 视为一个 32 位整数的序列
// 写入一个值
view[0] = 123456;
// 遍历值
for (const num of view) {
alert(num); // 123456,然后 0,0,0(一共 4 个值)
}
const buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
const view = new Uint32Array(buffer); // 将 buffer 视为一个 32 位整数的序列
// 写入一个值
view[0] = 123456;
// 遍历值
for (const num of view) {
alert(num); // 123456,然后 0,0,0(一共 4 个值)
}
DataView
除了 TypedArray 外,通过 DataView 视图也能操作字节数组,它是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
它可以接受一个 buffer
参数,或者再另外加上可选参数 byteOffset
和 byteLength
时,一个表示指定数据缓存区的新 DataView 对象将会被创建。
你可以把返回的对象想象成一个二进制字节缓存区的“解释器”——它知道如何在读取或写入时正确地转换字节码。这意味着它能在二进制层面处理整数与浮点转化、字节顺序等其他有关的细节问题。
// 4 个字节的二进制数组,每个都是最大值 255
const buffer = new Uint8Array([255, 255, 255, 255]).buffer;
const dataView = new DataView(buffer);
// 在偏移量为 0 处获取 8 位数字
console.log(dataView.getUint8(0)); // 255
// 现在在偏移量为 0 处获取 16 位数字,它由 2 个字节组成,一起解析为 65535
console.log(dataView.getUint16(0)); // 65535(最大的 16 位无符号整数)
// 在偏移量为 0 处获取 32 位数字
console.log(dataView.getUint32(0)); // 4294967295(最大的 32 位无符号整数)
dataView.setUint32(0, 0); // 将 4 个字节的数字设为 0,即将所有字节都设为 0
// 4 个字节的二进制数组,每个都是最大值 255
const buffer = new Uint8Array([255, 255, 255, 255]).buffer;
const dataView = new DataView(buffer);
// 在偏移量为 0 处获取 8 位数字
console.log(dataView.getUint8(0)); // 255
// 现在在偏移量为 0 处获取 16 位数字,它由 2 个字节组成,一起解析为 65535
console.log(dataView.getUint16(0)); // 65535(最大的 16 位无符号整数)
// 在偏移量为 0 处获取 32 位数字
console.log(dataView.getUint32(0)); // 4294967295(最大的 32 位无符号整数)
dataView.setUint32(0, 0); // 将 4 个字节的数字设为 0,即将所有字节都设为 0
可见 DataView 是在 ArrayBuffer 上的一种特殊的超灵活“未类型化”视图。它允许以任何格式访问任何偏移量(offset)的数据。
Blob
字节数组不仅能够通过类型数组和 DataView 视图进行操作,还能传递给 Blob 构造函数,然后得到一个新创建的 Blob 对象,其内容由参数中给定的数组拼接组成。
基础 API
Blob 对象表示一个不可变、原始数据的类文件对象,它表示的不一定是 JavaScript 原生格式的数据。File 接口基于 Blob,继承了 blob 的功能并将其扩展以支持用户系统上的文件。
类型 | API | 描述 |
---|---|---|
属性 | size(只读) | 数据的大小(字节) |
属性 | type(只读) | 数据的 MIME 类型。如果类型未知,则该值为空字符串 |
方法 | arrayBuffer() | 返回一个 Promise,数据为二进制格式的 ArrayBuffer |
方法 | slice() | 返回一个新的 Blob 对象,包含指定范围的数据 |
方法 | text() | 返回一个 Promise,包含所有内容的 UTF-8 格式的字符串 |
当用字节数组创建 Blob 对象时:
const buffer = new Uint8Array([255, 255, 255, 255]).buffer;
const blob = new Blob([buffer]);
console.log(blob.size);
const buffer = new Uint8Array([255, 255, 255, 255]).buffer;
const blob = new Blob([buffer]);
console.log(blob.size);
FileReader
常见的一种从 Blob 中读取内容的方法是使用 FileReader,FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。
以下代码将 Blob 的内容作为类型化数组读取:
const reader = new FileReader();
reader.addEventListener('loadend', () => {
// reader.result 包含被转化为类型化数组的 Blob 中的内容
console.log(reader.result);
});
reader.readAsArrayBuffer(blob);
const reader = new FileReader();
reader.addEventListener('loadend', () => {
// reader.result 包含被转化为类型化数组的 Blob 中的内容
console.log(reader.result);
});
reader.readAsArrayBuffer(blob);
另一种读取 Blob 中内容的方式是使用 Response 对象(Fetch API 的 Response 接口)。下述代码将 Blob 中的内容读取为文本:
const text = await new Response(blob).text();
const text = await new Response(blob).text();
另外,通过使用 FileReader 的其他方法可以把 Blob 读取为数据 URL 或者对象 URL。
Canvas
无论是数据 URL 还是对象 URL 的图片数据都可以传递给 <image>
标签进行展示,这在上传图片前进行预览时会很有用,而图片则又可以交由 Canvas 绘制。
Canvas API 提供了一个通过 JavaScript 和 HTML 的 <canvas>
元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。
<canvas id="canvas"></canvas>
<div style="display:none;">
<img id="source" src="demo.jpg" width="300" height="227" />
</div>
<canvas id="canvas"></canvas>
<div style="display:none;">
<img id="source" src="demo.jpg" width="300" height="227" />
</div>
其 API 中的 CanvasRenderingContext2D.drawImage()
方法提供了多种在画布(Canvas)上绘制图像的方式。
下面从原图像坐标 (33,71) 处截取一个宽度为 104 高度为 124 的图像。并将其绘制到画布的 (21, 20) 坐标处,并将其缩放为宽 87、高 104 的图像:
const canvas = document.getElementById('canvas');
const image = document.getElementById('source');
const ctx = canvas.getContext('2d');
image.addEventListener('load', e => {
ctx.drawImage(image, 33, 71, 104, 124, 21, 20, 87, 104);
});
const canvas = document.getElementById('canvas');
const image = document.getElementById('source');
const ctx = canvas.getContext('2d');
image.addEventListener('load', e => {
ctx.drawImage(image, 33, 71, 104, 124, 21, 20, 87, 104);
});
而根据 HTMLCanvasElement 中的 toBlob()
和 toDataURL()
则又可以得到对应的 Blob 对象和数据 URL。
Demo
预览本地图片:
// 通过 input[type=file] 选取本地文件
const fileList = e.target.files;
const file = fileList[0];
// 使用 FileReader 读取文件对象
const reader = new FileReader();
reader.onload = event => {
const imgUrl = event.target.result;
// 此处得到的 base64 的地址可直接提供过 img 标签进行加载预览
};
// 把文件对象作为一个 dataURL
reader.readAsDataURL(file);
// 通过 input[type=file] 选取本地文件
const fileList = e.target.files;
const file = fileList[0];
// 使用 FileReader 读取文件对象
const reader = new FileReader();
reader.onload = event => {
const imgUrl = event.target.result;
// 此处得到的 base64 的地址可直接提供过 img 标签进行加载预览
};
// 把文件对象作为一个 dataURL
reader.readAsDataURL(file);
将字符串内容下载为一个文件:
const json = {
heelo: 'world'
};
const josnStr = JSON.stringify(json, null, 2);
const jsonBlob = new Blob([josnStr], { type: 'application/json' });
const objectURL = URL.createObjectURL(jsonBlob);
const aEl = document.createElement('a');
aEl.download = 'hello.json';
aEl.rel = 'noopener';
aEl.href = objectURL;
aEl.dispatchEvent(new MouseEvent('click'));
URL.revokeObjectURL(objectURL);
const json = {
heelo: 'world'
};
const josnStr = JSON.stringify(json, null, 2);
const jsonBlob = new Blob([josnStr], { type: 'application/json' });
const objectURL = URL.createObjectURL(jsonBlob);
const aEl = document.createElement('a');
aEl.download = 'hello.json';
aEl.rel = 'noopener';
aEl.href = objectURL;
aEl.dispatchEvent(new MouseEvent('click'));
URL.revokeObjectURL(objectURL);
附录
- Data URL
Data URL,即前缀为 data: 协议的 URL,其允许内容创建者向文档中嵌入小文件。
Data URL 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身:
data:[<mediatype>][;base64],<data>
data:[<mediatype>][;base64],<data>
- Object URL
URL.createObjectURL()
静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个新的 URL 对象表示指定的 File 对象或 Blob 对象。
// 参数 object 为 File 对象、Blob 对象或者 MediaSource 对象
const objectURL = URL.createObjectURL(object);
// 参数 object 为 File 对象、Blob 对象或者 MediaSource 对象
const objectURL = URL.createObjectURL(object);
在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。
浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。