ArrayBuffer,二进制数组
在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。 不过,在 JavaScript 中有很多种二进制数据格式,会有点容易混淆。比如: ArrayBuffer
,Uint8Array
,DataView
,Blob
,File
及其他。
- 基本的二进制对象是
ArrayBuffer
—— 对固定长度的连续内存空间的引用。JavaScriptlet buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer alert(buffer.byteLength); // 16
ArrayBuffer
不是某种东西的数组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
之间的浮点数。
ArrayBuffer
是核心对象,是所有的基础,是原始的二进制数据。
let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
let view = new Uint32Array(buffer); // 将 buffer 视为一个 32 位整数的序列
alert(Uint32Array.BYTES_PER_ELEMENT); // 每个整数 4 个字节
alert(view.length); // 4,它存储了 4 个整数
alert(view.byteLength); // 16,字节中的大小
// 让我们写入一个值
view[0] = 123456;
// 遍历值
for(let num of view) {
alert(num); // 123456,然后 0,0,0(一共 4 个值)
}
- TypedArray
- 所有这些视图(
Uint8Array
,Uint32Array
等)的通用术语是 TypedArray。它们共享同一方法和属性集。 - 没有名为
TypedArray
的构造器,它只是表示ArrayBuffer
上的视图之一的通用总称术语:Int8Array
,Uint8Array
及其他,很快就会有完整列表。 - 当你看到
new TypedArray
之类的内容时,它表示new Int8Array
、new Uint8Array
及其他中之一。
JavaScriptnew TypedArray(buffer, [byteOffset], [length]); new TypedArray(object); new TypedArray(typedArray); new TypedArray(length); new TypedArray();
- 如果给定的是
ArrayBuffer
参数,则会在其上创建视图。我们已经用过该语法了。 可选,我们可以给定起始位置byteOffset
(默认为 0)以及length
(默认至 buffer 的末尾),这样视图将仅涵盖buffer
的一部分。 - 如果给定的是
Array
,或任何类数组对象,则会创建一个相同长度的类型化数组,并复制其内容。
JavaScriptlet arr = new Uint8Array([0, 1, 2, 3]); alert( arr.length ); // 4,创建了相同长度的二进制数组 alert( arr[1] ); // 1,用给定值填充了 4 个字节(无符号 8 位整数)
- 如果给定的是另一个
TypedArray
,也是如此:创建一个相同长度的类型化数组,并复制其内容。如果需要的话,数据在此过程中会被转换为新的类型。
JavaScriptlet arr16 = new Uint16Array([1, 1000]); let arr8 = new Uint8Array(arr16); alert( arr8[0] ); // 1 alert( arr8[1] ); // 232,试图复制 1000,但无法将 1000 放进 8 位字节中(详述见下文)。
- 对于数字参数
length
—— 创建类型化数组以包含这么多元素。它的字节长度将是length
乘以单个TypedArray.BYTES_PER_ELEMENT
中的字节数:
JavaScriptlet arr = new Uint16Array(4); // 为 4 个整数创建类型化数组 alert( Uint16Array.BYTES_PER_ELEMENT ); // 每个整数 2 个字节 alert( arr.byteLength ); // 8(字节中的大小)
- 不带参数的情况下,创建长度为零的类型化数组。 我们可以直接创建一个
TypedArray
,而无需提及ArrayBuffer
。但是,视图离不开底层的ArrayBuffer
,因此,除第一种情况(已提供ArrayBuffer
)外,其他所有情况都会自动创建ArrayBuffer
。 如要访问底层的ArrayBuffer
,那么在TypedArray
中有如下的属性:arr.buffer
—— 引用ArrayBuffer
。arr.byteLength
——ArrayBuffer
的长度。
- 类型化数组的列表:
Uint8Array
,Uint16Array
,Uint32Array
—— 用于 8、16 和 32 位的整数。Uint8ClampedArray
—— 用于 8 位整数,在赋值时便“固定“其值(见下文)。
Int8Array
,Int16Array
,Int32Array
—— 用于有符号整数(可以为负数)。Float32Array
,Float64Array
—— 用于 32 位和 64 位的有符号浮点数。
- 没有
int8
或类似的单值类型- 尽管有类似
Int8Array
这样的名称,但 JavaScript 中并没有像int
,或int8
这样的单值类型。
- 尽管有类似
- 所有这些视图(
- 越界行为
- 尝试将越界值写入类型化数组会出现什么情况?不会报错。但是多余的位被切除。
let uint8array = new Uint8Array(16);
let num = 256;
alert(num.toString(2)); // 100000000(二进制表示)
uint8array[0] = 256;
uint8array[1] = 257;
alert(uint8array[0]); // 0
alert(uint8array[1]); // 1
Uint8ClampedArray
在这方面比较特殊,它的表现不太一样。对于大于 255 的任何数字,它将保存为 255,对于任何负数,它将保存为 0。此行为对于图像处理很有用。 4. TypedArray 方法 1. TypedArray
具有常规的 Array
方法,但有个明显的例外。我们可以遍历(iterate),map
,slice
,find
和 reduce
等。 2. 但是有几件事不行: - 没有 splice
—— 我们无法“删除”一个值,因为类型化数组是缓冲区(buffer)上的视图,并且缓冲区(buffer)是固定的、连续的内存区域。我们所能做的就是分配一个零值。 - 无 concat
方法。 - arr.set(fromArr, [offset])
从 offset
(默认为 0)开始,将 fromArr
中的所有元素复制到 arr
。 - arr.subarray([begin, end])
创建一个从 begin
到 end
(不包括)相同类型的新视图。这类似于 slice
方法(同样也支持),但不复制任何内容 —— 只是创建一个新视图,以对给定片段的数据进行操作。 5. DataView 1. DataView 是在 ArrayBuffer
上的一种特殊的超灵活“未类型化”视图。它允许以任何格式访问任何偏移量(offset)的数据。 - 对于类型化的数组,构造器决定了其格式。整个数组应该是统一的。第 i 个数字是 arr[i]
。 - 通过 DataView
,我们可以使用 .getUint8(i)
或 .getUint16(i)
之类的方法访问数据。我们在调用方法时选择格式,而不是在构造的时候。 2. 语法:new
DataView``(
buffer,
[
byteOffset]``,
[
byteLength]``)
- buffer
—— 底层的 ArrayBuffer
。与类型化数组不同,DataView
不会自行创建缓冲区(buffer)。我们需要事先准备好。 - byteOffset
—— 视图的起始字节位置(默认为 0)。 - byteLength
—— 视图的字节长度(默认至 buffer
的末尾)。 3. 从同一个 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
TextDecoder 和 TextEncoder
- TextDecoder 如果二进制数据实际上是一个字符串,内建的 TextDecoder 对象在给定缓冲区(buffer)和编码格式(encoding)的情况下,允许将值读取为实际的 JavaScript 字符串。
let decoder = new TextDecoder([label], [options]);
label
—— 编码格式,默认为utf-8
,但同时也支持big5
,windows-1251
等许多其他编码格式。options
—— 可选对象:fatal
—— 布尔值,如果为true
则为无效(不可解码)字符抛出异常,否则(默认)用字符\uFFFD
替换无效字符。ignoreBOM
—— 布尔值,如果为true
则忽略 BOM(可选的字节顺序 Unicode 标记),很少需要使用。
let str = decoder.decode([input], [options]);
input
—— 要被解码的BufferSource
。options
—— 可选对象:stream
—— 对于解码流,为 true,则将传入的数据块(chunk)作为参数重复调用decoder
。在这种情况下,多字节的字符可能偶尔会在块与块之间被分割。这个选项告诉TextDecoder
记住“未完成”的字符,并在下一个数据块来的时候进行解码。
- TextEncoder
- TextEncoder 做相反的事情 —— 将字符串转换为字节。
let encoder = new TextEncoder();
只支持 utf-8
编码。 它有两种方法:
encode(str)
—— 从字符串返回Uint8Array
。encodeInto(str, destination)
—— 将str
编码到destination
中,该目标必须为Uint8Array
。
Blob
arrayBuffer
和视图(view)都是 ECMA 标准的一部分,是 JavaScript 的一部分。 在浏览器中,还有其他更高级的对象,特别是 Blob
,在 File API 中有相关描述。 Blob
由一个可选的字符串 type
(通常是 MIME 类型)和 blobParts
组成 —— 一系列其他 Blob
对象,字符串和 BufferSource
。 构造函数的语法为:
new Blob(blobParts, options);
blobParts
是Blob
/BufferSource
/String
类型的值的数组。options
可选对象:type
——Blob
类型,通常是 MIME 类型,例如image/png
,endings
—— 是否转换换行符,使Blob
对应于当前操作系统的换行符(\r\n
或\n
)。默认为"transparent"
(啥也不做),不过也可以是"native"
(转换)。
// 从字符串创建 Blob
let blob = new Blob(["<html>…</html>"], {type: 'text/html'});
// 请注意:第一个参数必须是一个数组 [...]
// 从类型化数组(typed array)和字符串创建 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
,等。
- Blob 用作 URL
- Blob 可以很容易用作
<a>
、<img>
或其他标签的 URL,来显示它们的内容。 - 多亏了
type
,让我们也可以下载/上传Blob
对象,而在网络请求中,type
自然地变成了Content-Type
。
- Blob 可以很容易用作
<!-- 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>
也可以在 Javascript 中动态创建一个链接,通过 link.click()
模拟一个点击,然后便自动下载了。
let link = document.createElement('a');
link.download = 'hello.txt';
let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
URL.createObjectURL
取一个 Blob
,并为其创建一个唯一的 URL,形式为 blob:<origin>/<uuid>
。 浏览器内部为每个通过 URL.createObjectURL
生成的 URL 存储了一个 URL → Blob
映射。因此,此类 URL 很短,但可以访问 Blob
。 生成的 URL(即其链接)仅在当前文档打开的状态下才有效。它允许引用 <img>
、<a>
中的 Blob
,以及基本上任何其他期望 URL 的对象。 不过它有个副作用。虽然这里有 Blob
的映射,但 Blob
本身只保存在内存中的。浏览器无法释放它。 在文档退出时(unload),该映射会被自动清除,因此 Blob
也相应被释放了。但是,如果应用程序寿命很长,那这个释放就不会很快发生。 因此,如果我们创建一个 URL,那么即使我们不再需要该 Blob
了,它也会被挂在内存中。URL.revokeObjectURL(url)
从内部映射中移除引用,因此允许 Blob
被删除(如果没有其他引用的话),并释放内存。 在上面最后一个示例中,我们打算仅使用一次 Blob
,来进行即时下载,因此我们立即调用 URL.revokeObjectURL(link.href)
。 而在前一个带有可点击的 HTML 链接的示例中,我们不调用 URL.revokeObjectURL(link.href)
,因为那样会使 Blob
URL 无效。在调用该方法后,由于映射被删除了,因此该 URL 也就不再起作用了
- Blob 转换为 base64
URL.createObjectURL
的一个替代方法是,将Blob
转换为 base64-编码的字符串。- 这种编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读“。更重要的是 —— 我们可以在 “data-url” 中使用此编码。
- “data-url” 的形式为
data:[<mediatype>][;base64],<data>
。我们可以在任何地方使用这种 url,和使用“常规” url 一样。
<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
我们使用内建的 FileReader
对象来将 Blob
转换为 base64。它可以将 Blob
中的数据读取为多种格式。
let link = document.createElement('a');
link.download = 'hello.txt';
let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
let reader = new FileReader();
reader.readAsDataURL(blob); // 将 Blob 转换为 base64 并调用 onload
reader.onload = function() {
link.href = reader.result; // data url
link.click();
};
URL.createObjectURL(blob)
- 如果介意内存,我们需要撤销(revoke)它们
- 直接访问
Blob
,无需“编码/解码” Blob 转换为 data url - 无需撤销(revoke)任何操作。
- 对大的
Blob
进行编码时,性能和内存会有损耗。
- Image 转换为 blob
- 我们可以创建一个图像(image)的、图像的一部分、或者甚至创建一个页面截图的
Blob
。这样方便将其上传至其他地方。 - 图像操作是通过
<canvas>
元素来实现的:- 使用 canvas.drawImage 在 canvas 上绘制图像(或图像的一部分)。
- 调用 canvas 方法 .toBlob(callback, format, quality) 创建一个
Blob
,并在创建完成后使用其运行callback
。
- 我们可以创建一个图像(image)的、图像的一部分、或者甚至创建一个页面截图的
// 获取任何图像
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');
- Blob 转换为 ArrayBuffer
Blob
构造器允许从几乎任何东西创建 blob,包括任何BufferSource
。- 但是,如果我们需要执行低级别的处理时,我们可以从
blob.arrayBuffer()
中获取最低级别的ArrayBuffer
:
// 从 blob 获取 arrayBuffer
const bufferPromise = await blob.arrayBuffer();
// 或
blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer */);
- Blob 转换为 Stream
- 当我们读取和写入超过
2 GB
的 blob 时,将其转换为arrayBuffer
的使用对我们来说会更加占用内存。这种情况下,我们可以直接将 blob 转换为 stream 进行处理。 - stream 是一种特殊的对象,我们可以从它那里逐部分地读取(或写入)。这块的知识点不在本文的范围之内,但这里有一个例子,你可以在 https://developer.mozilla.org/en-US/docs/Web/API/Streams_API 了解更多相关内容。对于适合逐段处理的数据,使用 stream 是很方便的。
Blob
接口里的stream()
方法返回一个ReadableStream
,在被读取时可以返回Blob
中包含的数据。
- 当我们读取和写入超过
// 从 blob 获取可读流(readableStream)
const readableStream = blob.stream();
const stream = readableStream.getReader();
while (true) {
// 对于每次迭代:value 是下一个 blob 数据片段
let { done, value } = await stream.read();
if (done) {
// 读取完毕,stream 里已经没有数据了
console.log('all blob processed.');
break;
}
// 对刚从 blob 中读取的数据片段做一些处理
console.log(value);
}
File 和 FileReader
File 对象继承自 Blob
,并扩展了与文件系统相关的功能。 有两种方式可以获取它。 第一种,与 Blob
类似,有一个构造器:
new File(fileParts, fileName, [options])
fileParts
—— Blob/BufferSource/String 类型值的数组。fileName
—— 文件名字符串。options
—— 可选对象:lastModified
—— 最后一次修改的时间戳(整数日期)。
第二种,更常见的是,我们从 <input type="file">
或拖放或其他浏览器接口来获取文件。在这种情况下,file 将从操作系统(OS)获得 this 信息。
<input type="file" onchange="showFile(this)">
<script>
function showFile(input) {
let file = input.files[0];
alert(`File name: ${file.name}`); // 例如 my.png
alert(`Last modified: ${file.lastModified}`); // 例如 1552830408824
}
</script>
name
—— 文件名,lastModified
—— 最后一次修改的时间戳。- 输入(input)可以选择多个文件,因此
input.files
是一个类数组对象。这里我们只有一个文件,所以我们只取input.files[0]
。
- FileReader
- 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
—— 当我们想在src
中使用此数据,并将其用于img
或其他标签时。正如我们在 Blob 一章中所讲的,还有一种用于此的读取文件的替代方案:URL.createObjectURL(file)
。
- 读取过程中,有以下事件:
loadstart
—— 开始加载。progress
—— 在读取过程中出现。load
—— 读取完成,没有 error。abort
—— 调用了abort()
。error
—— 出现 error。loadend
—— 读取完成,无论成功还是失败。
- 读取完成后,我们可以通过以下方式访问读取结果:
reader.result
是结果(如果成功)reader.error
是 error(如果失败)。
html<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>
FileReader
用于 blob。可以使用它将 blob 转换为其他格式:readAsArrayBuffer(blob)
—— 转换为ArrayBuffer
,readAsText(blob, [encoding])
—— 转换为字符串(TextDecoder
的一个替代方案),readAsDataURL(blob)
—— 转换为 base64 的 data url。
- 在 Web Workers 中可以使用
FileReaderSync
- 对于 Web Worker,还有一种同步的
FileReader
变体,称为 FileReaderSync。 - 它的读取方法
read*
不会生成事件,但是会像常规函数那样返回一个结果。
- 对于 Web Worker,还有一种同步的
- FileReader 是一个对象,其唯一目的是从