理解JS中的二进制对象

背景

最早js是无法处理二进制的,如果非要处理,是通过charCodeAt逐个转化成Unicode编码的二进制数据,知道es5 引入blob,js才算真正可以处理二进制

blob有衍生了一系列如File对象,FileReader对象,FileList对象,URL对象;

Ajax初期只能获取文本数据,在XMLHttpRequest第二个版本也允许了服务器返回二进制数据,如果明确知道返回的二进制数据类型,返回类型就被设置为responseType:arrayBuffer;如果不知道就为blob;

canvas元素输出的二进制元素是TypedArray;

为了配合以上API,es6吧arraybuffer对象,typedarray视图和dataview视图纳入了ecmascript规范,他们都是以数组的语法处理二进制数据,统称二进制数组。

二进制数组并不是真正的数组,而是类似数组的对象。

它很像 C 语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了 JavaScript 处理二进制数据的能力,使得开发者有可能通过 JavaScript 与操作系统的原生接口进行二进制通信

es6 ArrayBuffer document

二进制数组

arrayBuffer对象

  • arrayBuffer表示一段储存了二进制数据的内存
  • 不能直接读写arrayBuffer,要用typedarray视图和dataview视图
  • 视图的作用是以指定格式解读二进制数据
  • ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const buffer = new ArrayBuffer(32);
//创建一段32字节的内存区域,每个字节默认值是0

console.log(buffer.byteLength) //32

newBuffer = buffer.slice(0, 3);
//slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。

//slice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。

//除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。

ArrayBuffer.isView(buffer) // false

const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
//ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。

typedArray视图

  • 共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等
  • TypedArray视图用来读写简单类型的二进制数据
数据类型 字节长度 含义 对应的 C 语言类型
Int8 1 8 位带符号整数 signed char
Uint8 1 8 位不带符号整数 unsigned char
Uint8C 1 8 位不带符号整数(自动过滤溢出) unsigned char
Int16 2 16 位带符号整数 short
Uint16 2 16 位不带符号整数 unsigned short
Int32 4 32 位带符号整数 int
Uint32 4 32 位不带符号的整数 unsigned int
Float32 4 32 位浮点数 float
Float64 8 64 位浮点数 double

这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,都能用方括号运算符([])获取单个元素,所有数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差异主要在以下方面。

  • TypedArray 数组的所有成员,都是同一种类型。
  • TypedArray 数组的成员是连续的,不会有空位。
  • TypedArray 数组成员的默认值为 0。比如,new Array(10)返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)返回一个 TypedArray 数组,里面 10 个成员都是 0。
  • TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。

dataView视图

  • 可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
  • DataView视图用来读写复杂类型的二进制数据

DataURL

Data URLs 由四个部分组成:前缀(data:)、指示数据类型的MIME类型、如果非文本则为可选的base64标记、数据本身:

1
2
3
4
5
6
7
8
9
10
data:[<mediatype>][;base64],<data>

data:,Hello%2C%20World!
//简单的 text/plain 类型数据
data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D
//上一条示例的 base64 编码版本
data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E
//一个HTML文档源代码 <h1>Hello, World</h1>
data:text/html,<script>alert('hi');</script>
//一个会执行 JavaScript alert 的 HTML 文档。注意 script 标签必须封闭。

mediatype 是个 MIME 类型的字符串,例如 "image/jpeg" 表示 JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII

如果数据是文本类型,你可以直接将文本嵌入 (根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行base64编码之后再进行嵌入。

base64

上面提到的 base64 不算是一种加密算法,它只是简单地将每 3 个 8bit 字符转换为 4 个 6Bit 字符(base64 只有 2^6 = 64 种字符,因此得名),这样保证了传输中必定使用 ASCII 中可见字符,不会出奇怪的空白字符或是功能性标志 。

由于是 3 个字符变 4 个,那么很明显了,base64 编码后,编码对象的体积会变成原来的 4/3 倍

特别要注意的是如果 bit 数不能被 3 整除,需要在末尾添加 1 或 2 个 byte(8 或 16bit),并且末尾的 0 不使用 A 而使用 =,这就是为什么 base64 有的编码结果后面会有一或两个等号。

btoa

WindowOrWorkerGlobalScope.btoa()String 对象中创建一个 base-64 编码的 ASCII 字符串,其中字符串中的每个字符都被视为一个二进制数据字节。

1
2
let encodedData = window.btoa("Hello, world"); // 编码
let decodedData = window.atob(encodedData);    // 解码

unicode 字符串

在多数浏览器中,使用 btoa() 对 Unicode 字符串进行编码都会触发 InvalidCharacterError 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ucs-2 string to base64 encoded ascii
function utoa(str) {
    return window.btoa(unescape(encodeURIComponent(str)));
}
// base64 encoded ascii to ucs-2 string
function atou(str) {
    return decodeURIComponent(escape(window.atob(str)));
}
// Usage:
utoa('✓ à la mode'); // 4pyTIMOgIGxhIG1vZGU=
atou('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"

utoa('I \u2661 Unicode!'); // SSDimaEgVW5pY29kZSE=
atou('SSDimaEgVW5pY29kZSE='); // "I ♡ Unicode!"

blob

  • 二进制大对象,并非前端特有,计算机通用术语
  • 一个Blob对象就是一个包含有只读原始数据的类文件对象
  • 一个不可修改的二进制文件

二进制数据关联

概述

  • Blob、ArrayBuffer、File可以归为一类,它们都是数据;
  • FileReader算是一种工具,用来读取数据;
  • FormData可以看做是一个应用数据的场景。

blob和ArrayBuffer

  • 相同点: Blob和ArrayBuffer都是二进制的容器;
  • ArrayBuffer:ArrayBuffer更底层,就是一段纯粹的内存上的二进制数据,我们可以对其任何一个字节进行单独的修改,也可以根据我们的需要以我们指定的形式读取指定范围的数据
  • Blob:Blob就是将一段二进制数据做了一个封装,我们拿到的就是一个整体,可以看到它的整体属性大小、类型;可以对其分割,但不能了解到它的细节
  • 联系:Blob可以接受一个ArrayBuffer作为参数生成一个Blob对象,此行为就相当于对ArrayBuffer数据做一个封装,之后就是以整体的形式展现了
  • 应用上的区别:由于ArrayBuffer和Blob的特性,Blob作为一个整体文件,适合用于传输;而只有需要关注细节(比如要修改某一段数据时),才需要用到ArrayBuffer

Blob与File

  • file是二进制文件,是blob的一个小类,blob的所有属性和方法都可以用于file

数据转化

ArrayBuffer to DataUrl(base64)

  • 利用blob作为媒介
  • buffer->blob->filereader->dataurl
1
2
3
4
5
6
7
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]);       // 注意必须包裹[]

var a = new FileReader();
a.onload = function(e) {callback(e.target.result);};
a.readAsDataURL(blob);

File/Blob to DataUrl(base64)

  • 利用filereader的readAsDataURL
1
2
3
4
5
6
7
8
9
10
11
12
function readBlobAsDataURL(blob, callback) {
    var a = new FileReader();
    a.onload = function(e) {callback(e.target.result);};
    a.readAsDataURL(blob);
}
//example:
readBlobAsDataURL(blob, function (dataurl){
    console.log(dataurl);
});
readBlobAsDataURL(file, function (dataurl){
    console.log(dataurl);
});

Blob to ArrayBuffer

  • 利用filereader的readAsArrayBuffer
1
2
3
4
5
6
var blob = new Blob([data], {type: text/plain});
var reader = new FileReader();
reader.onload = function(e) {
    callback(e.target.result);
}; 
reader.readAsArrayBuffer(blob);

ArrayBuffer to Blob

  • arraybuffer可以作为New Blob的入参
1
2
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]);       // 注意必须包裹[]

ArrayBuffer to Uint8

Uint8数组可以直观的看到ArrayBuffer中每个字节(1字节 == 8位)的值。一般我们要将ArrayBuffer转成Uint类型数组后才能对其中的字节进行存取操作。

1
2
var buffer = new ArrayBuffer(32);
var u8 = new Uint8Array(buffer);

Uint8 to ArrayBuffer

我们Uint8数组可以直观的看到ArrayBuffer中每个字节(1字节 == 8位)的值。一般我们要将ArrayBuffer转成Uint类型数组后才能对其中的字节进行存取操作。

1
2
var uint8 = new Uint8Array();
var buffer = uint8.buffer; 

Array to ArrayBuffer

1
2
3
var arr = [0x15,0xFF,0x01,0x00,0x34,0xAB,0x11];
var uint8 = new Uint8Array(arr);
var buffer = uint8.buffer;

TypeArray to Array

1
2
3
4
5
6
7
function Uint8Array2Array(u8a) {
    var arr = [];
    for (var i = 0; i < u8a.length; i++) {
        arr.push(u8a[i]);
    }
    return arr;
}

Blob to File

  • 对blob添加name等参数就可以变成file
1
2
3
4
5
6
//将blob转换为file
blobtoFile:function(theBlob,filename){
   theBlob.lastModifiedDate = new Date();
   theBlob.name = fileName;
   return theBlob;
}

base64(dataurl) to Blob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//将base64转换为blob
//dataurl = "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ"
dataURLtoBlob: function (dataurl) {
  var arr = dataurl.split(','),
      // ["data:text/plain;base64", "SGVsbG8sIFdvcmxkIQ"]
      mime = arr[0].match(/:(.*?);/)[1],
      //[":text/plain;", "text/plain", index: 4, input: "data:text/plain;base64", groups: undefined]
      bstr = atob(arr[1]),
      //atob(arr[1]) = "Hello, World!"
      n = bstr.length,
      u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  //u8arr : Uint8Array(13) [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]
  return new Blob([u8arr], {type: mime});
  //Blob {size: 13, type: "text/plain"}
},

string to blob

1
2
3
4
//将字符串转换成 Blob对象
var blob = new Blob(['中文字符串'], {
    type: 'text/plain'
});

blob to string

1
2
3
4
5
6
//将Blob 对象转换成字符串
var reader = new FileReader();
reader.readAsText(blob, 'utf-8');
reader.onload = function (e) {
    console.info(reader.result);
}

使用场景

生成的blob下载到本地

原理就是利用Blob对象把需要下载的内容转换为二进制,然后借助``标签的href属性和download属性,实现下载。

1
2
3
4
5
6
7
8
9
10
11
12
function saveShareContent (content, fileName) {
    let downLink = document.createElement('a')
    downLink.download = fileName
    //字符内容转换为blod地址
    let blob = new Blob([content])
    downLink.href = URL.createObjectURL(blob)
    // 链接插入到页面
    document.body.appendChild(downLink)
    downLink.click()
    // 移除下载链接
    document.body.removeChild(downLink)
}

dataURL图片数据绘制到canvas

  • 先构造Image对象,src为dataURL,图片onload之后绘制到canvas
1
2
3
4
5
var img = new Image();
img.onload = function(){
    canvas.drawImage(img);
};
img.src = dataurl;

File,Blob的图片文件数据绘制到canvas

  • 还是先转换成一个url,然后构造Image对象,src为dataURL,图片onload之后绘制到canvas
  • 利用上面的 readBlobAsDataURL 函数,由File,Blob对象得到dataURL格式的url,再参考 dataURL图片数据绘制到canvas
1
2
3
4
5
6
7
8
9
10
11
12
13
function readBlobAsDataURL(blob, callback) {
    var a = new FileReader();
    a.onload = function(e) {callback(e.target.result);};
    a.readAsDataURL(blob);
}

readBlobAsDataURL(file, function (dataurl){
    var img = new Image();
    img.onload = function(){
        canvas.drawImage(img);
    };
    img.src = dataurl;
});

Canvas转换为Blob对象并使用Ajax发送

  • 转换为Blob对象后,可以使用Ajax上传图像文件。
  • 先从canvas获取dataurl, 再将dataurl转换为Blob对象
1
2
3
4
5
6
7
8
var dataurl = canvas.toDataURL(image/png);
var blob = dataURLtoBlob(dataurl);
//使用ajax发送
var fd = new FormData();
fd.append("image", blob, "image.png");
var xhr = new XMLHttpRequest();
xhr.open(POST, /server, true);
xhr.send(fd);

总结

  1. blob作为二进制容器,可以利用fileReader来吧blob文件转化为arrayBuffer,dataurl,string等格式内容,如果要编辑blob文件,可以转化为arraybuffer,通过视图操作然后在转化回来在进行逆向操作
  2. 基于blob,利用canvas可以实现一系列如图片压缩,图片编辑等前端功能