二进制数据和文件
一、ArrayBuffer 和二进制数组
当我们处理文件时(创建、上传、下载),经常会遇到二进制数据。
在 JavaScript 中有很多二进制数据的格式,比如
- ArrayBuffer、Uint8Array、DataView、Blob、File 等
最基本的二进制对象是 ArrayBuffer——对固定长度的连续内存空间的引用。
我们这样来创建它
javascript1let buffer = new ArrayBuffer(16) //创建一个长度为16的数组缓冲区 2buffer.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)。
javascript1let buffer = new ArrayBuffer(16) // 创建16位字节长度的buffer 2let view = new Uint32Array(buffer) // 用32位的视图对象去读它,将 buffer 视为一个 32 位整数的序列 3console.log(Uint32Array.BYTES_PER_ELEMENT) //32位视图对象视角下 每个整数占4字节 4console.log('view', view) // {0:0,1:0,2:0,3:0} 5console.log(view.length) //4 里面存储4个整数 预设都为0 6console.log(view.byteLength) //字节中长度 16 7 8view[0] = 123 9view[1] = 456 10view[2] = 789 11 12for (let i of view) { 13 console.log(i) // 123 456 789 0 一共4个值 14}
1.1、TypedArray
所有的视图有一个通用术语—— TypedArray(类型化数组) 。
它是视图对象的统称术语。并不存在 TypedArray 的构造函数,它可以用来表示 Uint8Array,也可以用来表示 Uint64Array 等视图。
之所以用这个统称是因为所有视图对象都有同一组方法和属性,用统称的形式更容易表达。
类型化数组的行为类似于常规数组:具有索引,也可以被迭代。
一个类型化数组的构造器(不管是 Int8Array 还是 Float64Array 都无关紧要),它们的行为取决于参数类型。
参数有 5 种变体:
javascript1new TypedArray(buffer, [byteOffset], [length]) 2new TypedArray(object) 3new TypedArray(typedArray) 4new TypedArray(length) 5new TypedArray()
-
如果参数是一个 buffer 对象,则会创建一个视图。后面的参数则是可选的。
byteOffset 是起始位置(默认为 0)以及 length(默认是 buffer 的末尾),我们可以指定 buffer 的某些部分。
-
如果给定的是 Array,或者是其他类数组对象,则会创建一个相同长度的类型化数组,并复制其内容。
javascript1let arr = new Uint16Array([1, 2, 3, 4, 5]) 2console.log(arr.length) // 5 3console.log(arr[1]) // 2 4console.log(Array.isArray(arr)) // false 说明返回的是类型化数组 -
如果给定的是另一个类型化数组(TypedArray),会创建一个相同长度的类型化数组,并复制其内容。
javascript1let arr16 = new Uint16Array([1, 1000]) 2let arr8 = new Uint8Array(arr16) 3console.log(arr8[0]) // 1 4console.log(arr8[1]) // 232 试图复制 1000,但无法将 1000 放进 8 位字节中 -
对于数字参数 length —— 创建类型化数组可以包含那么多元素,它的字节长度是 length 乘以单个 TypedArray.BYTES_PER_ELEMENT 的字节数。
javascript1let arr = new Uint16Array(4) // 为 4 个整数创建类型化数组 2alert(Uint16Array.BYTES_PER_ELEMENT) // 每个整数 2 个字节 3alert(arr.byteLength) // 8(字节中的大小) -
不带参数的情况下,创建长度为 0 的类型化数组。
我们可以直接创建一个 TypedArray,而无需传入 ArrayBuffer。但是视图离不开底层的 ArrayBuffer,因此,除第一种情况(提供 buffer)外,其他所有情况都会自动创建 ArrayBuffer。
想要访问自动创建的 ArrayBuffer,可以用以下属性:
- arr.buffer —— 引用 ArratBuffer
- arr.byteLength —— ArrayBuffer 的字节长度
因此,我们总是可以从一个视图转换到另一个视图
javascript1let arr8 = new Uint8Array([0, 1, 2, 3]) 2 3// 同一数据的另一个视图 4let arr16 = new Uint16Array(arr8.buffer)
1.2、越界行为
当我们将越界值写入类型化数组时,多余的位会被切除。
比如,Uint8Array 的最大字节长度为 8,当我们放入 256 时,256 的二进制值为 100000000(9 位数),但是 Unit8Array 最大字节数只能放 8 位,于是仅保存最右边的 8 位,其余部分会被切除。
所以结果会是 0。
257 的二进制格式位 100000001,最右边的 8 位会被储存,因此数组中会有 1
1.3、TypedArray 方法
TypedArray 方法具有常规的 Array 方法,但是有个明显的例外
我们可以遍历(部署了 iterate 接口),map、slice、find 和 reduce 等。
但是我们没办法做:
- 没有 splice —— 因为我们没办法删除一个值,因为类型化数组时缓冲区(buffer)的视图,并且缓冲区(buffer)是固定的、连续的内存区域。我们所能做的就是分配一个零值。
- 没有 concat 方法
但拥有两种其他方法:
arr.set(fromArr,[offset])从 offset 开始。将fromArr中的所有元素复制到 arrarr.subarray([begin,end])创建一个从begin到end(不包括)相同类型的新视图。这有点类似于 slice 方法,但不复制任何内容 —— 只是创建一个新视图,以对给定片段地数据进行操作。
1.4、DataView
DataView是在ArrayBuffer上的一种特殊的超灵活未类型化的视图,它允许以任何格式访问任何偏移量(offset)的数据。
- 对于类型化的数组,构造器决定了其格式。整个数组应该是统一的,第 i 个数字是
arr[i]。 - 通过 DataView。我们可以使用
.getUint8(i)或者.getUint16(i)之类的方法访问数据。我们在调用方法时选择格式,而不是在构造的时候。
语法:
javascript1new DataView(buffer, [byteOffset], [byteLength])
- Buffer ——底层的 ArrayBuffer。与类型化数组不同的是,DataView 不会自己创建缓冲区(buffer)。我们需要实现准备好。
- byteOffset —— 视图的起始字节位置(默认为 0)
- byteLength —— 视图的字节长度(默认到 buffer 的末尾)
例如,这里我们从同一个 buffer 中提取不同格式的数字:
javascript1// 4 个字节的二进制数组,每个都是最大值 255 2let buffer = new Uint8Array([255, 255, 255, 255]).buffer 3 4let dataView = new DataView(buffer) 5 6// 在偏移量为 0 处获取 8 位数字 7alert(dataView.getUint8(0)) // 255 8 9// 现在在偏移量为 0 处获取 16 位数字,它由 2 个字节组成,一起解析为 65535 10alert(dataView.getUint16(0)) // 65535(最大的 16 位无符号整数) 11 12// 在偏移量为 0 处获取 32 位数字 13alert(dataView.getUint32(0)) // 4294967295(最大的 32 位无符号整数) 14 15dataView.setUint32(0, 0) // 将 4 个字节的数字设为 0,即将所有字节都设为 0
1.5、小结
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 和 TextEncoder
2.1、TextDecoder
如果二进制数据实际上是一个字符串,我们可以用 textDecoder 对象在给定缓冲区 buffer 和编码格式 encoding 的情况下,将值读取到实际的字符串。可以理解为这是给实际内容字符串的二进制数据解码。
语法是这样的
-
创建一个解码对象
javascript1let decoder = new TextDecoder([label], [options])label —— 编码格式,默认为
utf-8,同时也支持 big5、windows-1251 等编码格式options —— 可选对象:
- fatal —— 布尔值,如果为
true则为无效字符(不可解码)字符抛出异常,否则用字符\uFFFD替换无效字符。 - ignoreBOM —— 布尔值,如果为
true则 BOM(可选的字节顺序 unicode 标记)
- fatal —— 布尔值,如果为
-
解码
javascript1let str = decoder.decode([input], [options])- Input —— 要被解码的
BufferSource - options —— 可选对象:
- stream —— 对于解码流,为 true,则将传入的数据块(chunk)作为参数重复调用 decoder。在这种情况下,多字节的字符可能偶尔会在块与块之间分割。这个选项告诉 TextDecoder 记住未完成的字符,并在下一个数据块来的时候进行解码
- Input —— 要被解码的
以下为例子
javascript1let uint8Array = new Uint8Array([72, 101, 108, 108, 111]) 2 3alert(new TextDecoder().decode(uint8Array)) // Hello
我们可以通过为其创建子数组视图来解码部分缓冲区:
javascript1let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]) 2 3// 该字符串位于中间 4// 在不复制任何内容的前提下,创建一个新的视图 5let binaryString = uint8Array.subarray(1, -1) 6 7alert(new TextDecoder().decode(binaryString)) // Hello
2.2、TextEncoder
#TextEncoder 做相反的事情 —— 将字符串转换为字节。
语法:
javascript1let encoder = new TextEncoder()
只支持 utf-8 编码。
它有两种方法:
encode(str)—— 从字符串返回Uint8Array。encodeInto(str, destination)—— 将str编码到destination中,该目标必须为Uint8Array。
javascript1let encoder = new TextEncoder() 2 3let uint8Array = encoder.encode('Hello') 4alert(uint8Array) // 72,101,108,108,111
三、Blob
ArrayBuffer 和视图 view 都是 ECMA 标准的一部分,是 JavaScript 的一部分。
此外,还有浏览器提供的高级对象 —— Blob。
Blob 由一个可选的字符串type(MIME 类型)和blobParts组成。
blobParts 是一系列其他 Blob 对象,字符串和 BufferSource。
构造函数的语法是
javascript1new Blob(blobParts, options)
- blobParts 是 Blob/BufferSource/String 类型的值的数组
- options 可选对象:
- type —— Blob 类型 通常是 MIME 类型,例如
image/png - Ending —— 是否转换换行符,使 Blob 对应于当前操作系统的换行符(\r\n 或\n)。默认为“transparent”。
- type —— Blob 类型 通常是 MIME 类型,例如
例如:
javascript1// 从字符串创建Blob 2let blob = new Blob(['<html></html'], { type: 'text/html' }) 3// 第一个参数必须是数组
javascript1//从类型化数组(typedArray)和字符串创建blob 2let hello = new Uint8Array([72, 101, 108, 108, 111]) //二进制的“hello” 3let blob = new Blob([hello, 'world'], { 4 type: 'text/plain', 5})
我们可以用 slice 方法来提取 Blob 片段:
javascript1blob.slice([byteStart], [byteEnd], [contentType])
byteStart—— 起始字节,默认为 0。byteEnd—— 最后一个字节(专有,默认为最后)。contentType—— 新 blob 的type,默认与源 blob 相同。
Blob对象是不可改变的我们无法直接在
Blob中更改数据,但我们可以通过slice获得Blob的多个部分,从这些部分创建新的Blob对象,将它们组成新的Blob,等。这种行为类似于 JavaScript 字符串:我们无法更改字符串中的字符,但可以生成一个新的改动过的字符串。
3.1、Blob 用作 URL
Blob 可以转化成 a、img 标签的 url,来显示它们的内容。
下面是一段通过点击链接,下载一个动态生成hello world的 Blob 文件。
javascript1const a = document.createElement('a') 2a.download = 'hello.txt' 3const blob = new Blob(['hello', 'world'], { type: 'text/plain' }) 4const url = URL.createObjectURL(blob) // 从blob创建对象url 5a.href = url 6document.body.appendChild(a) 7a.click() 8URL.revokeObjectURL(url) // 从内部映射中移除引用,允许删除blob,释放内存
上面代码中,我们使用URL.createObjectURL(blob)来为 blob 生成一段对象 URL,它大概长这样
javascript1'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)释放内存。
如果代码是这样的:
html1<!-- download 特性(attribute)强制浏览器下载而不是导航 --> 2#<a download="hello.txt" href="#" id="link">Download</a> 3 4<script> 5 let blob = new Blob(['Hello, world!'], { type: 'text/plain' }) 6 7 link.href = URL.createObjectURL(blob) 8</script>
我们不能马上调用URL.revokeObjectURL(url),调用该方法时,由于映射被删除了,因此BlobURL 无效。
3.2、Blob 转换成 base64
URL.createObjectURL 的一个替代方法是,将 Blob 转换为 base64-编码的字符串
这种编码是将二进制数据表示为一个由 0-64 的 ASCII 码组成的字符串,浏览器会对其进行解码,并显示内容。
我们可以在data-url中使用 base64 编码,它的格式是这样的
data:[<mediatype>][;base64],<data>
以下的 src 是 data-url 的应用。
javascript1<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">
我们使用内置的FileReader对象来讲 Blob 转换成 base64。它可以将 Blob 数据读取成多种格式。
下面是通过 base64 的形式来完成下载的代码
javascript1const a = document.createElement('a') 2a.download = 'hello.txt' 3const blob = new Blob(['hello', 'world'], { type: 'text/plain' }) 4const reader = new FileReader() 5reader.readAsDataURL(blob) // 将blob读成data-URL 6// readAsDataURL后触发obload事件 7reader.onload = function () { 8 a.href = reader.result 9 a.click() 10} 11 12document.body.appendChild(a)
以下是两种方式创建 URL 的对比
| URL.createObjectURL(blob) | Blob 转换为 data-url |
|---|---|
| 需要用 revoke 清理内存 | 无需撤销内存(revoke) |
| 直接访问 Blob,无需编码/解码 | 对大的 Blob 进行编码/解码时,性能和内存会有耗损 |
3.4、Image 转换为 blob
我们可以创建一个 image、image 的一部分、或者创建一个页面截图的 Blob。
图像的操作使用<canvas>元素实现的:
- 使用
canvas.drawImage在canvas上绘制图像 - 调用
canvas.toBlob(callback,format,quality)创建一个 Blob,并在创建完成后调用callback
在下面的示例中,图像被复制了,我们在创建 blob 之前,从中裁剪图像,或者在 canvas 上对其进行转换:
javascript1// 获取任何图像 2let img = document.querySelector('img') 3 4// 生成同尺寸的 <canvas> 5let canvas = document.createElement('canvas') 6canvas.width = img.clientWidth 7canvas.height = img.clientHeight 8 9let context = canvas.getContext('2d') 10 11// 向其中复制图像(此方法允许剪裁图像) 12context.drawImage(img, 0, 0) 13// 我们 context.rotate(),并在 canvas 上做很多其他事情 14 15// toBlob 是异步操作,结束后会调用 callback 16canvas.toBlob(function (blob) { 17 // blob 创建完成,下载它 18 let link = document.createElement('a') 19 link.download = 'example.png' 20 21 link.href = URL.createObjectURL(blob) 22 link.click() 23 24 // 删除内部 blob 引用,这样浏览器可以从内存中将其清除 25 URL.revokeObjectURL(link.href) 26}, 'image/png') 27 28/*****或者使用promise替代回调******/ 29let blob = await new Promise((resolve, reject) => canvas.toBlob(resolve, 'image/png'))
对于页面截屏,我们可以使用html2canvas的库,它会扫描一遍浏览器页面,并将其绘制在<canvas>上。
3.5、Blob 转换为 ArrayBuffer
Blob 构造器允许所有东西构造 blob,包括 BufferSource。
如果我们需要执行低级别的操作,则可以使用FileReader从 blob 上获取最低级别的 ArrayBuffer:
javascript1// 从 blob 获取 arrayBuffer 2let fileReader = new FileReader() 3 4fileReader.readAsArrayBuffer(blob) 5 6fileReader.onload = function (event) { 7 let arrayBuffer = fileReader.result 8}
3.6、小结
ArrayBuffer、Uint8Array 及 BufferSource 是二进制的数据,而 Blob 表示有类型的二进制数据。
使用 Blob 在浏览器上能够完成非常常见的上传/下载操作。
XMLHttpRequest,fetch 等进行 Web 请求的方法可以自然地使用 Blob,也可以使用其他类型的二进制数据。
我们可以非常轻松地在 Blob 和低级别的二进制数据类型之间进行转换:
- 使用
new Blob(...)构造函数从一个类型化数组创建 Blob - 使用
FileReader从Blob中取出ArrayBuffer,然后在其上创建一个视图(view),用于低级别的二进制处理。
四、File 和 FileReader
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 对象的方式:
html1<input type="file" onchange="showFile(this)" /> 2 3<script> 4 function showFile(input) { 5 // 输入(input)可以选择多个文件,因此 input.files 是一个类数组对象。 6 // 这里我们只有一个文件,所以我们只取 input.files[0]。 7 let file = input.files[0] 8 9 alert(`File name: ${file.name}`) // 例如 my.png 10 alert(`Last modified: ${file.lastModified}`) // 例如 1552830408824 11 } 12</script>
4.1、FileReader
FileReader 是一个对象,它可以从 Blob 或者 File 对象上读取数据。
它使用事件来传递数据,因为从磁盘上读取数据可能比较费时间。
构造函数:
javascript1let 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(失败情况)
下面是读取文件的示例:
html1<input type="file" onchange="readFile(this)" /> 2 3<script> 4 function readFile(input) { 5 let file = input.files[0] 6 7 let reader = new FileReader() 8 9 reader.readAsText(file) 10 11 reader.onload = function () { 12 console.log(reader.result) 13 } 14 15 reader.onerror = function () { 16 console.log(reader.error) 17 } 18 } 19</script>
4.2、小结
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 对象。