基本概念
二进制数组由三类对象组成: 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对象