目前在一个项目中,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
2
3
var buf = new ArrayBuffer(32);
var dataView = new DataView(buf);
dataView.getUint8(0) // 0

上面代码对一段32字节的内存,建立DataView视图,然后以不带符号的8位整数格式,读取第一个元素,结果得到0,因为原始内存的ArrayBuffer对象,默认所有位都是0。

另外,我们可以将ArrayBuffer生成的结果,传入TypedArray中:

1
2
3
4
5
6
7
8
var buffer = new ArrayBuffer(12);

var x1 = new Int32Array(buffer);
x1[0] = 1;
var x2 = new Uint8Array(buffer);
x2[0] = 2;

x1[0] // 2

ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。

1
2
3
var buffer = new ArrayBuffer(32);
buffer.byteLength
// 32

如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。

1
2
3
4
5
if (buffer.byteLength === n) {
// 成功
} else {
// 失败
}

ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。

1
2
var buffer = new ArrayBuffer(8);
var newBuffer = buffer.slice(0, 3);

上面代码拷贝buffer对象的前3个字节(从0开始,到第3个字节前面结束),生成一个新的ArrayBuffer对象。slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。

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

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

ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。

1
2
3
4
5
var buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false

var v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true

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
2
3
4
5
6
7
8
9
10
11
// 创建一个8字节的ArrayBuffer
var b = new ArrayBuffer(8);

// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
var v1 = new Int32Array(b);

// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
var v2 = new Uint8Array(b, 2);

// 创建一个指向b的Int16视图,开始于字节2,长度为2
var v3 = new Int16Array(b, 2, 2);

对于以上代码,v1、v2和v3是重叠的:v1[0]是一个32位整数,指向字节0~字节3;v2[0]是一个8位无符号整数,指向字节2;v3[0]是一个16位整数,指向字节2~字节3。只要任何一个视图对内存有所修改,就会在另外两个视图上反应出来。

注意,byteOffset必须与所要建立的数据类型一致,否则会报错。

1
2
3
var buffer = new ArrayBuffer(8);
var i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2

上面代码中,新生成一个8个字节的ArrayBuffer对象,然后在这个对象的第一个字节,建立带符号的16位整数视图,结果报错。因为,带符号的16位整数需要两个字节,所以byteOffset参数必须能够被2整除。

如果想从任意字节开始解读ArrayBuffer对象,必须使用DataView视图,因为TypedArray视图只提供9种固定的解读格式。

  • TypedArray(length)

视图还可以不通过ArrayBuffer对象,直接分配内存而生成。

1
2
3
4
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];
  • TypedArray(typedArray)

类型化数组的构造函数,可以接受另一个视图实例作为参数。

1
var typedArray = new Int8Array(new Uint8Array(4));

上面代码中,Int8Array构造函数接受一个Uint8Array实例作为参数。

注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。

1
2
3
4
5
6
7
var x = new Int8Array([1, 1]);
var y = new Int8Array(x);
x[0] // 1
y[0] // 1

x[0] = 2;
y[0] // 1

上面代码中,数组y是以数组x为模板而生成的,当x变动的时候,y并没有变动。

如果想基于同一段内存,构造不同的视图,可以采用下面的写法。

1
2
3
4
5
6
7
var x = new Int8Array([1, 1]);
var y = new Int8Array(x.buffer);
x[0] // 1
y[0] // 1

x[0] = 2;
y[0] // 2
  • 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
2
3
4
5
6
7
8
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8
ArrayBuffer与字符串的互相转换

ArrayBuffer转为字符串,或者字符串转为ArrayBuffer,有一个前提,即字符串的编码方法是确定的。假定字符串采用UTF-16编码(JavaScript的内部编码方式),可以自己编写转换函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ArrayBuffer转为字符串,参数为ArrayBuffer对象
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}

// 字符串转为ArrayBuffer对象,参数为字符串
function str2ab(str) {
var buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
var bufView = new Uint16Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
TypedArray.prototype.set()

TypedArray数组的set方法用于复制数组(正常数组或TypedArray数组),也就是将一段内容完全复制到另一段内存。

1
2
3
4
var a = new Uint8Array(8);
var b = new Uint8Array(8);

b.set(a);

上面代码复制a数组的内容到b数组,它是整段内存的复制,比一个个拷贝成员的那种复制快得多。set方法还可以接受第二个参数,表示从b对象哪一个成员开始复制a对象。

1
2
3
4
var a = new Uint16Array(8);
var b = new Uint16Array(10);

b.set(a, 2)

上面代码的b数组比a数组多两个成员,所以从b[2]开始复制。

TypedArray.prototype.subarray()

subarray方法是对于TypedArray数组的一部分,再建立一个新的视图。

1
2
3
4
5
var a = new Uint16Array(8);
var b = a.subarray(2,3);

a.byteLength // 16
b.byteLength // 2

subarray方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以,上面代码的a.subarray(2,3),意味着b只包含a[2]一个成员,字节长度为2。

TypedArray.prototype.slice()

TypeArray实例的slice方法,可以返回一个指定位置的新的TypedArray实例。

1
2
3
let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1)
// Uint8Array [ 2 ]

上面代码中,ui8是8位无符号整数数组视图的一个实例。它的slice方法可以从当前视图之中,返回一个新的视图实例。

slice方法的参数,表示原数组的具体位置,开始生成新数组。负值表示逆向的位置,即-1为倒数第一个位置,-2表示倒数第二个位置,以此类推。

TypedArray.of()

TypedArray数组的所有构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。

1
2
Float32Array.of(0.151, -8, 3.7)
// Float32Array [ 0.151, -8, 3.7 ]
TypedArray.from()

静态方法from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray实例。

1
2
Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]

这个方法还可以将一种TypedArray实例,转为另一种。

1
2
var ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
ui16 instanceof Uint16Array // true

from方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似map方法。

1
2
3
4
5
Int8Array.of(127, 126, 125).map(x => 2 * x)
// Int8Array [ -2, -4, -6 ]

Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
// Int16Array [ 254, 252, 250 ]

上面的例子中,from方法没有发生溢出,这说明遍历是针对新生成的16位整数数组,而不是针对原来的8位整数数组。也就是说,from会将第一个参数指定的TypedArray数组,拷贝到另一段内存之中(占用内存从3字节变为6字节),然后再进行处理。

DataView

如果一段数据包括多种类型(比如服务器传来的HTTP数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。

DataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。

DataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。

1
DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);

下面是一个例子。

1
2
var buffer = new ArrayBuffer(24);
var dv = new DataView(buffer);

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
2
3
4
5
6
7
8
9
10
11
var buffer = new ArrayBuffer(24);
var dv = new DataView(buffer);

// 从第1个字节读取一个8位无符号整数
var v1 = dv.getUint8(0);

// 从第2个字节读取一个16位无符号整数
var v2 = dv.getUint16(1);

// 从第4个字节读取一个16位无符号整数
var v3 = dv.getUint16(3);

上面代码读取了ArrayBuffer对象的前5个字节,其中有一个8位整数和两个十六位整数。

如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。默认情况下,DataView的get方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get方法的第二个参数指定true。

1
2
3
4
5
6
7
8
// 小端字节序
var v1 = dv.getUint16(1, true);

// 大端字节序
var v2 = dv.getUint16(3, false);

// 大端字节序
var v3 = dv.getUint16(3);

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
2
3
4
5
6
7
8
// 在第1个字节,以大端字节序写入值为25的32位整数
dv.setInt32(0, 25, false);

// 在第5个字节,以大端字节序写入值为25的32位整数
dv.setInt32(4, 25);

// 在第9个字节,以小端字节序写入值为2.5的32位浮点数
dv.setFloat32(8, 2.5, true);

如果不确定正在使用的计算机的字节序,可以采用下面的判断方式。

1
2
3
4
5
var littleEndian = (function() {
var buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true);
return new Int16Array(buffer)[0] === 256;
})();

Blob

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

要从其他非blob对象和数据构造一个Blob,请使用 Blob() 构造函数。要创建包含另一个blob数据的子集blob,请使用 slice()方法。要获取用户文件系统上的文件对应的Blob对象,请参阅 File文档。

从Blob中读取内容的唯一方法是使用 FileReader。以下代码将 Blob 的内容作为类型数组读取:

1
2
3
4
5
var reader = new FileReader();
reader.addEventListener("loadend", function() {
// reader.result 包含转化为类型数组的blob
});
reader.readAsArrayBuffer(blob);

更多关于Blob的内容,请直接查看这里

数据格式转换

String转Blob

1
2
3
4
5
6
//将字符串 转换成 Blob 对象
var blob = new Blob(["Hello World!"], {
type: 'text/plain'
});
console.info(blob);
console.info(blob.slice(1, 3, 'text/plain'));

TypeArray转Blob

1
2
3
4
5
6
7
8
9
10
11
12
//将 TypeArray  转换成 Blob 对象
var array = new Uint16Array([97, 32, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]);
//测试成功
//var blob = new Blob([array], { type: "application/octet-binary" });
//测试成功, 注意必须[]的包裹
var blob = new Blob([array]);
//将 Blob对象 读成字符串
var reader = new FileReader();
reader.readAsText(blob, 'utf-8');
reader.onload = function (e) {
console.info(reader.result); //a Hello world!
}

ArrayBuffer转Blob

1
2
var buffer = new ArrayBuffer(32);
var blob = new Blob([buffer]); // 注意必须包裹[]

Blob转String

这里需要注意的是readAsText方法的使用。

1
2
3
4
5
6
7
8
9
10
//将字符串转换成 Blob对象
var blob = new Blob(['中文字符串'], {
type: 'text/plain'
});
//将Blob 对象转换成字符串
var reader = new FileReader();
reader.readAsText(blob, 'utf-8');
reader.onload = function (e) {
console.info(reader.result);
}

Blob转ArrayBuffer

这里需要注意的是readAsArrayBuffer方法的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//将字符串转换成 Blob对象
var blob = new Blob(['中文字符串'], {
type: 'text/plain'
});
//将Blob 对象转换成 ArrayBuffer
var reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = function (e) {
console.info(reader.result); //ArrayBuffer {}
//经常会遇到的异常 Uncaught RangeError: byte length of Int16Array should be a multiple of 2
//var buf = new int16array(reader.result);
//console.info(buf);

//将 ArrayBufferView 转换成Blob
var buf = new Uint8Array(reader.result);
console.info(buf); //[228, 184, 173, 230, 150, 135, 229, 173, 151, 231, 172, 166, 228, 184, 178]
reader.readAsText(new Blob([buf]), 'utf-8');
reader.onload = function () {
console.info(reader.result); //中文字符串
};

//将 ArrayBufferView 转换成Blob
var buf = new DataView(reader.result);
console.info(buf); //DataView {}
reader.readAsText(new Blob([buf]), 'utf-8');
reader.onload = function () {
console.info(reader.result); //中文字符串
};
}