这八大金刚有什么神通呢?我们无法直接读写 buffer 数据,而这八种数据类型充当了读写 buffer 内容的桥梁。
还是上面的 buffer,我们想查看一下 buffer 内容。首先创建一个读写该 buffer 的桥梁:
let int8Array = new Int8Array(buffer);
我们创建了一个读写 buffer 的桥梁,用 8 位有符号整数来读写 buffer。那现在我们看看 buffer 第一位的内容是什么吧?
console.log(int8Array[0]);
输出:0,看来初始化的时候,buffer 的各个字节存储的值默认都是 0 了。
我们看看如何使用 Int8Array 给 buffer 赋值:
int8Array[0] = 30; int8Array[1] = 41; int8Array[2] = 52; int8Array[3] = 63; int8Array[4] = 74; int8Array[5] = 85; int8Array[6] = 86; int8Array[7] = 97;
很简单,因为 Int8Array 是一个字节的长度,和 buffer 的单位一致,所以我们可以通过索引的形式对 buffer 指定位置进行赋值操作。
很简单吧?就是这么简单。
另外七大金刚和 Int8Array 的用法一样,但是有所不同,我们看下他们之间的区别。
这次我们使用 Int16Array,仍然是刚才的 buffer,我们创建一个新的桥梁,这座桥梁仍然通往 buffer 。
let int16Array = new Int16Array(buffer);
大家试想一下 int16Array[0] 是什么内容?
我们输出一下:
console.log(int16Array[0])
咦,结果怎么是 10526?
不太理解了。好吧,我们分析下 10526 怎么得来的。
我们看下 buffer 中的二进制数据。
由于 Int16Array 占两个字节,所以我们在用它读写数据的时候,一个索引所代表的数据等于 buffer 中两个字节。
我们可以看到 int16Array[0] 里面的二进制数据是由30的二进制和41的二进制数据拼接而成:00011110(30) 00101001(41)。
我们按照 30、41的顺序计算一下二进制对应的十进制数。
parseInt(1111000101001, 2) //输出 7721
算出来的值是 7721,这和我们输出的不一致呀?
这就涉及到字节顺序的概念。在我们的个人笔记本上一般都是小端字节序。小端字节序体现在我们这个示例中即是 41、30的二进制顺序,我们刚才的计算顺序有问题,那按照 41、30 的二进制顺序计算一下
parseInt(10100100011110, 2) //输出 10526
可以看到输出结果是 10526,和我们直接使用 int16Array[0]
得出的结果一致。
上面这个例子,告诉我们在换数据结构解析 buffer 的时候,数据会变得不容易理解,我们一定要谨慎处理。
属性和方法
类型化数组实例化的对象包含一些很有用的属性和方法:
length
length属性返回类型化数组的数据成员个数。
byteLength
返回类型化数组的字节长度。注意与length的区别。通长 byteLength = length * 每个数据占用字节数
byteOffset
返回该类型化数组的数据从所处 buffer 中的哪个字节开始。
buffer
类型化数组对应的 buffer。
set
复制数组,将某段内存中的数据完整地复制到另一段内存。
let a = new Uint8Array(12); a[0] = 31; a[1] = 32; let b = new Uint8Array(12); b.set(a);
上面这段代码的意思是将 a 这段buffer中的内容,完整地拷贝到 b 这段 buffer 中,这种方式比按索引赋值要快速地多。
当然,set 支持从某个索引开始复制数据
let a = new Uint8Array(12); a[0] = 31; a[1] = 32; let b = new Uint8Array(10); b.set(a, 2);
上面这段代码意思是从b的第三个索引位置开始复制 a 中的数据。
subarray
subarray的意思是对一个类型化数组,取其子数组的内容,返回一个新的类型化数组。
let a = new Uint8Array(8); a[2] = 1; let b = a.subarray(2,3); console.log(b.length); console.log(b.byteLength);
subarray 的第一个参数,代表从源数组的第几个索引开始截取,第二个参数代表截取到第几个索引。
混合视图
有一点需要注意,我们的类型数组初始化的时候,可以指定 buffer的某一段,这就意味着,我们可以对一段 buffer 内存区域指定多个类型数,我们称之为 混合视图。
let buffer = new Buffer(8); let idArray = new Int8Array(buffer, 0,2); let nameArray = new Int8Array(buffer, 2, 4); let ageArray = new Int8Array(buffer, 6, 2);
我们用一段内存区域表示一个人的 id、name、age。这种结构类似于 C 语言中的 struct 。
我们将一段 8 个字节的内存分成三个部分:
DataView
JavaScript 还引入了另外种视图DataView,也能达到操作 buffer 的目的,但相比之下,DataView 操作粒度更细一些,而且还能够设置字节序为大端还是小端。
DataView 的构造函数:
DataView(ArrayBuffer对象 buffer, 从 buffer 的第几个字节开始读取, 读取的长度);
举个例子来说:
let buffer = new ArrayBuffer(10); let view = new DataView(buffer);
如何读取?
我们创建好了视图 view, 那该如何读取呢?
JavaScript 提供了 8 种读取方式,功能很简单,也很容易理解,这里就不一一做示例了,大家可以自己试一下。
刚刚我们也说了,DataView 也支持设置字节序,在上面 8 中读取方式中,第一个字节是索引,第二个字节允许我们设置字节序,true 代表 小端字节序读取,false 代表大端字节序读取,默认为 false。
如何写入?
DataView 不仅能支持细粒度的读取操作,也支持细粒度的写入操作:
order 的意思仍然是设置写入时的字节序, true 为小端字节序,false 为大端字节序,默认为 false。
用法也很简单,大家可以练习一下。
至此,关于 JavaScript 操作二进制的方式就介绍完了,大家以后碰到需要直接操作内存的场景时,不妨用用这两种方式。
未来的你会感谢今天努力的你。
总结