先记住这个答案
这四个类型是一条从文件到字节的链。File 继承自 Blob,是 Blob 加上文件名 name 和修改时间 lastModified,所以任何接收 Blob 的接口也能接收 File。Blob 表示一块不可修改的原始数据,有大小 size 和 MIME 类型 type,可以切片、上传、生成下载链接,但不能直接读里面的字节。要读字节,就用 blob.arrayBuffer() 转成 ArrayBuffer,它是一段可读写的内存。ArrayBuffer 自己也不能按下标访问,要通过 Uint8Array、Float32Array 这类类型化数组,或者 DataView 去读写,它们决定了把这些字节当成什么类型来解读。
File继承Blob,能用Blob的所有方法Blob不可修改,ArrayBuffer是可读写的内存ArrayBuffer不能直接读写,要通过视图访问- 同一个
ArrayBuffer上的多个视图共享内存
读取文件头:四层一起用
用户用 <input type="file"> 选了一个文件,想读前 4 个字节判断它是不是 PNG。
File 一层层拿到字节JavaScriptasync function readSignature(file) {
console.log(file.name, file.type, file.size) // File 的文件信息
const header = file.slice(0, 4) // 继承自 Blob 的方法,返回 Blob
const buffer = await header.arrayBuffer() // 转成可读写的内存
const bytes = new Uint8Array(buffer) // 按一个字节一个元素来读
return [...bytes].map(b => b.toString(16).padStart(2, '0')).join(' ')
}
// 传入一个 PNG 文件,返回 '89 50 4e 47'每一层干一件事。file.slice 是 File 从 Blob 继承来的方法,切出前 4 个字节得到一个新的 Blob,不会把整个文件读进内存。arrayBuffer() 把这 4 个字节读成内存。Uint8Array 把内存按无符号 8 位整数来解读,每个元素正好是一个字节。
注意 file.type 是浏览器根据扩展名猜出来的,改个扩展名就变了,不能用来做安全校验。读文件头也只适合给用户提示,服务端要重新检查。
ArrayBuffer 和类型化数组的关系
ArrayBuffer 只是一段字节,它不知道这些字节代表图片、音频还是数字。类型化数组是在它上面加一层「怎么读」的规则:Uint8Array 每个元素占 1 个字节,Int32Array 每 4 个字节当一个整数,Float32Array 每 4 个字节当一个浮点数。
- 多个视图共享同一块内存
- 同一个
ArrayBuffer上可以同时创建多个视图。比如new Uint8Array(buffer)和new Uint32Array(buffer)指向同一块内存,通过一个视图改了字节,另一个视图读到的值也会变。视图本身不存数据,要复制数据得调用slice()。 还可以只看其中一段:new Uint8Array(buffer, 4, 8)从第 4 个字节开始看 8 个字节,常用于解析文件头、网络协议这类有固定结构的数据。
容易答错的地方
- 认为
File和Blob是两个不相关的类型 File就是一种Blob,所以file instanceof Blob为true。FormData.append、URL.createObjectURL、fetch的请求体这些接收Blob的地方,都可以直接传File。- 以为改了
ArrayBuffer之后Blob也会变 blob.arrayBuffer()读出的是一份新内存,和Blob没有关联。反过来用new Blob([buffer])创建Blob时,数据也是在那一刻复制进去的,之后改buffer不会影响这个Blob。
面试官还会怎么问?
blob.slice() 会复制数据吗?
规范只规定它返回一个包含指定范围的新 Blob,没规定要不要物理复制。浏览器通常不会马上复制,而是记录范围,共享底层数据。因为 Blob 不可修改,共享也是安全的,调用方不需要关心。
怎样把 ArrayBuffer 变回 Blob 或 File?
用 new Blob([buffer], { type: 'image/png' }),要文件名就用 new File([buffer], 'a.png', { type: 'image/png' })。type 要自己填对,浏览器不会根据内容推断。
ArrayBuffer 和 Node.js 的 Buffer 是什么关系?
Node.js 的 Buffer 是 Uint8Array 的子类,底层也是 ArrayBuffer。所以 Buffer 能用类型化数组的方法,也可以通过 buf.buffer 拿到底层的 ArrayBuffer,但要注意 byteOffset,小的 Buffer 可能共享一个更大的内存池。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。