JavaScript之ArrayBuffer
在写作这篇博客的时候,参照了下面三篇博客:
https://www.cnblogs.com/jixiaohua/p/10714662.html (写的很详细,参照比较多)
https://www.cnblogs.com/copperhaze/p/6149041.html
https://zh.javascript.info/arraybuffer-binary-arrays
文章中有一些内容是直接从上面博客复制过来的,并不是想要抄袭,只是觉得写博客可以增加理解度,别切可以避免遗忘。在此感谢上面三位博主的文章。
DataView部分完全复制上面第一个链接的博客。
ArrauBuffer对象、TypedArray视图和DataView视图是JavaScript中**专门操作二进制数据的接口**。他们都是以数组的方式操作二进制数组,所以被称为二进制数组。最初为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式的背景下诞生的。一.ArrayBuffer相关介绍
ArrayBuffer指的是一段连续的内存区域。
let buffer = new ArrayBuffer(40); // 在内存中开辟40个字节长度的内存区域alert(buffer.byteLength); // 40
1.通过ArrayBuffer的构造函数可以开辟指定长度的内存区域,单位是字节。
2.在没有赋值的情况下,开辟的内存区域中都是以0填充的。
3.创建内存区域后,无论赋不赋值,内存区域的大小不会改变。
4.通过ArrayBuffer创建的内存区域,不能直接读写,需要通过视图来进行操作(在视图部分会进行讲解)。
5.开辟出来的内存区域是用来存放原始二进制数据的。
当开辟的内存区域比较大的时候,可能会由于内存区域不足而报错,所以可以在创建完后看一看是否创建成功。
let buffer = new ArrayBuffer(40);if(buffer.byteLength === 40){ alert("创建成功");}else{ alert("创建失败");}
ArrayBuffer有一个长度属性,是byteLength,返回的是这个ArrayBuffer对象占多少个字节的内存大小。
还有一个比较常用的方法,是slice(begin,end)
let buffer = new ArrayBuffer(40);let newBuffer = buffer.slice(10,30);alert(newBuffer.byteLength); // 20
slice通过拷贝原有对象的内存区域,生成一个新的内存区域。
因为不能直接对ArrayBuffer直接进行读写,所以可用的属性和方法也并不多。
上面也已经提到过,通过ArrayBuffer创建的内存区域并不能直接进行读写,而要通过TypedArray视图或者DataView视图进行读写操作,
他们的作用就是将内存区域中的数据以特定的格式表示出来,并通过这种特定的格式来操作这段内存区域。TypedArray视图和DataView视图的区别是,
TypedArray视图中的数组成员都是同一种类型,而DataView视图的数组成员可以是不同的类型。
二.TypedArray视图
TypedArray视图并没有什么特别之处,它只是用来操作存放二进制数据内存的一种方式。
上面也已经提到过,作用就是讲内存区域以特定的格式表示出来,具体都有哪些特定的格式,JavaScript中有以下几种方式。
数据类型 | 字节长度 | 含义 |
---|---|---|
Int8Array | 1个字节 | 有符号整数,以8位(1个字节)的内存长度读写内存区域 |
Uint8Array | 1个字节 | 无符号整数,以8位(1个字节)的内存长度读写内存区域 |
Int16Array | 2个字节 | 有符号整数,以16位(2个字节)的内存长度读写内存区域 |
Unit16Array | 2个字节 | 无符号整数,以16位(2个字节)的内存长度读写内存区域 |
Int32Array | 4个字节 | 有符号整数,以32位(4个字节)的内存长度读写内存区域 |
Unit32Array | 4个字节 | 无符号整数,以32位(4个字节)的内存长度读写内存区域 |
Float32Array | 4个字节 | 浮点数,以32位(4个字节)的内存长度读写内存区域 |
Float64Array | 8个字节 | 浮点数,以64位(8个字节)的内存长度读写内存区域 |
以上8个数据类型,也是8个构造函数,都被称为TypedArray视图。
它们很像普通数组,都有length属性,都能用方括号运算符([])获取单个元素,所有数组的方法,在它们上面都能使用,所以TypedArray也被成为类型数组。
普通数组与 TypedArray 数组的差异主要在以下方面。
TypedArray 数组的所有成员,都是同一种类型。
TypedArray 数组的成员是连续的,不会有空位。
TypedArray 数组成员的默认值为 0。比如,new Array(10)返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)返回一个 TypedArray 数组,里面 10 个成员都是 0。
TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。
我们以Int16Array作为例子,来讲解TypedArray视图的用法。
Int16Array中的16表示的单个元素所占内存的位数,也就是说如果用Int16Array构造函数创建了一个对象来读写二进制数据内存,这个数组对象每次的每个元素都是16位(2个字节)大小。
可以通过BYTES_PER_ELEMENT属性,查看每一种类型中,元素所占内存大小。
alert(Int16Array.BYTES_PER_ELEMENT); // 2 每个元素占2个字节内存大小
使用方法:
可以通过构造函数来创建TypedArray数组对象。
1.TypedArray(buffer, byteOffset=0, length)
第一个参数(必须):指定Arraybuffer对象,也就是说是通过指定的ArrayBuffer对象来创建TypedArray数组对象,前面也已经说过了,TypedArray视图本身并不存储数据,实际的数据还是存在ArrayBuffer开辟的二进制内存区域上。
第二个参数:视图对象从ArrayBuffer内存区域的第几个字节开始,默认从0开始。
第三个参数:视图中包含的数据个数,默认是到指定ArrayBuffer对象内存区域的末尾。
// 开辟一段8个字节的内存区域const buffer = new ArrayBuffer(8);// 在buffer内存区域上创建一个Unit32位视图,从字节0开始,一直到结尾// 因为一个元素占32位(4个字节),总共有8个字节,所以在buffer这段内存区域上,能存储2个数据类型为Unit32的数据let uint32 = new Uint32Array(buffer);// 在buffer内存区域上从第二个字节开始,创建4个数据类型为Unit8Array的数据let uint8 = new Uint8Array(buffer,2,4);// 在buffer内存区域上从第四个字节开始,创建2个数据类型为Int16Array的数据let int16 = new Int16Array(buffer,4,2);
上述代码执行结束后buffer内存示意图如下
从上面的代码以及示意图中可以看出,对于一段ArrayBuffer内存区域,可以同时创建多个不同类型的TypedArray视图,但是这样会造成视图重叠,如果通过视图给内存赋值了,会造成数据覆盖。
2.TypedArray(length)
不通过ArrayBuffer,直接分配内存生成。其实这种方式的本质还是会先创建一个Arraybuffer对象。
let uint32 = new Uint32Array(4); // 创建一个元素个数为4的Uint32类型视图对象alert(uint32.buffer.byteLength); // 16 通过buffer属性,可以获取该视图锁对应的ArrayBuffer对象,可以看到该视图对象对应的ArrayBuffer内存区域大小为16个字节
3.TypedArray(typedArray)
通过另一个TypedArray实例对象来创建一个TypedArray。
const typedArray = new Int8Array(new Uint8Array(4)); //0,0,0,0
上面代码中,Int8Array构造函数接受一个Uint8Array实例作为参数。
注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
const x = new Int8Array([1, 1]);const y = new Int8Array(x);x[0] // 1y[0] // 1x[0] = 2;y[0] // 1
上面代码中,数组y是以数组x为模板而生成的,当x变动的时候,y并没有变动。
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。
const x = new Int8Array([1, 1]);const y = new Int8Array(x.buffer);x[0] // 1y[0] // 1x[0] = 2;y[0] // 2
4.TypedArray(arrayLikeObject)
构造函数的参数也可以是一个普通数组,然后直接生成TypedArray实例。const typedArray = new Uint8Array([1, 2, 3, 4]);
注意,这时TypedArray视图会重新开辟内存,不会在原数组的内存上建立视图。
上面代码从一个普通的数组,生成一个 8 位无符号整数的TypedArray实例。该数组有4个成员,每一个都是8位无符号整数。
TypedArray 数组也可以转换回普通数组。const normalArray = [...typedArray];
// or
const normalArray = Array.from(typedArray);
// or
const normalArray = Array.prototype.slice.call(typedArray);
与普通数组相比,TypedArray 数组的最大优点就是可以直接操作内存,不需要数据类型转换,所以速度快得多。
ArrayBuffer 与字符串的互相转换
ArrayBuffer转为字符串,或者字符串转为ArrayBuffer,有一个前提,即字符串的编码方法是确定的。假定字符串采用 UTF-16 编码(JavaScript 的内部编码方式),可以自己编写转换函数。
// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象function ab2str(buf) { // 注意,如果是大型二进制数组,为了避免溢出, // 必须一个一个字符地转 if (buf && buf.byteLength < 1024) { return String.fromCharCode.apply(null, new Uint16Array(buf)); } const bufView = new Uint16Array(buf); const len = bufView.length; const bstr = new Array(len); for (let i = 0; i < len; i++) { bstr[i] = String.fromCharCode.call(null, bufView[i]); } return bstr.join('');}// 字符串转为 ArrayBuffer 对象,参数为字符串function str2ab(str) { const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节 const bufView = new Uint16Array(buf); for (let i = 0, strLen = str.length; i < strLen; i++) { bufView[i] = str.charCodeAt(i); } return buf;}
三.DataView 视图
如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。
DataView视图提供更多操作选项,而且支持设定字节序。
本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;
而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
DataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。
DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
const buffer = new ArrayBuffer(24);const dv = new DataView(buffer);
DataView实例有以下属性,含义与TypedArray实例的同名方法相同。
DataView.prototype.buffer:返回对应的 ArrayBuffer 对象
DataView.prototype.byteLength:返回占据的内存字节长度
DataView.prototype.byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始
DataView 的读取
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方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。
// 从第一个字节开始读取8位无符号整数const v1 = dv.getUint8(0);// 从第2个字节开始读取16位有符号整数,占2个字节const v2 = dv.getInt16(1);// 从第4个字节开始读取16位有符号整数,2个字节const v3 = dv.getInt16(3);
上面代码读取了ArrayBuffer对象的前 5 个字节,其中有一个 8 位整数和两个十六位整数。
如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。
默认情况下,DataView的get方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get方法的第二个参数指定true。
// 小端字节序const v1 = dv.getUint16(1, true);// 大端字节序const v2 = dv.getUint16(3, false);// 大端字节序const v3 = dv.getUint16(3);
DataView 的写入
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个字节,以大端字节序写入值为25的32位整数dv.setInt32(0, 25, false);// 在第5个字节,以大端字节序写入值为25的32位整数dv.setInt32(4, 25);// 在第9个字节,以小端字节序写入值为2.5的32位浮点数dv.setFloat32(8, 2.5, true);