JavaScript之ArrayBuffer
更新:HHH   时间:2023-1-7


在写作这篇博客的时候,参照了下面三篇博客:
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] // 1
y[0] // 1

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

上面代码中,数组y是以数组x为模板而生成的,当x变动的时候,y并没有变动。
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。

const x = new Int8Array([1, 1]);
const y = new Int8Array(x.buffer);
x[0] // 1
y[0] // 1

x[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];<br/>// or<br/>const normalArray = Array.from(typedArray);<br/>// or<br/>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);
返回开发技术教程...