Skip to content
本页目录

Excerpt

javascript二进制整理和二进制简单应用,主要介绍了ArrayBuffer、Blob及它们的转换等...


ArrayBuffer

**ArrayBuffer**对象用来表示通用的、固定长度的原始二进制数据缓冲区,同时它的内容是只读的。但可以通过视图(view)对它进行读写操作。

先创建一个实例看看:

javascript
let arrayBuffer = new ArrayBuffer(32)
console.log(arrayBuffer.byteLength) // 32
// 创建一个可以容纳32字节的区域,并且这片区域会用0预填充

arrayBuffer.png

可以看到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位有符号整型
  • ...

举几个例子说明一下:

javascript
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为例,看看它还可以怎么初始化

javascript
new Uint8Array(3)
new Uint8Array(typedArray)
new Uint8Array(buffer [, byteOffset [, length]])
// 其中byteOffset和length是可选的

试试传入一个ArrayBuffer,并且修改数据

javascript
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


从上面的代码中可以看出:

  1. 视图的确可以对arrayBuffer进行读写
  2. 不同视图对同一arrayBuffer的操作都是映射在arrayBuffer本身的
  3. 不同视图的‘读取方式’是不一样的,Uint8Array是8位一读,Uint16Array是16位一读。
  4. 视图的buffer属性指向就是ArrayBuffer本身

不同读取方式.png

可能有眼尖的同学一眼就看出上面Uint16Array的二进制转换有误,但其实这是正确的,不过是因为环境默认采用的是小端字节序(Little Endian),以【00100000 00000000】为例,按照小端字节序读法,应该是先读后面的字节,所以应当读作二进制0b0000000000100000,也就是十进制32;而最后的4相当于是左移了8位,即4<<8 === 1024

简单的总结大小端存储方式:

  • 大端字节序 Big Endian 是指低地址端 存放 高位字节。
  • 小端字节序 Little Endian 是指低地址端 存放 低位字节。

附上检测方法:

javascript
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来创建一个视图:

javascript
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']


视图现在有了,我们如何去读写数据呢?

javascript
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

javascript
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);


9.png 那如何获得一个File对象呢?

可以通过input标签上传本地文件获取一个file对象:

上传图片File对象.png

可以看到,blob实例属性很简单,只有size和type,而由于File继承自Blob,因此也有这两个属性。

  • size(只读):表示Blob对象中所包含数据的字节大小

  • type(只读):表明Blob对象所包含数据的 MIME 类型。如果类型未知,则该值为空字符串。

  • name: file文件名

  • lastModified: file最后被修改的时间戳

有了Blob对象,我们如何读取它呢?

FireReader

FileReader 对象允许 Web 应用程序_异步_读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用FileBlob对象指定要读取的文件或数据。

FileReader有以下方法,可以把Blob转化为其它数据

  • FileReader.prototype.readAsArrayBuffer
  • FileReader.prototype.readAsText
  • FileReader.prototype.readAsDataURL
  • ~FileReader.prototype.readAsBinaryString(已废弃)~

见名知意,这几个方法分别是把blob读成ArrayBuffer,Text,DataURL,BinaryString。举个🌰

javascript
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)
}

10.png

由于平时接触的二进制数据处理场景并不多,所以说的还是云里雾里的,那么就用一个实际应用场景练练手:

二进制小demo :图片上传

先来实现一个上传后预览功能

javascript
<!-- 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)
  })
}


12.png

实现大小和格式校验:

javascript
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
  }
}


原始格式.png 通常,我们校验文件格式是通过文件扩展名来判断的,如上面的图。通过扩展名'.jpeg'可以认为它就是jpeg图片格式。这种方式简单粗暴,能应付大多数场景,但是它并不准确。

举个🌰:我把上面这个文件扩展名篡改一下,那就不能正确判断了。

篡改扩展名后.png

通过匹配MagicNumber判断:

javascript
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);
  }
}

对比扩展名.png 可以看到,File读取到的格式是我们篡改的扩展名.png,而通过对它的文件头分析,实际上它是一个.jpeg图片。

实现图片压缩功能:

javascript
// 用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了。因此它文件大小会少很多,同时也会糊很多,可以当做缩略图。

canvas压缩.png

附上完整代码:

html
<!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很大的情况下内存也放不下,只能通过流处理。

最后用一张图来表达: 转换图.png