Excerpt
javascript二进制整理和二进制简单应用,主要介绍了ArrayBuffer、Blob及它们的转换等...
ArrayBuffer
**ArrayBuffer
**对象用来表示通用的、固定长度的原始二进制数据缓冲区,同时它的内容是只读的。但可以通过视图(view)对它进行读写操作。
先创建一个实例看看:
let arrayBuffer = new ArrayBuffer(32)
console.log(arrayBuffer.byteLength) // 32
// 创建一个可以容纳32字节的区域,并且这片区域会用0预填充
可以看到ArrayBuffer实例有一个byteLength属性,并且就是我们创建时传入的字节数。这个实例身上有一些奇怪的东西【Int8Array、Uint8Array、Int16Array、Int32Array】,这些就是接下来要说的第一类视图TypedArray
了
TypedArray
TypedArray
是 ES6+ 新增的描述二进制数据的类数组数据结构。但它本身不可以被实例化,也无法访问,你可以把它理解为一个Abstract Class
。该对象描述了一个底层的二进制数据缓冲区(binary data buffer)的一个类数组视图(view)。
关于 TypedArray 的具体实现,它存在以下方式(只展示部分):
Int8Array
8位有符号整型Uint8Array
8位无符号整型Int16Array
16位有符号整型Int32Array
32位有符号整型- ...
举几个例子说明一下:
let int8 = new Int8Array([-1,2,-3])
console.log(int8.length,int8.byteLength) // 3 3
// Int8Array代表每次读写的单位数据都占8个比特位,由于带符号,最高位被符号占用,应此可表示从10000000 ~ 01111111,即十进制-128~127。
//此时内存空间是 10000001, 00000010, 10000011` 占用3个字节,共24位
let uint8 = new Uint8Array([1,2,3])
console.log(uint8.length,uint8.byteLength) // 3 3
// 和上面的区别是,每一项是无符号的,可表示从00000000 ~ 11111111。
//内存空间是 00000001, 00000010, 00000011 占用3个字节,共24位
let int16 = new Int16Array([1,2,3])
console.log(int16.length,int16.byteLength) // 3 6
// 代表每次读写的最小单位为16个比特位,即两个字节
// 此时内存空间是 00000000 00000001, 00000000 00000010, 00000000 00000011 占用6个字节,共48位
let int32 = new Int32Array([1,2,3])
console.log(int16.length,int16.byteLength) // 3 12
// 代表每次读写的单位数据都占32个比特位,即三个字节
// 此时内存空间是 00000000 00000000 00000000 00000001, 00000000 00000000 00000000 00000010, 00000000 00000000 00000000 00000011 占用12个字节,共96位
除了上面这种传一个数组对象,我们这里以UInt8Array
为例,看看它还可以怎么初始化
new Uint8Array(3)
new Uint8Array(typedArray)
new Uint8Array(buffer [, byteOffset [, length]])
// 其中byteOffset和length是可选的
试试传入一个ArrayBuffer,并且修改数据
let buffer = new ArrayBuffer(8)
let uint8 = new Uint8Array(buffer)
console.log(uint8)
// Uint8Array(8)[0, 0, 0, 0, 0, 0, 0, 0, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 8, Symbol(Symbol.toStringTag): 'Uint8Array']
uint8[2] = 8, uint8[7] = 4
console.log(uint8)
// Uint8Array(8)[0, 0, 8, 0, 0, 0, 0, 4, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 8, Symbol(Symbol.toStringTag): 'Uint8Array']
let uint16 = new Uint16Array(buffer)
console.log(uint16)
// Uint16Array(4)[0, 8, 0, 1024, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 4, Symbol(Symbol.toStringTag): 'Uint16Array']
uint16[0] = 32,uint16[2] = 128
// Uint8Array(8)[32, 0, 8, 0, 128, 0, 0, 4, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 8, Symbol(Symbol.toStringTag): 'Uint8Array']
uint8.buffer === buffer // true
uint16.buffer === buffer //true
uint8.buffer === uint16.buffer //true
从上面的代码中可以看出:
- 视图的确可以对arrayBuffer进行读写
- 不同视图对同一arrayBuffer的操作都是映射在arrayBuffer本身的
- 不同视图的‘读取方式’是不一样的,Uint8Array是8位一读,Uint16Array是16位一读。
- 视图的buffer属性指向就是ArrayBuffer本身
可能有眼尖的同学一眼就看出上面Uint16Array的二进制转换有误
,但其实这是正确的,不过是因为环境默认采用的是小端字节序(Little Endian),以【00100000 00000000】为例,按照小端字节序读法,应该是先读后面的字节,所以应当读作二进制0b0000000000100000,也就是十进制32;而最后的4相当于是左移了8位,即4<<8 === 1024
简单的总结大小端存储方式:
- 大端字节序 Big Endian 是指低地址端 存放 高位字节。
- 小端字节序 Little Endian 是指低地址端 存放 低位字节。
附上检测方法:
function checkEndian() {
var arrayBuffer = new ArrayBuffer(2);
var uint8Array = new Uint8Array(arrayBuffer);
var uint16array = new Uint16Array(arrayBuffer);
uint8Array[0] = 0xAA;
uint8Array[1] = 0xBB;
if(uint16array[0] === 0xBBAA) return"little endian";
if(uint16array[0] === 0xAABB) return"big endian";
else throw new Error("Something crazy just happened");
}
DataView
**DataView
**视图是一个可以从二进制ArrayBuffer
对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的(endianness)问题。 让我们用DataView来创建一个视图:
let arrayBuffer = new ArrayBuffer(8);
let view = new DataView(arrayBuffer);
// 创建一个视图,从第一个字节开始,直到最后
let view1 = new DataView(arrayBuffer, 2)
// 此DataView对象从arrayBuffer第3个字节开始,直到最后
let view2 = new DataView(arrayBuffer, 2, 4)
// 此DataView对象从arrayBuffer第3个字节开始且长度为4
//类似 splice()这两种传参
['a','b','c','d','e','f','g','h'].splice(2)
// ['c', 'd', 'e', 'f', 'g', 'h']
['a','b','c','d','e','f','g','h'].splice(2,4)
// ['c', 'd', 'e', 'f']
视图现在有了,我们如何去读写数据呢?
let arrayBuffer = new ArrayBuffer(8);
let view = new DataView(arrayBuffer);
view.setUint8(0,135)
// 在第1个字节位置向视图写入8位无符号整数
view.setUint16(2,522)
// 在第3个字节位置向视图写入16位无符号整数
let view1 = view.getUint8(0); // 135
// 从第1个字节读取一个8位无符号整数
let view2 = view.getUint16(2); // 522
// 从第3个字节读取一个16位无符号整数
let view3 = view.getUint8(1); // 0
// 从第2个字节读取一个8位无符号整数
let view4 = view.getUint16(1); // 2
从上面的例子上可以看出,用DataView读写arrayBuffer数据不限制字节数和位置,比较灵活
Blob和File
Blob
(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。
File
对象继承自Blob
对象,是特殊类型的Blob
,且可以用在任意的 Blob 类型的 context 中。
在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据 ,是JavaScript对不可修改二进制数据的封装类型。包含字符串的数组、ArrayBuffers、ArrayBufferViews,甚至其他 Blob 都可以用来创建blob
let arr = ['abc','xyz'];
let arrayBuffer = new ArrayBuffer(8);
let uint8 = new Uint8Array([9,5,2,7]);
let dataView = new DataView(arrayBuffer);
let blob0 = new Blob([...arr,arrayBuffer,uint8,dataView]);
let blob1 = new Blob([blob0]);
console.log(blob0);
console.log(blob1);
那如何获得一个File对象呢?
可以通过input标签上传本地文件获取一个file对象:
可以看到,blob实例属性很简单,只有size和type,而由于File继承自Blob,因此也有这两个属性。
size(只读):表示
Blob
对象中所包含数据的字节大小type(只读):表明
Blob
对象所包含数据的 MIME 类型。如果类型未知,则该值为空字符串。name: file文件名
lastModified: file最后被修改的时间戳
有了Blob对象,我们如何读取它呢?
FireReader
FileReader
对象允许 Web 应用程序_异步_读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用File
或Blob
对象指定要读取的文件或数据。
FileReader
有以下方法,可以把Blob
转化为其它数据
- FileReader.prototype.readAsArrayBuffer
- FileReader.prototype.readAsText
- FileReader.prototype.readAsDataURL
- ~FileReader.prototype.readAsBinaryString(已废弃)~
见名知意,这几个方法分别是把blob读成ArrayBuffer,Text,DataURL,BinaryString。举个🌰
let str = '我是一只小鸭子,咿呀咿呀哟';
let blob = new Blob([str]);
//
let reader1 = new FileReader()
reader1.readAsArrayBuffer(blob)
reader1.onload = function(){
console.log(reader1.result)
}
//
let reader2 = new FileReader()
reader2.readAsText(blob)
reader2.onload = function(){
console.log(reader2.result)
}
//
let reader3 = new FileReader()
reader3.readAsDataURL(blob)
reader3.onload = function(){
console.log(reader3.result)
}
//
let reader4 = new FileReader()
reader4.readAsBinaryString(blob)
reader4.onload = function(){
console.log(reader4.result)
}
由于平时接触的二进制数据处理场景并不多,所以说的还是云里雾里的,那么就用一个实际应用场景练练手:
二进制小demo :图片上传
先来实现一个上传后预览功能
<!-- html -->
<img id="pic1" />
<img id="pic2" />
<img id="pic3" />
<input type="file" id="fileUpload">
<!-- javascript -->
let el = document.getElementById('fileUpload')
let pic = document.getElementById('pic1')
let pic2 = document.getElementById('pic2')
let pic3 = document.getElementById('pic3')
el.onchange= function onload(e){
let file = e.target.files[0]
// 方法一: blob直接转成Object URL引入
let url = window.URL.createObjectURL(file)
pic1.src = url
// 方法二: blob用FileReader转成Base64引入
let reader=new FileReader();
reader.onload=function(e) {
pic2.src = reader.result
console.log(reader.result)
}
reader.readAsDataURL(file);
// 方法三: blob转成ArrayBuffer再转成Base64
file.arrayBuffer().then(buffer => {
let bytes = new Uint8Array(buffer);
let binary = ''
for(let len = bytes.byteLength, i = 0;i < len;i++) {
binary += String.fromCharCode(bytes[i])
}
pic3.src = 'data:image/png;base64,' + window.btoa(binary)
})
}
实现大小和格式校验:
function fileVerification(file){
console.log(file) // 85102
// 限制文件上传大小 2MB ,size 的单位是 B 字节
if(file.size > 2*1024*1024) {
alert('文件大小不得超过2MB!')
return false
}
// 限制类型
const types = ['jpg','jpeg','png','gif'] // 允许上传的类型
const fileType = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase()
console.log('文件扩展名:',fileType)
if(!types.includes(fileType)){
alert('不支持的格式')
return false
}
}
通常,我们校验文件格式是通过文件扩展名来判断的,如上面的图。通过扩展名'.jpeg'可以认为它就是jpeg图片格式。这种方式简单粗暴,能应付大多数场景,但是它并不准确。
举个🌰:我把上面这个文件扩展名篡改一下,那就不能正确判断了。
通过匹配MagicNumber判断:
function fileVerification(file){
// 通过MagicNumber校验
let fileHead = file.slice(0,4) // 截取文件头
let reader = new FileReader()
reader.readAsArrayBuffer(fileHead)
reader.onload = function (e) {
let buffer = reader.result
let view = new DataView(buffer)
// 将这四个字节的内容,视作一个32位整数
let magicNumber = view.getUint32(0, false)
console.log('magicNumber',magicNumber)
// 根据文件的前四个字节,判断它的类型
switch(magicNumber) {
case 0x89504E47: file.verified_type = 'image/png'; break;
case 0x47494638: file.verified_type = 'image/gif'; break;
case 0xFFD8FFe0: file.verified_type = 'image/jpeg'; break;
case 0xFFD8FFe1: file.verified_type = 'image/jpg'; break;
}
console.log('文件名:',file.name, '图片真实格式:',file.verified_type);
}
}
可以看到,File读取到的格式是我们篡改的扩展名.png
,而通过对它的文件头分析,实际上它是一个.jpeg
图片。
实现图片压缩功能:
// 用canvas实现压缩
function compress (file,rate,type,resolve) {
let image = new Image()
image.src = window.URL.createObjectURL(file)
image.onload = () => { // 图片加载完成后才能进行压缩处理,从而转换为base64 进行赋值
let ctx1, ctx2,img64, img64Resize;
// 保持大小不变的前提下减少文件大小
let canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
ctx1 = canvas.getContext("2d");
ctx1.drawImage(image, 0, 0, image.width, image.height)
img64 = canvas.toDataURL("image/jpeg", rate)
// 缩略图 固定50*50
let canvas1 = document.createElement('canvas')
canvas1.width = 50
canvas1.height = 50
ctx2 = canvas1.getContext("2d");
ctx2.drawImage(image, 0, 0, 50, 50);
img64Resize = canvas1.toDataURL("image/jpeg", rate)
resolve([new File([dataUrl2file(img64)], file.name, {type}),
new File([dataUrl2file(img64Resize)], file.name, {type})])
}
}
// dataUrl 转 file
function dataUrl2file(dataUrl){
let arr = dataUrl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return u8arr
}
这里三张图片第一张未压缩,第二张图未改变其原始宽高的情况下进行压缩,而最后的图固定尺寸为50* 50,但是把它渲染成200* 200了。因此它文件大小会少很多,同时也会糊很多,可以当做缩略图。
附上完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
</head>
<body>
<!-- html -->
<img id="pic1" width="200px" />
<img id="pic2" width="200px" />
<img id="pic3" width="200px" />
<input type="file" id="fileUpload">
<script>
let el = document.getElementById('fileUpload')
let pic = document.getElementById('pic1')
let pic2 = document.getElementById('pic2')
let pic3 = document.getElementById('pic3')
el.onchange= function onload(e){
let file = e.target.files[0]
new Promise(resolve=>{
compress(file,resolve) //图片压缩
}).then(newFile=>{
console.log('压缩前:',file,'压缩后:',newFile)
fileVerification(newFile) // 图片验证
preview(file,newFile) // 预览
})
}
function compress (file,resolve) {
let image = new Image()
image.src = window.URL.createObjectURL(file)
image.onload = () => { // 图片加载完成后才能进行压缩处理,从而转换为base64 进行赋值
let canvas, ctx, img64;
canvas = document.createElement('canvas')
// 保持大小不变的前提下减少文件大小
canvas.width = image.width
canvas.height = image.height
ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0, image.width, image.height);
img64 = canvas.toDataURL("image/jpeg", 0.4) // 压缩率 0.4
// dataUrl 转 file
let arr = img64.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
resolve(new File([u8arr], file.name, {type:mime}))
}
}
function preview (file,newFile){
// 方法一: 直接转成Object URL引入
let url = window.URL.createObjectURL(file)
pic1.src = url
// 方法二: 用FileReader转成Base64引入
let reader=new FileReader();
reader.onload=function(e) {
pic2.src = reader.result
}
reader.readAsDataURL(newFile);
// 方法三:转成ArrayBuffer再转成Base64
file.arrayBuffer().then(buffer => {
let bytes = new Uint8Array(buffer)
let url,binary = ''
for (let len = bytes.byteLength, i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
pic3.src = url = 'data:image/png;base64,' + window.btoa(binary)
})
}
function fileVerification(file){
// 限制文件上传大小 2MB ,size 的单位是 B 字节
if(file.size > 2*1024*1024) {
alert('文件大小不得超过2MB!')
return false
}
// 简单校验 (对文件扩展名进行匹配校验,如果扩展名被篡改,会误判)
const types = ['jpg','jpeg','png','gif'] // 允许上传的类型
const fileType = file.name.substring(file.name.lastIndexOf('.') + 1).toLowerCase()
console.log('文件扩展名:',fileType)
if(!types.includes(fileType)){
alert('不支持的格式')
return false
}
// 通过MagicNumber校验
let fileHead = file.slice(0,4) // 截取文件头
let reader = new FileReader()
reader.readAsArrayBuffer(fileHead)
reader.onload = function (e) {
let buffer = reader.result
// 将这四个字节的内容,视作一个32位整数
let view = new DataView(buffer)
let magicNumber = view.getUint32(0, false)
console.log('magicNumber',magicNumber)
// 根据文件的前四个字节,判断它的类型
switch(magicNumber) {
case 0x89504E47: file.verified_type = 'image/png'; break;
case 0x47494638: file.verified_type = 'image/gif'; break;
case 0xFFD8FFe0: file.verified_type = 'image/jpeg'; break;
case 0xFFD8FFe1: file.verified_type = 'image/jpg'; break;
}
console.log('文件名:',file.name, '图片真实格式:',file.verified_type);
}
}
</script>
</body>
</html>
说了这么多,可能都有些混乱了,再来捋一捋我们的两个主角,Blob和ArrayBuffer:
ArrayBuffer表达的是一片可编辑的区域,通常是在内存中的,里面的数据是可读可写(需要用到view)的,我们用到它一般都是因为需要去操作它。 Blob则表示的是一个做为一个整体的文件,通常是在磁盘上的。类似于字符串那样是不可变的,更多的目的是直接使用或者通过转换间接使用它,而不是去修改其内容。
借贺佬的话来描述一下底层的区别。
ArrayBuffer其实就是一块连续内存,所以是low-level的。你可以将这块内存映射为某种数组(TypedArray)或者是自定义的数据视图(DataView)
Blob(binary large object)则是一个相对high-level的概念,来自于数据库,可以认为就是「文件」(所以blob是有文件类型的,即mime type),只不过是脱离具体文件系统的文件(不需要有文件名、文件路径之类的东西)。 Blob对象并不对应内存,一个blob引用更像文件句柄,你读取blob的内容,可以是全放进一个ArrayBuffer里,也可以直接得到一个字符串(如果是文本文件),还可以通过Stream来读取,特别是blob很大的情况下内存也放不下,只能通过流处理。
最后用一张图来表达: