Skip to content
JavaScript Binary

随着用户设备的升级和浏览器的版本更新,许多二进制的操作都可以放到浏览器中进行了,如 PDF 的生成、对多个文件打包下载和音视频的编解码等,以便于降低服务端压力和节省资源。

到目前为止,浏览器已经提供了诸多操作二进制的数据类型,并且支持各种数据类型进行相互转换,接下来就来了解下。

ArrayBuffer

ArrayBuffer 是一个字节数组,通常在其他语言中称为“byte array”,该对象用来表示通用的、固定长度的原始二进制数据缓冲区。

你不能直接操作 ArrayBuffer 的内容,而是要通过 TypedArray 或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。

基础使用

通过 ArrayBuffer 构造函数可以创建一个指定字节长度的 ArrayBuffer 对象。

它接受一个参数用于指定 ArrayBuffer 的大小(单位为字节),然后返回一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。

如果指定的 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。

以下是一些常见的属性和方法:

属性/方法说明
ArrayBuffer.lengthArrayBuffer 构造函数的 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 的大小

和数组的区别

需要注意的是字节数组其实和我们平时使用的普通数组没有共性,相反存在着明显的异同点:

  • 它正好占用了内存中的那么多空间。
  • 它的长度是固定的,我们无法增加或减少它的长度。
  • 要访问单个字节,需要另一个“视图”对象,而不是直接使用索引。

事实上,它就是一个内存区域。它里面存储了什么?无从判断。只是一个原始的字节序列。

js
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 12718 位二进制有符号整数byteint8_t
Uint8Array0 to 25518 位无符号整数(超出范围后从另一边界循环)octetuint8_t
Uint8ClampedArray0 to 25518 位无符号整数(超出范围后为边界值)octetuint8_t
Int16Array-32768 to 32767216 位二进制有符号整数shortint16_t
Uint16Array0 to 65535216 位无符号整数 unsignedshortuint16_t
Int32Array-2147483648 to 2147483647432 位二进制有符号整数longint32_t
Uint32Array0 to 4294967295432 位无符号整数 unsignedlonguint32_t
Float32Array1.2×10-38 to 3.4×1038432 位 IEEE 浮点数(7 位有效数字,如 1.1234567) unrestrictedfloatfloat
Float64Array5.0×10-324 to 1.8×10308864 位 IEEE 浮点数(16 有效数字,如 1.123...15) unrestricteddoubledouble
BigInt64Array-263 to 263-1864 位二进制有符号整数bigintint64_t (signed long long)
BigUint64Array0 to 264-1864 位无符号整数bigintuint64_t (unsigned long long)

如上的构造函数都可以接受多种参数,比如传入 length 参数时,一个内部的数组缓冲区会被创建在内存中,该缓存区的大小是传入的 length 乘以数组中每个元素的字节数(BYTES_PER_ELEMENT),每个元素的值都为 0。

操作字节数组

除了上面说的一种参数外,此处主要说的是:当传入一个 buffer 参数,或者再另外加上可选参数 byteOffsetlength 时,一个新的类型化数组视图将会被创建,并可用于呈现传入的 ArrayBuffer 实例。

其中,byteOffsetlength 参数指定了类型化数组视图将要暴露的内存范围。如果两者都未传入,那么整个 buffer 都会被呈现;如果仅仅忽略 length,那么 buffer 中偏移了 byteOffset 后剩下的 buffer 将会被呈现。

比如我们先创建一个 16 字节的字节数组,然后分别将其传递给 Uint8Array、Uint16Array、Uint32Array 和 Float64Array 等类型化数组:

abta

然后通过视图,我们就可以写入值或遍历字节数组:

js
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 参数,或者再另外加上可选参数 byteOffsetbyteLength 时,一个表示指定数据缓存区的新 DataView 对象将会被创建。

你可以把返回的对象想象成一个二进制字节缓存区的“解释器”——它知道如何在读取或写入时正确地转换字节码。这意味着它能在二进制层面处理整数与浮点转化、字节顺序等其他有关的细节问题。

js
// 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 对象时:

js
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 的内容作为类型化数组读取:

js
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 中的内容读取为文本:

js
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> 元素来绘制图形的方式。它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面。

html
<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 的图像:

js
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

预览本地图片:

js
// 通过 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);

将字符串内容下载为一个文件:

js
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 标记、数据本身:

plaintext
data:[<mediatype>][;base64],<data>
data:[<mediatype>][;base64],<data>
  • Object URL

URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个新的 URL 对象表示指定的 File 对象或 Blob 对象。

js
// 参数 object 为 File 对象、Blob 对象或者 MediaSource 对象
const objectURL = URL.createObjectURL(object);
// 参数 object 为 File 对象、Blob 对象或者 MediaSource 对象
const objectURL = URL.createObjectURL(object);

在每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。

浏览器在 document 卸载的时候,会自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。

Refs

Developed by Kisstar & Powered by VitePress.