当我们处理文件时(创建、上传、下载),经常会遇到二进制数据。
在 JavaScript 中有很多二进制数据的格式,比如
- ArrayBuffer、Uint8Array、DataView、Blob、File 等
最基本的二进制对象是 ArrayBuffer——对固定长度的连续内存空间的引用。
我们这样来创建它
let buffer = new ArrayBuffer(16); //创建一个长度为16的数组缓冲区
buffer.byteLength; // 16 访问buffer的字节长度
上面的代码会分配一个 16 个字节的连续内存空间,并用 0 进行预填充。
需要注意的是,ArrayBuffer 并不是某种东西的数组,它与 Array 没有任何共同之处:
- 它的长度是固定的,不能像数组一样增加或者减少。
- 它正好占用那么多内存空间
- 要访问单个字节,需要用“视图对象”来实现,而不能像数组一样用 buffer[index]来访问某个字节
ArrayBuffer 是一个内存区域,它里面存储着原始的字节序列。
想要操作 ArrayBuffer,我们需要用到“视图对象”
视图对象本身不存储任何东西,它就像一幅眼镜,通过它来解释 ArrayBuffer 中的字节。
比如以下视图对象:
- Uint8Array —— 将 ArrayBuffer 中的每个字节都视为 0-255 之间的单个数字(每个字节是 8 位)。这称为“8 位无符号整数”。
- Uint16Array —— 将每 2 个字节视为一个 0-65535 之间的整数。这称为“16 位无符号整数”
- Uint32Array —— 将每 4 个字节视为一个 0-4294967295 之间的整数。这称为“32 位无符号整数”
- Float64Array —— 将每 8 个字节视为一个
5.0x10-324
到1.8x10308
之间的浮点数。
比如说,0 的二进制是 0,1 的二进制是 01,2 的二进制是 10,3 的二进制是 11...255 的二进制是 1111 1111。
所以 Unit8Array 的二进制最高为一个字节 8 位数长度(byteLength),故而最大的整数就是 255。以此类推,Unit16Array 就占用 2 个字节,最长为 16 位。
如果用格子表示字节,那么 8 位字节下 0-255 只能占用一个格子,而 16 位字节下 0-255 可以占用两个格子。
ArrayBuffer 是核心对象,是原始的二进制数据,是所有的基础。
如果我们要对他进行操作(遍历、写入)等,基本上所有的操作,我们都必须使用视图(View)。
let buffer = new ArrayBuffer(16); // 创建16位字节长度的buffer
let view = new Uint32Array(buffer); // 用32位的视图对象去读它,将 buffer 视为一个 32 位整数的序列
console.log(Uint32Array.BYTES_PER_ELEMENT); //32位视图对象视角下 每个整数占4字节
console.log('view', view); // {0:0,1:0,2:0,3:0}
console.log(view.length); //4 里面存储4个整数 预设都为0
console.log(view.byteLength); //字节中长度 16
view[0] = 123;
view[1] = 456;
view[2] = 789;
for (let i of view) {
console.log(i); // 123 456 789 0 一共4个值
}
所有的视图有一个通用术语—— TypedArray(类型化数组) 。
它是视图对象的统称术语。并不存在 TypedArray 的构造函数,它可以用来表示 Uint8Array,也可以用来表示 Uint64Array 等视图。
之所以用这个统称是因为所有视图对象都有同一组方法和属性,用统称的形式更容易表达。
类型化数组的行为类似于常规数组:具有索引,也可以被迭代。
一个类型化数组的构造器(不管是 Int8Array 还是 Float64Array 都无关紧要),它们的行为取决于参数类型。
参数有 5 种变体:
new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
-
如果参数是一个 buffer 对象,则会创建一个视图。后面的参数则是可选的。
byteOffset 是起始位置(默认为 0)以及 length(默认是 buffer 的末尾),我们可以指定 buffer 的某些部分。
-
如果给定的是 Array,或者是其他类数组对象,则会创建一个相同长度的类型化数组,并复制其内容。
let arr = new Uint16Array([1, 2, 3, 4, 5]); console.log(arr.length); // 5 console.log(arr[1]); // 2 console.log(Array.isArray(arr)); // false 说明返回的是类型化数组
-
如果给定的是另一个类型化数组(TypedArray),会创建一个相同长度的类型化数组,并复制其内容。
let arr16 = new Uint16Array([1, 1000]); let arr8 = new Uint8Array(arr16); console.log(arr8[0]); // 1 console.log(arr8[1]); // 232 试图复制 1000,但无法将 1000 放进 8 位字节中
-
对于数字参数 length —— 创建类型化数组可以包含那么多元素,它的字节长度是 length 乘以单个 TypedArray.BYTES_PER_ELEMENT 的字节数。
let arr = new Uint16Array(4); // 为 4 个整数创建类型化数组 alert(Uint16Array.BYTES_PER_ELEMENT); // 每个整数 2 个字节 alert(arr.byteLength); // 8(字节中的大小)
-
不带参数的情况下,创建长度为 0 的类型化数组。
我们可以直接创建一个 TypedArray,而无需传入 ArrayBuffer。但是视图离不开底层的 ArrayBuffer,因此,除第一种情况(提供 buffer)外,其他所有情况都会自动创建 ArrayBuffer。
想要访问自动创建的 ArrayBuffer,可以用以下属性:
- arr.buffer —— 引用 ArratBuffer
- arr.byteLength —— ArrayBuffer 的字节长度
因此,我们总是可以从一个视图转换到另一个视图
let arr8 = new Uint8Array([0, 1, 2, 3]);
// 同一数据的另一个视图
let arr16 = new Uint16Array(arr8.buffer);
当我们将越界值写入类型化数组时,多余的位会被切除。
比如,Uint8Array 的最大字节长度为 8,当我们放入 256 时,256 的二进制值为 100000000(9 位数),但是 Unit8Array 最大字节数只能放 8 位,于是仅保存最右边的 8 位,其余部分会被切除。
所以结果会是 0。
257 的二进制格式位 100000001,最右边的 8 位会被储存,因此数组中会有 1
TypedArray 方法具有常规的 Array 方法,但是有个明显的例外
我们可以遍历(部署了 iterate 接口),map、slice、find 和 reduce 等。
但是我们没办法做:
- 没有 splice —— 因为我们没办法删除一个值,因为类型化数组时缓冲区(buffer)的视图,并且缓冲区(buffer)是固定的、连续的内存区域。我们所能做的就是分配一个零值。
- 没有 concat 方法
但拥有两种其他方法:
arr.set(fromArr,[offset])
从 offset 开始。将fromArr
中的所有元素复制到 arrarr.subarray([begin,end])
创建一个从begin
到end
(不包括)相同类型的新视图。这有点类似于 slice 方法,但不复制任何内容 —— 只是创建一个新视图,以对给定片段地数据进行操作。
DataView
是在ArrayBuffer
上的一种特殊的超灵活未类型化
的视图,它允许以任何格式访问任何偏移量(offset)的数据。
- 对于类型化的数组,构造器决定了其格式。整个数组应该是统一的,第 i 个数字是
arr[i]
。 - 通过 DataView。我们可以使用
.getUint8(i)
或者.getUint16(i)
之类的方法访问数据。我们在调用方法时选择格式,而不是在构造的时候。
语法:
new DataView(buffer, [byteOffset], [byteLength]);
- Buffer ——底层的 ArrayBuffer。与类型化数组不同的是,DataView 不会自己创建缓冲区(buffer)。我们需要实现准备好。
- byteOffset —— 视图的起始字节位置(默认为 0)
- byteLength —— 视图的字节长度(默认到 buffer 的末尾)
例如,这里我们从同一个 buffer 中提取不同格式的数字:
// 4 个字节的二进制数组,每个都是最大值 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
let dataView = new DataView(buffer);
// 在偏移量为 0 处获取 8 位数字
alert(dataView.getUint8(0)); // 255
// 现在在偏移量为 0 处获取 16 位数字,它由 2 个字节组成,一起解析为 65535
alert(dataView.getUint16(0)); // 65535(最大的 16 位无符号整数)
// 在偏移量为 0 处获取 32 位数字
alert(dataView.getUint32(0)); // 4294967295(最大的 32 位无符号整数)
dataView.setUint32(0, 0); // 将 4 个字节的数字设为 0,即将所有字节都设为 0
ArrayBuffer 是核心对象,是对固定长度的连续内存区域的引用。
几乎任何对 ArrayBuffer 的操作,都需要一个视图来完成。
- 视图可以是 TypedArray:
Uint8Array
,Uint16Array
,Uint32Array
—— 用于 8 位、16 位和 32 位无符号整数。Uint8ClampedArray
—— 用于 8 位整数,在赋值时便“固定”其值。Int8Array
,Int16Array
,Int32Array
—— 用于有符号整数(可以为负数)。Float32Array
,Float64Array
—— 用于 32 位和 64 位的有符号浮点数。
- DataView —— 使用方法来指定格式的视图,例如:getUint8(offset)
在大多数情况下,我们直接对类型化数组进行创建和操作,而将 ArrayBuffer 作为通用标识符隐藏起来。我们可以通过.buffer
来访问它,并在需要时创建另一个视图。
另外还有两个术语,用于对二进制数据进行操作的方法的描述:
- ArrayBufferView 是所有这些视图的总称
- BufferSource 是 ArrayBuffer 或者 ArrayBufferView 的总称
如果二进制数据实际上是一个字符串,我们可以用 textDecoder 对象在给定缓冲区 buffer 和编码格式 encoding 的情况下,将值读取到实际的字符串。可以理解为这是给实际内容字符串的二进制数据解码。
语法是这样的
-
创建一个解码对象
let decoder = new TextDecoder([label], [options]);
label —— 编码格式,默认为
utf-8
,同时也支持 big5、windows-1251 等编码格式options —— 可选对象:
- fatal —— 布尔值,如果为
true
则为无效字符(不可解码)字符抛出异常,否则用字符\uFFFD
替换无效字符。 - ignoreBOM —— 布尔值,如果为
true
则 BOM(可选的字节顺序 unicode 标记)
- fatal —— 布尔值,如果为
-
解码
let str = decoder.decode([input], [options]);
- Input —— 要被解码的
BufferSource
- options —— 可选对象:
- stream —— 对于解码流,为 true,则将传入的数据块(chunk)作为参数重复调用 decoder。在这种情况下,多字节的字符可能偶尔会在块与块之间分割。这个选项告诉 TextDecoder 记住未完成的字符,并在下一个数据块来的时候进行解码
- Input —— 要被解码的
以下为例子
let uint8Array = new Uint8Array([72, 101, 108, 108, 111]);
alert(new TextDecoder().decode(uint8Array)); // Hello
我们可以通过为其创建子数组视图来解码部分缓冲区:
let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]);
// 该字符串位于中间
// 在不复制任何内容的前提下,创建一个新的视图
let binaryString = uint8Array.subarray(1, -1);
alert(new TextDecoder().decode(binaryString)); // Hello
TextEncoder 做相反的事情 —— 将字符串转换为字节。
语法:
let encoder = new TextEncoder();
只支持 utf-8 编码。
它有两种方法:
encode(str)
—— 从字符串返回Uint8Array
。encodeInto(str, destination)
—— 将str
编码到destination
中,该目标必须为Uint8Array
。
let encoder = new TextEncoder();
let uint8Array = encoder.encode('Hello');
alert(uint8Array); // 72,101,108,108,111
ArrayBuffer 和视图 view 都是 ECMA 标准的一部分,是 JavaScript 的一部分。
此外,还有浏览器提供的高级对象 —— Blob。
Blob 由一个可选的字符串type
(MIME 类型)和blobParts
组成。
blobParts 是一系列其他 Blob 对象,字符串和 BufferSource。
构造函数的语法是
new Blob(blobParts, options);
- blobParts 是 Blob/BufferSource/String 类型的值的数组
- options 可选对象:
- type —— Blob 类型 通常是 MIME 类型,例如
image/png
- Ending —— 是否转换换行符,使 Blob 对应于当前操作系统的换行符(\r\n 或\n)。默认为“transparent”。
- type —— Blob 类型 通常是 MIME 类型,例如
例如:
// 从字符串创建Blob
let blob = new Blob(['<html></html'], { type: 'text/html' });
// 第一个参数必须是数组
//从类型化数组(typedArray)和字符串创建blob
let hello = new Uint8Array([72, 101, 108, 108, 111]); //二进制的“hello”
let blob = new Blob([hello, 'world'], {
type: 'text/plain',
});
我们可以用 slice 方法来提取 Blob 片段:
blob.slice([byteStart], [byteEnd], [contentType]);
byteStart
—— 起始字节,默认为 0。byteEnd
—— 最后一个字节(专有,默认为最后)。contentType
—— 新 blob 的type
,默认与源 blob 相同。
Blob
对象是不可改变的我们无法直接在
Blob
中更改数据,但我们可以通过slice
获得Blob
的多个部分,从这些部分创建新的Blob
对象,将它们组成新的Blob
,等。这种行为类似于 JavaScript 字符串:我们无法更改字符串中的字符,但可以生成一个新的改动过的字符串。
Blob 可以转化成 a、img 标签的 url,来显示它们的内容。
下面是一段通过点击链接,下载一个动态生成hello world
的 Blob 文件。
const a = document.createElement('a');
a.download = 'hello.txt';
const blob = new Blob(['hello', 'world'], { type: 'text/plain' });
const url = URL.createObjectURL(blob); // 从blob创建对象url
a.href = url;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url); // 从内部映射中移除引用,允许删除blob,释放内存
上面代码中,我们使用URL.createObjectURL(blob)
来为 blob 生成一段对象 URL,它大概长这样
'blob:chrome://new-tab-page/7df9762f-6d76-412f-badd-adf74b004fe7';
它的形式一般是blob:<origin>/<uuid>
浏览器内部会为每个URL.createObjectURL(blob)
生成的 URL 存储一个 URL ——> Blob 的映射。通过这个映射关系,我们能够访问到 Blob。
生成的 URL 仅在当前文档打开的状态下才有效。它允许 img、a 标签引用。
但是也有个副作用。这里的虽然有 Blob 的映射,但是 Blob 是保存在浏览器的内存中,因此,浏览器无法释放它。
在文档退出时,即进入 unload 生命周期,这个映射会被自动清除,因此 Blob 也应该被释放。但是如果应用程序一直不关闭,那这个内存就不会被释放。
因此,如果我们创建了 URL,但即使我们已经不需要 Blob 了,它还是会被挂在内存中。
我们需要调用URL.revokeObjectURL(url)
从内部映射中移除引用,允许 Blob 被删除,并释放内存。
所以在上面的例子中,我们只使用一次 Blob,使用完就马上调用URL.revokeObjectURL(url)
释放内存。
如果代码是这样的:
<!-- download 特性(attribute)强制浏览器下载而不是导航 -->
<a download="hello.txt" href="#" id="link">Download</a>
<script>
let blob = new Blob(['Hello, world!'], { type: 'text/plain' });
link.href = URL.createObjectURL(blob);
</script>
我们不能马上调用URL.revokeObjectURL(url)
,调用该方法时,由于映射被删除了,因此Blob
URL 无效。
URL.createObjectURL
的一个替代方法是,将 Blob
转换为 base64-编码的字符串
这种编码是将二进制数据表示为一个由 0-64 的 ASCII 码组成的字符串,浏览器会对其进行解码,并显示内容。
我们可以在data-url
中使用 base64 编码,它的格式是这样的
data:[<mediatype>][;base64],<data>
以下的 src 是 data-url 的应用。
<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
我们使用内置的FileReader
对象来讲 Blob 转换成 base64。它可以将 Blob 数据读取成多种格式。
下面是通过 base64 的形式来完成下载的代码
const a = document.createElement('a');
a.download = 'hello.txt';
const blob = new Blob(['hello', 'world'], { type: 'text/plain' });
const reader = new FileReader();
reader.readAsDataURL(blob); // 将blob读成data-URL
// readAsDataURL后触发obload事件
reader.onload = function () {
a.href = reader.result;
a.click();
};
document.body.appendChild(a);
以下是两种方式创建 URL 的对比
URL.createObjectURL(blob) | Blob 转换为 data-url |
---|---|
需要用 revoke 清理内存 | 无需撤销内存(revoke) |
直接访问 Blob,无需编码/解码 | 对大的 Blob 进行编码/解码时,性能和内存会有耗损 |
我们可以创建一个 image、image 的一部分、或者创建一个页面截图的 Blob。
图像的操作使用<canvas>
元素实现的:
- 使用
canvas.drawImage
在canvas
上绘制图像 - 调用
canvas.toBlob(callback,format,quality)
创建一个 Blob,并在创建完成后调用callback
在下面的示例中,图像被复制了,我们在创建 blob 之前,从中裁剪图像,或者在 canvas 上对其进行转换:
// 获取任何图像
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');
/*****或者使用promise替代回调******/
let blob = await new Promise((resolve, reject) =>
canvas.toBlob(resolve, 'image/png')
);
对于页面截屏,我们可以使用html2canvas的库,它会扫描一遍浏览器页面,并将其绘制在<canvas>
上。
Blob 构造器允许所有东西构造 blob,包括 BufferSource。
如果我们需要执行低级别的操作,则可以使用FileReader
从 blob 上获取最低级别的 ArrayBuffer:
// 从 blob 获取 arrayBuffer
let fileReader = new FileReader();
fileReader.readAsArrayBuffer(blob);
fileReader.onload = function (event) {
let arrayBuffer = fileReader.result;
};
ArrayBuffer、Uint8Array 及 BufferSource 是二进制的数据,而 Blob 表示有类型的二进制数据。
使用 Blob 在浏览器上能够完成非常常见的上传/下载操作。
XMLHttpRequest,fetch 等进行 Web 请求的方法可以自然地使用 Blob
,也可以使用其他类型的二进制数据。
我们可以非常轻松地在 Blob 和低级别的二进制数据类型之间进行转换:
- 使用
new Blob(...)
构造函数从一个类型化数组创建 Blob - 使用
FileReader
从Blob
中取出ArrayBuffer
,然后在其上创建一个视图(view),用于低级别的二进制处理。
File
对象继承自 Blob,并扩展了与文件系统有关的功能。
有两种方式可以获取它
- new File(fileParts,fileName,[options])
fileParts
—— Blob/BufferSource/String 类型值的数组。fileName
—— 文件名字符串。options
—— 可选对象:lastModified
—— 最后一次修改的时间戳(整数日期)。
<input type='file'/>
或其他浏览器接口来获取文件。在这种情况下,file 将从操作系统(os)获取信息。
由于 File 是继承自 Blob 的,所以 File 对象具有相同的属性,并额外有以下信息:
- name —— 文件名
- lastModified —— 最后一次修改时间
下面是从<input type='file'/>
中获取 File 对象的方式:
<input type="file" onchange="showFile(this)" />
<script>
function showFile(input) {
// 输入(input)可以选择多个文件,因此 input.files 是一个类数组对象。
// 这里我们只有一个文件,所以我们只取 input.files[0]。
let file = input.files[0];
alert(`File name: ${file.name}`); // 例如 my.png
alert(`Last modified: ${file.lastModified}`); // 例如 1552830408824
}
</script>
FileReader 是一个对象,它可以从 Blob 或者 File 对象上读取数据。
它使用事件来传递数据,因为从磁盘上读取数据可能比较费时间。
构造函数:
let reader = new FileReader(); // 没有参数
主要方法:
readAsArrayBuffer(blob)
—— 将数据读取为二进制格式的ArrayBuffer
。readAsText(blob, [encoding])
—— 将数据读取为给定编码(默认为utf-8
编码)的文本字符串。readAsDataURL(blob)
—— 读取二进制数据,并将其编码为 base64 的 data url。abort()
—— 取消操作。
read*方法的选择,取决于我们如何使用数据
readAsArrayBuffer
—— 用于二进制文件,执行低级别的二进制操作。诸如切片(slicing)之类的高级别操作,File 是继承自 Blob 的,所以我们可以直接调用它们而无需读取。readAsText
—— 用于文本文件,当我们需要获取字符串时。readAsDataURL
—— 当我们需要在 img 或者 a 标签的 src 引用数据时。(替代URL.createObjectURL(file)
)
读取过程中,有以下事件:
- loadstart —— 开始加载
- progress —— 在读取过程中出现
- load —— 读取完成,没有 error
- abort —— 调用 abort()方法
- error —— 出现 error
- loadend —— 读取完成,无论成功还是失败
读取完成后,我们可以通过以下方式获取读取结果
- reader.result —— 结果(成功情况)
- reader.error —— error(失败情况)
下面是读取文件的示例:
<input type="file" onchange="readFile(this)" />
<script>
function readFile(input) {
let file = input.files[0];
let reader = new FileReader();
reader.readAsText(file);
reader.onload = function () {
console.log(reader.result);
};
reader.onerror = function () {
console.log(reader.error);
};
}
</script>
File
对象继承自 Blob
。
除了 Blob
方法和属性外,File
对象还有 name
和 lastModified
属性,以及从文件系统读取的内部功能。我们通常从用户输入如 <input>
或拖放事件来获取 File
对象。
FileReader
对象可以从文件或 blob 中读取数据,可以读取为以下三种格式:
- 字符串(
readAsText
)。 ArrayBuffer
(readAsArrayBuffer
)。- data url,base-64 编码(
readAsDataURL
)。
但是,在很多情况下,我们不必读取文件内容。就像我们处理 blob 一样,我们可以使用 URL.createObjectURL(file)
创建一个短的 url,并将其赋给 <a>
或 <img>
。这样,文件便可以下载文件或者将其呈现为图像,作为 canvas 等的一部分。
而且,如果我们要通过网络发送一个 File
,那也很容易:像 XMLHttpRequest
或 fetch
等网络 API 本身就接受 File
对象。