JavaScript简明教程-ArrayBuffer

3,565 阅读14分钟

基本概念

二进制数组由三类对象组成: ArrayBuffer对象、TypedArray视图、DataView视图

二进制数组设计的目的与 WebGL 项目相关,是为了解决浏览器与显卡之间通信的问题,因为要与显卡通信的话,会要求数据必须是二进制

二进制数组并不是真正数组,只是类数组对象

ArrayBuffer对象

代表原始的二进制数据

代表内存之中的一段二进制数据,不能直接读写,只能通过视图(TypedArray视图和DataView视图)进行操作,因为视图部署了数组接口,所以可以使用数组的方法操作内存

ArrayBuffer对象作为内存区域,可以存放多种类型数据,同一段内存,不同数据有不同的解读方式,不同的解读方式就叫做“视图”(view),ArrayBuffer有两种视图,一种是TypedArray视图(数组成员都是同一种数据类型组成的简单二进制),一种是DataView视图(数组成员可以是不同的数据类型组成的复杂二进制)

const buf = new ArrayBuffer(32); // 生成一段32字节的内存区域,每个字节默认为0
const dataView = new DataView(buf); // 创建DataView视图
dataView.getUint8(0); // 0

TypedArray与 DataView的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式

const x1 = new Int8Array(buf);
const x2 = new Uint8Array(buf);

这里是建立了两种TypedArray视图

ArrayBuffer.prototype.byteLength

返回ArrayBuffer 实例的 byteLength 属性,返回内存区域的字节长度。考虑如果分配的内存区域过大,可能会分配失败,所以正常使用时需要检查下分配是否成功

const buffer = new ArrayBuffer(32);
if (buffer.length === 32) {
...
} else {
...
}

ArrayBuffer.prototype.slice

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

const buffer = new ArrayBuffer(32);
const newBuffer = buffer.slice(0,3); // 拷贝buffer对象的前3个字节

ArrayBuffer.prototype.isView

表示参数是否为ArrayBuffer 的视图实例,其实就是判断是否为TypedArray 实例或DataView 实例

const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer);
// false

const int32 = new Int32Array(buffer);
ArrayBuffer.isView(int32);
// true

TypedArray视图

用来读写简单类型的二进制数据

数据类型 字节长度 含义
Int8Array 1 8 位带符号整数
Uint8Array 1 8 位不带符号整数
Uint8CArray 1 8 位不带符号整数(自动过滤溢出)
Int16Array 2 16 位带符号整数
Uint16Array 2 16 位不带符号整数
Int32Array 4 32 位带符号整数
Uint32Array 4 32 位不带符号的整数
Float32Array 4 32 位浮点数
Float64Array 8 64 位浮点数

上面9种类型都是一种构造函数,它们很像普通数组,都有length 属性,都能用[]获取单个元素,所有数组的方法,都可以使用。但它们(下面简称视图数组)和普通数组还是有下面的差距:

  • 视图数组的所有成员,只能是同一种类型
  • 视图数组的成员是连续的,不会有空位
  • 视图数组的成员默认值是0,new Uint8Array(10) 返回一个 TypedArray 数组,数组10个成员都是0
  • 视图数组本身不能单独使用,必须借助ArrayBuffer 才可使用

DataView视图支持除Uint8C以外的其他 8 种

// 分配一段32字节的内存区域,每个字节默认都是0
// 参数是所需要的内存大小(单位字节)
const buf = new ArrayBuffer(32);
// 这时如果我们在控制台查看buf,能看到一些有关buf的数据信息
// [[Int8Array]]:Int8Array(32) [0,0,0...0]
// [[Int16Array]]:Int16Array(16) [0,0,0...0]
// [[Int32Array]]:Int32Array(8) [0,0,0...0]
// [[Uint8Array]]:Uint8Array(32) [0,0,0...0]
// 控制台的信息验证了数据类型的字节长度,以及默认值都为0

TypedArray除了接受ArrayBuffer实例作为参数,还可以直接接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时赋值

const typedArray = new Uint8Array([0,1,2]);
typedArray.length; 
// 3
typedArray
// Uint8Array(3) [0, 1, 2]

TypedArray(buffer, buteOffset = 0, length)

根据不同的数据类型,建立多个视图

const buf = new ArrayBuffer(8);

const v1 = new Int32Array(buf);
const v2 = new Unit8Array(buf, 2);
const v3 = new Int16Array(buf, 2, 2);

上面代码是在一段长度为8个字节的内存之上,生成三个视图

视图构造函数接受三个参数:

  • 第一个参数(必须):视图对应的底层ArrayBuffer 对象
  • 第二个参数(可选):视图开始的字节序号,默认从0开始
  • 第三个参数(可选):视图包含的数据个数,默认到本段内存区域结束

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

TypedArray(length)

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

const f64 = new Float64Array(8);
f64[0] = 10;
f64[1] = 20;

可以生成8个成员的Float64Array数组,然后依次分配(视图数组的赋值和普通数组操作一致)

TypedArray(typedArray)

TypedArray 数组的构造函数,可以接受另一个typedArray 实例作为参数

const typedArray = new Int8Array(new Unit8Array(4));

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

TypedArray(arrayLikeObject)

视图数组的构造函数的参数也可以是一个普通数组,然后直接生成TypedArray 实例

const typedArray = new Uint8Array([1, 2, 3, 4]);

TypedArray 视图会新开内存区域,建立新的视图,TypedArray 数组可以转换回普通数组

const normalArray = [...typedArray];
...
const normalArray = Array.from(typedArray);

TypedArray.prototype.buffer

返回整段内存区域对应的ArrayBuffer 对象,这是一个只读属性

const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);

上面a b对应同一个ArrayBuffer对象

TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

byteLength 属性返回 TypedArray 数组占据的内存长度,单位为字节。

byteOffset 属性返回 TypedArray 数组从底层ArrayBuffer对象的哪个字节开始。这两个属性都是只读属性

const b = new ArrayBuffer(8);

const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);

v1.byteLength // 8
v2.byteLength // 6
v3.byteLength // 4

v1.byteOffset // 0
v2.byteOffset // 2
v3.byteOffset // 2

TypedArray.prototype.length

length属性表示 TypedArray 数组含有多少个成员。

byteLength属性和length属性区分: 前者是字节长度,后者是成员长度。

const a = new Int16Array(8);

a.length // 8
a.byteLength // 16

TypedArray.prototype.set()

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

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);

上面代码复制a数组的内容到b数组,它是整段内存的复制,比一个个拷贝成员的那种复制快得多。

set方法还可以接受第二个参数,表示从b对象的哪一个成员开始复制a对象。

const a = new Uint16Array(8);
const b = new Uint16Array(10);

b.set(a, 2)

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

TypedArray.prototype.subarray()

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

const a = new Uint16Array(8);
const 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实例。

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

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

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

TypedArray.of()

所有视图数组都有一个静态方法of,用于将参数转为一个TypedArray 实例

下面三种方法都会生成同样一个 TypedArray 数组。

// 方法一
let tarr = new Uint8Array([1,2,3]);

// 方法二
let tarr = Uint8Array.of(1,2,3);

// 方法三
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;

TypedArray.from()

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

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

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

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

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

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方法没有发生溢出,这说明遍历不是针对原来的 8 位整数数组。也就是说,from会将第一个参数指定的 TypedArray 数组,拷贝到另一段内存之中,处理之后再将结果转成指定的数组格式。

普通数组的方法,除了concat方法,其他方法均可使用在视图数组上

视图数组部署了 Iterator 接口,所以可以被遍历

BYTES_PER_ELEMENT属性

每一种视图的构造函数,都有一个BYTES_PER_ELEMENT 属性,表示这种数据占据的字节数

Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Uint8ClampedArray.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与字符串转换

后续理解了再补充

溢出

后续理解了再补充

复合视图

由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。

const buffer = new ArrayBuffer(24);

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);

上面代码将一个 24 字节长度的ArrayBuffer对象,分成三个部分:

  • 字节 0 到字节 3:1 个 32 位无符号整数
  • 字节 4 到字节 19:16 个 8 位整数
  • 字节 20 到字节 23:1 个 32 位浮点数

DataView视图

可以自定义复合格式的视图,用来读写复杂类型的二进制数据

  • TypedArray视图,用来向网卡、声卡之类的本纪设备传送数据,使用本机字节序;
  • DataView视图,用来处理网络设备传送的数据,所以可以设定大端字节序或小端字节序。

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

DataView和TypedArray使用方式一致

DataView实例的属性

  • DataView.prototype.buffer:返回对应的 ArrayBuffer 对象
  • DataView.prototype.byteLength:返回占据的内存字节长度
  • DataView.prototype.byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始

DataView实例方法

读取内存的方法
  • 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 方法的第一个参数是一个字节序号(不能为负),表示从哪个字节开始读取,默认情况下是使用大端字节序读取数据,如果是小端字节序,必须在get 方法中指定第二个参数为true

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

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

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

// 小端字节序
const v1 = dv.getUint16(1, true);

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

// 大端字节序
const v3 = dv.getUint16(3);
写入内存的方法
  • 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表示使用小端字节序写入。
// 在第5个字节,以大端字节序写入值为25的32位整数
dv.setInt32(4, 25);

// 在第9个字节,以小端字节序写入值为2.5的32位浮点数
dv.setFloat32(8, 2.5, true);
字节序判断
const littleEndian = (function() {
  const buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true);
  return new Int16Array(buffer)[0] === 256;
})();

如果返回true,就是小端字节序;如果返回false,就是大端字节序。

字节序

字节序是指数值在内存中的表示方式

const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);
for (let i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
}
int32View;
// Int32Array(4) [0, 2, 4, 6]
...
const int16View = new Int16Array(buffer);
int16View;
// Int16Array(8) [0, 0, 2, 0, 4, 0, 6, 0]
  • 大端字节序:在内存中,按照最低有效字节到最高有效字节顺序存储对象,即数据的高字节保存在内存的低地址中,而数据的低字节,保存在内存的高地址中
  • 小端字节序:在内存中,按照从最高有效字节到最低有效字节的顺序存储对象,即数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中

比如一个占据四个字节的16进制数0x12345678,小端字节序的存储顺序就是78654321。大端字节序则相反,存储顺序为12345678

小端字节序

地址 数据
0x00000100 0x78
0x00000101 0x56
0x00000102 0x34
0x00000103 0x12

内存地址由低到高,数据字节由低到高

大端字节序

地址 数据
0x00000100 0x12
0x00000101 0x34
0x00000102 0x56
0x00000103 0x78

内存地址由低到高,数据字节由高到低

这里我们可以按平时书写数值的习惯来理解,比如我们写10000,就是先写高位,再写低位

网络字节序

根据UDP/TCP/IP协议规定,把接收的第一个字节当作高位字节看待,所以网络字节序是大端字节序

现有x86体系的计算机都采用小端字节序(little endian),TypedArray数组内部也是采用小端字节序,所以如果某段数据是大端字节序(很多网络设备和特定操作系统都采用大端字节序),TypedArray数组将无法正确解析,但是DataView对象可以设定字节序,能解决这个问题

二进制数组应用

AJAX

研究http请求时完善这部分

Canvas

研究webgl时完善这部分

WebSocket

研究WebSocket完善这部分

Fetch API

研究Fetch API完善这部分

File API

研究Node 文件这部分时完善这部分

SharedArrayBuffer

SharedArrayBuffer允许Worker线程和主线程共享一块内存

其他研究Web worker时完善

包括多线程以及Atomics对象