ArrayBuffer
ArrayBuffer: ArrayBuffer 对象用来表示通用的原始二进制数据缓冲区,接口原始设计目的与 WebGL 有关
WebGL: 指浏览器与显卡之间的通信接口
为了满足 JS 与显卡之间大量、实时的数据交互,它们之间的数据通信必须是二进制的,而不能是传统的文本格式
如文本格式传递一个 32 位整数,两端的 JS 脚本与显卡都要进行格式转化,将会非常耗时
所以如果可以将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升
二进制数组就是在这种背景下诞生的 → 它很像 C 语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了 Js 处理二进制数据的能力,使得开发者有可能通过 Js 与操作系统的原生接口进行二进制通信
二进制数组主要由这三类对象组成: ArrayByffer & TypedArray & DataView
-
ArrayBuffer 对象:
代表内存之中的一段二进制数据,可以通过 "视图" 进行操作,"视图" 部署了数组接口,这意味着,可以用数组的方法操作内存 → tip: ArrayBuffer 只能通过下面的两个视图对象进行操作
-
TypedArray 视图:
共包括 9 种类型的视图
-
数据类型 字节长度 含义 对应的 C 语言类型 Int8 1 8 位带符号整数 signed char Uint8 1 8 位不带符号整数 unsigned char Uint8C 1 8 位不带符号整数(自动过滤溢出 unsigned char Int16 2 16 位带符号整数 short Uint16 2 16 位不带符号整数 unsigned short Int32 4 32 位带符号整数 int Uint32 4 32 位不带符号的整数 unsigned int Float32 4 32 位浮点数 float Float64 8 64 位浮点数 double
-
-
DataView 视图:
可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序
-
概括:
ArrayBuffer 对象代表原始的二进制数据 | TypedArray 视图用来读写简单类型的二进制数据 | DataView 视图用来读写复杂类型的二进制数据
创建 ArrayBuffer 对象: new ArrayBuffer(byteLength) | new ArrayBuffer(byteLength,options)
-
const buffer = new ArrayBuffer(32) // --> 创建一个 32 字节的缓冲区(内存区域) → 每个字节默认都为 0
-
// 可通过第二个参数 options 中的 maxByteLength 来配置最大字节数 const buffer = new ArrayBuffer(32, { maxByteLength: 64 // --> 创建一个 32 字节的缓冲区,但允许最大扩展到 64 字节 })
通过视图进行基本读写操作: 举例
-
TypedArray
-
const buffer = new ArrayBuffer(16) // --> 创建一个 16 字节的缓冲区(内存区域) → 每个字节默认都为 0 const typed_array1 = new Int32Array(buffer) // --> 创建一个指向 buffer 的 Int32 视图(4字节) console.log(buffer, typed_array1) typed_array1[0] = 100 // --> 通过视图,修改第一个元素 console.log(typed_array1, typed_array1[0])
-
// TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值 const typed_array2 = new Int32Array([100, 0, 0, 0]) // --> 创建一个包含 4 个元素的 Int32 视图 console.log(typed_array2); // -- 与上面一致
-
-
DataView
-
// --> 创建一个指向 buffer 的 DataView 视图 → DataView视图的创建,需要提供ArrayBuffer对象实例作为参数 const buffer = new ArrayBuffer(16) const dataview = new DataView(buffer) console.log(dataview) // -- 在 DataView 中通过对应的 getInt32 方法,可以读取指定位置的字节,并返回一个 32 位有符号整数,通过 setInt32 方法,可以写入一个 32 位有符号整数到指定位置(其它 int8 等同理) dataview.setInt32(0, 100, true) console.log(dataview.getInt32(0, true)) //DataView 实例提供了以下方法来读取内存,参数是对应的字节序号(索引),表示读取的字节位置
-
具体使用在下面示例
-
实例属性: 原型
- byteLength:
属性返回该数组缓冲区的长度(以字节为单位)
- detached:
属性返回一个布尔值,指示该缓冲区是否已经分离(传输)
ArrayBuffer 被创建时该值位 false,当 ArrayBuffer 已被传输时该值就变为 true → 对应的实例也会从内存中分离 → 变为不可用
- maxByteLength:
属性返回该数组缓冲区可调整到的最大长度(以字节为单位)
- resizable:
属性返回此数组缓冲区是否可以调整大小
实例方法: 原型
-
resize:
方法将
ArrayBuffer调整为指定的大小,以字节为单位
-
const buffer = new ArrayBuffer(12, { maxByteLength: 32 }) console.log(buffer.byteLength) // log: 12 buffer.resize(16) // -- 调整缓冲区大小为 16 字节 console.log(buffer.byteLength) // log: 16;
-
-
slice:
方法用于将内存区域的一部分,拷贝生成一个新的 ArrayBuffer 对象
切片
-
const buffer = new ArrayBuffer(8); const newBuffer = buffer.slice(0, 3); // -- slice(start,end) : [start.end)
-
``slice
方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个
ArrayBuffer对象拷贝过去
-
-
transfer & transferToFixedLength:
实验性方法,自行查阅文档
视图
视图:
ArrayBuffer 对象作为内存区域,可以存放多种类型的数据 → 同一段内存,不同数据有不同的解读方式,这就叫做“视图”(view)
ArrayBuffer 有两种视图,一种是 TypedArray 视图,另一种是 DataView 视图
TypedArray 视图
TypedArray 视图: 目前 TypedArray 视图一共包括 9 种类型,每一种视图都是一种构造函数
→ 这 9 个构造函数生成的数组,统称为 TypedArray 视图
-
数据类型 字节长度 含义 对应的 C 语言类型 Int8 1 8 位带符号整数 signed char Uint8 1 8 位不带符号整数 unsigned char Uint8C 1 8 位不带符号整数(自动过滤溢出 unsigned char Int16 2 16 位带符号整数 short Uint16 2 16 位不带符号整数 unsigned short Int32 4 32 位带符号整数 int Uint32 4 32 位不带符号的整数 unsigned int Float32 4 32 位浮点数 float Float64 8 64 位浮点数 double -
与普通数组的差异:
-
+ TypedArray 数组的所有成员,都是同一种类型 + TypedArray 数组的成员是连续的,不会有空位 + TypedArray 数组成员的默认值为 0 + TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的 ArrayBuffer 对象之中,要获取底层对象必须使用 buffer 属性
-
TypedArray 视图构造函数的几种用法
-
1. TypedArray(buffer, byteOffset=0, length?):
同一个 ArrayBuffer 对象之上,可以根据不同的数据类型,建立多个视图
-
参数: + 第一个参数(必需):视图对应的底层 ArrayBuffer 对象 + 第二个参数(可选):视图开始的字节序号,默认从 0 开始 + 第三个参数(可选):视图包含的数据个数,默认直到本段内存区域结束
-
// 创建一个8字节的ArrayBuffer const b = new ArrayBuffer(8); // 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾 → 每个元素占用4个字节,所以对应 v1 的长度为 2 const v1 = new Int32Array(b); // 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾 → 每个元素占用 1 个字节,并且偏移两个字节,所以对应 v2 的长度为 6 const v2 = new Uint8Array(b, 2); // 创建一个指向b的Int16视图,开始于字节2,长度为2 → 每个元素占用 2 个字节,并且偏移两个字节与视图包含数据的个数位 2,所以对应 v3 的长度为 2 const v3 = new Int16Array(b, 2, 2); console.log(v1.length, v2.length, v3.length) // 输出:2 6 2 // -- 长度解析 ↓ v1 + 开始于字节 0,直到缓冲区的末尾 + 每个元素占用 4 个字节 + 由于 ArrayBuffer 的长度是 8 字节,可以容纳 2 个 32 位整数,所以 v1 的长度为 2 v2 + 开始于字节 2,直到缓冲区的末尾 + 每个元素占用 1 个字节 + 由于偏移了 2 个字节,并且 ArrayBuffer 的长度是 8 字节,所以 v2 的长度为 6(8 - 2 = 6) v3 + 开始于字节 2,长度为 2 + 每个元素占用 2 个字节 + 由于偏移了 2 个字节,并且长度为 2,所以 v3 的长度为 2
-
🔺只要任何一个视图对内存有所修改,对应其它也共享着该 ArrayBuffer 对应内存的视图上也反应出来
上面也提到了,因为视图本身不储存数据,当修改对应数据时,它的数据都是储存在底层的 ArrayBuffer 对象之中
-
console.log(v1, v2, v3); v3[0] = 100 // -- 修改 v3 视图中的第一个元素 console.log(v1, v2, v3); // -- 由于 v1 和 v2 也共享着该 v3 视图中的第一个元素的 ArrayBuffer 中的数据,所以对应的内存空间的元素也会进行改变
-
-
注意: byteOffset 必须与所要建立的数据类型一致,否则会报错
如下示例
-
const buffer = new ArrayBuffer(8); const i16 = new Int16Array(buffer, 1); // Uncaught RangeError: start offset of Int16Array should be a multiple of 2 ↑ 因为 i16 视图每个元素是占 2 个字节的,对应的 byteOffset 参数必须能够被 2 整除 → 即偏移对应元素的字节(如 1 3 5 等,就会抛出错误) + 如果想从任意字节开始解读 ArrayBuffer 对象,必须使用 DataView 视图,因为 TypedArray 视图只提供 9 种固定的解读格式
-
-
-
2. TypedArray(length):
视图可以不通过 ArrayBuffer 对象,直接分配内存生成
-
const f32a = new Float32Array(2) // --> 创建一个包含 2 个元素的 Float32Array 视图(每个元素4字节) console.log(f32a) f32a[0] = 1.1 f32a[1] = f32a[0] * 2 console.log(f32a) ↑ 生成 2 个成员的 Float32Array 数组(共8个字节)
-
-
3. TypedArray(typedArray):
接收另外一个 TypedArray 作为参数
-
const x1 = new Uint8Array(4) const typedArray = new Int8Array(x1) // -- 该 typedArray 根据 x1 生成一个新的 4 给元素的视图(每个元素 1 字节)
-
注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的 → 新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图(所以当某一个视图修改内容时,另一个是不会跟着改变的,因为是一块独立的内存空间)
-
如果想基于同一段内存,构造不同的视图,我们需要通过传入的参数位另一个 TypedArray 中的 buffer 属性作为参数 → buffer 属性,用于在视图中获取当前底层存储对象 ArrayBuffer
-
const x = new Int8Array([1, 1]); const y = new Int8Array(x.buffer);
-
-
-
4. TypedArray(arrayLikeObject):
接收一个普通数组,生成对应的 TypedArray 视图
-
const typedArray = new Uint8Array([1, 2, 3, 4]); // -- 此 TypedArray 视图会重新开辟内存,不会在原数组的内存上建立视图
-
TypedArray 数组也可以转换回普通数组
-
const normalArray = [...typedArray]; // or const normalArray = Array.from(typedArray); // or const normalArray = Array.prototype.slice.call(typedArray);
-
-
字节序: 字节序指的是数值在内存中的表示方式
-
示例:
-
const int32view = new Int32Array([0, 2, 4, 8]) // --> [0, 2, 4, 8] const int16view = new Int16Array(int32view.buffer) // -- 在上面 32 位整数的视图视图的基础上,创建一个 16 位整数的视图 console.log(int16view) // --> [0, 0, 2, 0, 4, 0, 8, 0]
-
-
我们来思考一下在 int32view 数据上接着建立一个 16 位整数的视图,则读出完全不一样的结果,这是为什么?
1. 由于每个 16 位整数占据 2 个字节,所以整个 int32view 中的 buffer 对象被分成了 8 段 → 由于 x86 体系的计算机都采用小端字节
2. 在小端字节(little endian)中,相对重要的字节会排在后面的地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果,如下图例
3. 所以,也会以 0 2 4 8 进行排列,如果是大端字节时就会以 8 4 2 0 进行排列(对应的 int16view = 80 40 20 00)
-
ArrayBuffer 与字符串的互相转换
实例属性: 原型
-
+ length: 属性返回 TypedArray 由多少个成员 → 注意与 byteLength 属性的区分,前者成员长度,后者字节长度
实例方法: 原型
-
set:
方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存
-
// 复制整个数组 set(arr) const a = new Uint8Array(8); const b = new Uint8Array(8); b.set(a); // -- 将 a 中的内容复制到 b 中的内存中
-
// 复制数组中的某一项 set(arr,index) → 方法还可以接受第二个参数,表示从b对象的哪一个成员开始复制a对象 const a = new Uint16Array(8); const b = new Uint16Array(10); b.set(a, 2) // -- 上面代码的 b 数组比 a 数组多两个成员,所以从 b[2] 开始复制
-
-
slice: TypeArray 实例的 slice 方法,可以返回一个指定位置的新的 TypedArray 实例
实例静态方法
-
of: TypedArray 数组的所有构造函数,都有一个静态方法 of,用于将参数转为一个 TypedArray 实例
-
Float32Array.of(0.151, -8, 3.7) // -- 传入的对应参数,会作为数组的每一项,创建处对应的 TypedArray 对象 // Float32Array [ 0.151, -8, 3.7 ]
-
// 方法一 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;
-
-
from: 静态方法 from 接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的 TypedArray 实例
-
Uint16Array.from([0, 1, 2]) // Uint16Array [ 0, 1, 2 ] 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 数组,拷贝到另一段内存之中,处理之后再将结果转成指定的数组格式
-
复合视图: 由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”
-
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 视图
DataView 视图: 如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作
创建视图
-
const buffer = new ArrayBuffer(24); const dv = new DataView(buffer);
实例读取方法
-
方法 描述 getnt8 读取 1 个字节,返回一个 8 位整数 getUint8 读取 1 个字节,返回一个无符号的 8 位整数 getInt16 读取 2 个字节,返回一个 16 位整数 getUint16 读取 2 个字节,返回一个无符号的 16 位整数 getInt32 读取 4 个字节,返回一个 32 位整数 getUint32 读取 4 个字节,返回一个无符号的 32 位整数 getFloat32 读取 4 个字节,返回一个 32 位浮点数 getFloat64 读取 8 个字节,返回一个 64 位浮点数 -
const buffer = new ArrayBuffer(24); const dv = new DataView(buffer); // 从第1个字节读取一个8位无符号整数 const v1 = dv.getUint8(0); // 从第2个字节读取一个16位无符号整数 const v2 = dv.getUint16(1); // 从第4个字节读取一个16位无符号整数 const v3 = dv.getUint16(3);
-
// 小端字节序 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 位浮点数 -
// 在第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);
-
如果不确定正在使用的计算机的字节序,可以采用下面的判断方式
-
const littleEndian = (function() { const buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true); return new Int16Array(buffer)[0] === 256; })();
-
如果返回 true,就是小端字节序;如果返回 false,就是大端字节序
-