目前在一个项目中,WebSocket部分由于后端使用了gzip压缩,前端处理起来废了一点时间,从而发现自己在二进制数据类型这个知识点还存在一定的盲区,因此这里进行总结。
本文主要简单介绍ArrayBuffer对象、TypedArray对象、DataView对象以及Blob原始数据类型,和它们之间的互相转换方法。部分代码参考这里而非本人原创,仅做个人学习使用。
这些类型化对象,一般会在以下场景中使用:
- WebGL 中,浏览器和显卡之间需要使用二进制数据进行通信。
- 在一些 Rest 接口或者 WebSocket 中,采用压缩过的数据进行通信,这个压缩和解压缩的过程可能需要借助二进制对象。
- 在 Canvas 中,我们可能需要通过生成 Blob 的方式保存当前内容。
- 在 Img 等资源文件中,URL 可以为 Blob 原始数据类型。
- 在读取用户上传文件时,可能需要用到二进制数据类型进行中间转换。
下文分两部分,前一部分概述各个二进制数据类型,后一部分将它们之间的互相转换。
二进制数据类型概述
ArrayBuffer
ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。
ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
1 | var buf = new ArrayBuffer(32); |
上面代码生成了一段32字节的内存区域,每个字节的值默认都是0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。
为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。
1 | var buf = new ArrayBuffer(32); |
上面代码对一段32字节的内存,建立DataView视图,然后以不带符号的8位整数格式,读取第一个元素,结果得到0,因为原始内存的ArrayBuffer对象,默认所有位都是0。
另外,我们可以将ArrayBuffer生成的结果,传入TypedArray中:
1 | var buffer = new ArrayBuffer(12); |
ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。
1 | var buffer = new ArrayBuffer(32); |
如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。
1 | if (buffer.byteLength === n) { |
ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。
1 | var buffer = new ArrayBuffer(8); |
上面代码拷贝buffer对象的前3个字节(从0开始,到第3个字节前面结束),生成一个新的ArrayBuffer对象。slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。
slice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。
除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。
ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。
1 | var buffer = new ArrayBuffer(8); |
TypedArray
目前,TypedArray对象一共提供9种类型的视图,每一种视图都是一种构造函数。
- Int8Array:8位有符号整数,长度1个字节。
- Uint8Array:8位无符号整数,长度1个字节。
- Uint8ClampedArray:8位无符号整数,长度1个字节,溢出处理不同。
- Int16Array:16位有符号整数,长度2个字节。
- Uint16Array:16位无符号整数,长度2个字节。
- Int32Array:32位有符号整数,长度4个字节。
- Uint32Array:32位无符号整数,长度4个字节。
- Float32Array:32位浮点数,长度4个字节。
- Float64Array:64位浮点数,长度8个字节。
这9个构造函数生成的对象,统称为TypedArray对象。它们很像正常数组,都有length属性,都能用方括号运算符([])获取单个元素,所有数组的方法,在类型化数组上面都能使用。两者的差异主要在以下方面。
- TypedArray数组的所有成员,都是同一种类型和格式。
- TypedArray数组的成员是连续的,不会有空位。
- Typed化数组成员的默认值为0。比如,new Array(10)返回一个正常数组,里面没有任何成员,只是10个空位;new Uint8Array(10)返回一个类型化数组,里面10个成员都是0。
- TypedArray数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。
构造函数
TypedArray数组提供9种构造函数,用来生成相应类型的数组实例。
构造函数有多种用法。
- TypedArray(buffer, byteOffset=0, length?)
同一个ArrayBuffer对象之上,可以根据不同的数据类型,建立多个视图。
1 | // 创建一个8字节的ArrayBuffer |
对于以上代码,v1、v2和v3是重叠的:v1[0]是一个32位整数,指向字节0~字节3;v2[0]是一个8位无符号整数,指向字节2;v3[0]是一个16位整数,指向字节2~字节3。只要任何一个视图对内存有所修改,就会在另外两个视图上反应出来。
注意,byteOffset必须与所要建立的数据类型一致,否则会报错。
1 | var buffer = new ArrayBuffer(8); |
上面代码中,新生成一个8个字节的ArrayBuffer对象,然后在这个对象的第一个字节,建立带符号的16位整数视图,结果报错。因为,带符号的16位整数需要两个字节,所以byteOffset参数必须能够被2整除。
如果想从任意字节开始解读ArrayBuffer对象,必须使用DataView视图,因为TypedArray视图只提供9种固定的解读格式。
- TypedArray(length)
视图还可以不通过ArrayBuffer对象,直接分配内存而生成。
1 | var f64a = new Float64Array(8); |
- TypedArray(typedArray)
类型化数组的构造函数,可以接受另一个视图实例作为参数。
1 | var typedArray = new Int8Array(new Uint8Array(4)); |
上面代码中,Int8Array构造函数接受一个Uint8Array实例作为参数。
注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
1 | var x = new Int8Array([1, 1]); |
上面代码中,数组y是以数组x为模板而生成的,当x变动的时候,y并没有变动。
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。
1 | var x = new Int8Array([1, 1]); |
- TypedArray(arrayLikeObject)
构造函数的参数也可以是一个普通数组,然后直接生成TypedArray实例。
1 | var typedArray = new Uint8Array([1, 2, 3, 4]); |
注意,这时TypedArray视图会重新开辟内存,不会在原数组的内存上建立视图。
上面代码从一个普通的数组,生成一个8位无符号整数的TypedArray实例。
TypedArray数组也可以转换回普通数组。
1 | var normalArray = Array.prototype.slice.call(typedArray); |
BYTES_PER_ELEMENT属性
每一种视图的构造函数,都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节数。
1 | Int8Array.BYTES_PER_ELEMENT // 1 |
ArrayBuffer与字符串的互相转换
ArrayBuffer转为字符串,或者字符串转为ArrayBuffer,有一个前提,即字符串的编码方法是确定的。假定字符串采用UTF-16编码(JavaScript的内部编码方式),可以自己编写转换函数。
1 | // ArrayBuffer转为字符串,参数为ArrayBuffer对象 |
TypedArray.prototype.set()
TypedArray数组的set方法用于复制数组(正常数组或TypedArray数组),也就是将一段内容完全复制到另一段内存。
1 | var a = new Uint8Array(8); |
上面代码复制a数组的内容到b数组,它是整段内存的复制,比一个个拷贝成员的那种复制快得多。set方法还可以接受第二个参数,表示从b对象哪一个成员开始复制a对象。
1 | var a = new Uint16Array(8); |
上面代码的b数组比a数组多两个成员,所以从b[2]开始复制。
TypedArray.prototype.subarray()
subarray方法是对于TypedArray数组的一部分,再建立一个新的视图。
1 | var a = new Uint16Array(8); |
subarray方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以,上面代码的a.subarray(2,3),意味着b只包含a[2]一个成员,字节长度为2。
TypedArray.prototype.slice()
TypeArray实例的slice方法,可以返回一个指定位置的新的TypedArray实例。
1 | let ui8 = Uint8Array.of(0, 1, 2); |
上面代码中,ui8是8位无符号整数数组视图的一个实例。它的slice方法可以从当前视图之中,返回一个新的视图实例。
slice方法的参数,表示原数组的具体位置,开始生成新数组。负值表示逆向的位置,即-1为倒数第一个位置,-2表示倒数第二个位置,以此类推。
TypedArray.of()
TypedArray数组的所有构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。
1 | Float32Array.of(0.151, -8, 3.7) |
TypedArray.from()
静态方法from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray实例。
1 | Uint16Array.from([0, 1, 2]) |
这个方法还可以将一种TypedArray实例,转为另一种。
1 | var ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2)); |
from方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似map方法。
1 | Int8Array.of(127, 126, 125).map(x => 2 * x) |
上面的例子中,from方法没有发生溢出,这说明遍历是针对新生成的16位整数数组,而不是针对原来的8位整数数组。也就是说,from会将第一个参数指定的TypedArray数组,拷贝到另一段内存之中(占用内存从3字节变为6字节),然后再进行处理。
DataView
如果一段数据包括多种类型(比如服务器传来的HTTP数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。
DataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
DataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。
1 | DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]); |
下面是一个例子。
1 | var buffer = new ArrayBuffer(24); |
DataView实例有以下属性,含义与TypedArray实例的同名方法相同。
- DataView.prototype.buffer:返回对应的ArrayBuffer对象
- DataView.prototype.byteLength:返回占据的内存字节长度
- DataView.prototype.byteOffset:返回当前视图从对应的ArrayBuffer对象的哪个字节开始
DataView实例提供8个方法读取内存。
- getInt8:读取1个字节,返回一个8位整数。
- getUint8:读取1个字节,返回一个无符号的8位整数。
- getInt16:读取2个字节,返回一个16位整数。
- getUint16:读取2个字节,返回一个无符号的16位整数。
- getInt32:读取4个字节,返回一个32位整数。
- getUint32:读取4个字节,返回一个无符号的32位整数。
- getFloat32:读取4个字节,返回一个32位浮点数。
- getFloat64:读取8个字节,返回一个64位浮点数。
这一系列get方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。
1 | var buffer = new ArrayBuffer(24); |
上面代码读取了ArrayBuffer对象的前5个字节,其中有一个8位整数和两个十六位整数。
如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。默认情况下,DataView的get方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get方法的第二个参数指定true。
1 | // 小端字节序 |
DataView视图提供8个方法写入内存。
- setInt8:写入1个字节的8位整数。
- setUint8:写入1个字节的8位无符号整数。
- setInt16:写入2个字节的16位整数。
- setUint16:写入2个字节的16位无符号整数。
- setInt32:写入4个字节的32位整数。
- setUint32:写入4个字节的32位无符号整数。
- setFloat32:写入4个字节的32位浮点数。
- setFloat64:写入8个字节的64位浮点数。
这一系列set方法,接受两个参数,第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据。对于那些写入两个或两个以上字节的方法,需要指定第三个参数,false或者undefined表示使用大端字节序写入,true表示使用小端字节序写入。
1 | // 在第1个字节,以大端字节序写入值为25的32位整数 |
如果不确定正在使用的计算机的字节序,可以采用下面的判断方式。
1 | var littleEndian = (function() { |
Blob
Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
要从其他非blob对象和数据构造一个Blob,请使用 Blob() 构造函数。要创建包含另一个blob数据的子集blob,请使用 slice()方法。要获取用户文件系统上的文件对应的Blob对象,请参阅 File文档。
从Blob中读取内容的唯一方法是使用 FileReader。以下代码将 Blob 的内容作为类型数组读取:
1 | var reader = new FileReader(); |
更多关于Blob的内容,请直接查看这里
数据格式转换
String转Blob
1 | //将字符串 转换成 Blob 对象 |
TypeArray转Blob
1 | //将 TypeArray 转换成 Blob 对象 |
ArrayBuffer转Blob
1 | var buffer = new ArrayBuffer(32); |
Blob转String
这里需要注意的是readAsText方法的使用。
1 | //将字符串转换成 Blob对象 |
Blob转ArrayBuffer
这里需要注意的是readAsArrayBuffer方法的使用。
1 | //将字符串转换成 Blob对象 |