ArrayBuffer
对象用来表示通用的原始二进制数据缓冲区。它可以用作存储各种类型的数据,图像、音频和视频数据。
也可以理解为它开辟了一片固定大小的内存区域(即数组缓冲区:代表内存之中的一段二进制数据;仅是一个个 0/1 组成的串;数据放在栈中)
缓冲区:又叫缓存,是内存空间的一部分,相当于一种媒介,介于输入和输出之间。缓冲区可以放置整型,浮点数以及别的类型。因此JavaScript提供了一种可以缓冲区的类型——Arraybuffer。
它是一个字节数组,通常在其他语言中称为“byte array”。跟其它 JavaScript 数组差不多,但是不是所有 JavaScript 类型都可以放进去,比如对象、字符串。唯一可以放进去的只有字节(可以用数字表示)。
但并不是直接把这个字节到 ArrayBuffer 里就行了,ArrayBuffer 并不知道字节有多长,该用多少位去存,它不知道第一个元素和第二个元素的分割点。我们也不能直接读写 ArrayBuffer
中的内容。而是要通过 TypedArray视图(类型化数组对象)或 DataView视图 来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
下面会详细说一下ArrayBuffer,TypedArray,DataView的构造函数,属性,实例方法等。
一. ArrayBuffer对象
1.构造函数
ArrayBuffer(length, options):用来分配一段可以存放数据的连续内存区域
length:要创建的数组缓冲区的大小(以字节为单位)
options(可选):包含 maxByteLength 属性的一个对象
const buffer = new ArrayBuffer(32);// 生成一段 32 字节的内存区域,每个字节的值默认都是0
//创建一个8字节的缓冲区,可以调整到的最大长度为16字节,然后使用resize()调整到12字节
const buffer1 = new ArrayBuffer(8, { maxByteLength: 16 });
buffer1.resize(12);
2.实例属性
ArrayBuffer.prototype.byteLength :返回所分配的内存区域的字节长度
目的:要分配的内存区域很大,如果没有那么多的连续空余内存,可能会分配失败,所以有必要去检查是否分配成功。
const buff = new ArrayBuffer(32);
console.log(buff.byteLength);//32
3.实例方法
ArrayBuffer.prototype.slice(start, end) :返回一个新的实例,其包含原 ArrayBuffer
实例中从 start 开始(包含)到 end
结束(不含)的所有字节的副本
start:拷贝开始的字节序号(含该字节)
end:拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。
const buff = new ArrayBuffer(32);
const newBuffer = buff.slice(0, 3);
// 拷贝buff对象的前3个字节(从0开始,到第3个字节前面结束),生成一个新的ArrayBuffer对象。
slice方法包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法
ArrayBuffer.prototype.resize() :上述提到的将 ArrayBuffer
调整为指定的大小,以字节为单位
4.静态方法
ArrayBuffer.isView() :
ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例,这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。
ArrayBuffer.isView(); // false
ArrayBuffer.isView([]); // false
ArrayBuffer.isView({}); // false
ArrayBuffer.isView(null); // false
ArrayBuffer.isView(undefined); // false
ArrayBuffer.isView(new ArrayBuffer(10)); // false
ArrayBuffer.isView(new Uint8Array()); // true
ArrayBuffer.isView(new Float32Array()); // true
const buffer = new ArrayBuffer(2);
ArrayBuffer.isView(buffer) // false
const dv = new DataView(buffer);
const dv1 = new Int32Array(buffer);
ArrayBuffer.isView(dv); // true
ArrayBuffer.isView(dv1); // true
二、类型化数组TypedArray
一个 TypedArray 描述了底层二进制数据缓冲区的类数组视图。所有的类型化数组都是基于 TypedArray 进行操作的,数组成员都是同一个数据类型。
1.类型化数组定义与构造
类型化数组:并不是真正的数组,而是类似数组的对象,是建立在ArrayBuffer对象的基础上的。它提供了一种用于访问原始二进制数据的机制。为了达到最大的灵活性和效率,类型数组将实现拆分为 缓冲 和 视图 两部分,一个缓冲描述的是一个数据块,缓冲没有格式可言,并且不提供机制访问内容。
(1)TypedArray视图与普通数组的区别:
①TypedArray数组的成员都是同一种类型
②TypedArray数组的成员是连续的,不会有空位
③TypedArray数组成员的默认值为0,比如,new Array(10)返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)返回一个 TypedArray 数组,里面 10 个成员都是 0。
④TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。
其余的和普通数组一样,普通数组的属性和方法都适用。
(2)构造函数—TypedArray(buffer, byteOffse, length)
第一个参数 buffer:视图对应的底层ArrayBuffer对象(必填)
第二个参数 byteOffset:视图开始的字节序号,默认从 0 开始
第三个参数 length:视图包含的数据个数,默认直到本段内存区域结束
但TypedArray不能被直接实例化(通过new构造),可以使用它的子类创建实例。子类有其下:
同一个ArrayBuffer对象上可以建立多个视图。
// 创建一个8字节的ArrayBuffer
const buffer = new ArrayBuffer(8);
// 创建一个指向buffer的Int32视图,开始于字节0,直到缓冲区的末尾
const v1 = new Int32Array(buffer);
// 创建一个指向buffer的Uint8视图,开始于字节2,直到缓冲区的末尾
const v2 = new Uint8Array(buffer, 2);
// 创建一个指向buffer的Int16视图,开始于字节2,长度为2
const v3 = new Int16Array(buffer, 2, 2);
console.log(v1)
//Int32Array(2) [0, 0, buffer: ArrayBuffer(8), byteLength: 8, byteOffset: 0, length: 2, Symbol(Symbol.toStringTag): 'Int32Array']
console.log(v2)
//Uint8Array(6) [0, 0, 0, 0, 0, 0, buffer: ArrayBuffer(8), byteLength: 6, byteOffset: 2, length: 6, Symbol(Symbol.toStringTag): 'Uint8Array']
console.log(v3)
//Int16Array(2) [0, 0, buffer: ArrayBuffer(8), byteLength: 4, byteOffset: 2, length: 2, Symbol(Symbol.toStringTag): 'Int16Array']
上述生成的三个视图表示对同一段内存数据的不同操作方式
V1,v2,v3是重叠的,
V1: Int32Array:成员是32位有符号整数,长度4个字节。内存buffer生成了2个成员。
V2: Uint8Array: 成员是8位无符号整数,长度1个字节,开始于字节2。内存buffer生成了6成员。
V3: Int16Array: 成员是16位有符号整数,长度2个字节,开始于字节2,长度为2。buffer生成了2个成员。
这里要注意, byteOffset
必须是新的类型化数组元素大小的整数倍,否则会报错。换句话说,偏移量必须是 BYTES_PER_ELEMENT
的倍数。后续会说到BYTES_PER_ELEMENT
属性。
const buffer = new ArrayBuffer(8);
const i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2
上面代码中,新生成一个 8 个字节的ArrayBuffer
对象,然后在这个对象的第一个字节,建立带符号的 16 位整数视图,结果报错。因为,带符号的 16 位整数需要两个字节,所以byteOffset
参数必须能够被 2 整除。
(3)构造函数—TypedArray(length)
length: 成员的个数
视图还可以不通过ArrayBuffer
对象,直接分配内存而生成。
const f64a = new Float64Array(8);//8个0
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];//10,20,30,0,0,0,0,0
上面代码生成一个 8 个成员的Float64Array
数组(共 64 字节),每个成员8字节,然后依次对每个成员赋值。可以看到,视图数组的赋值操作与普通数组的操作毫无两样。
注意:ArrayBuffer
构造函数的参数是所需要的内存大小(单位字节)。TypedArray构造函数的参数是指包含数组成员的个数(单位 位)
(4)构造函数—TypedArray(typedArray)
TypedArray 数组的构造函数,可以接受另一个TypedArray
实例作为参数。
const typedArray1 = new Int8Array(8);
typedArray1[0] = 32;
const typedArray2 = new Int8Array(typedArray1);
typedArray2[1] = 42;
console.log(typedArray1);
// Expected output: Int8Array [32, 0, 0, 0, 0, 0, 0, 0]
console.log(typedArray2);
// Expected output: Int8Array [32, 42, 0, 0, 0, 0, 0, 0]
typedArray1[0] = 30
console.log(typedArray1[0]);// 30
console.log(typedArray2[0]);// 32
注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
上面代码中,数组typedArray2是以数组typedArray1为模板而生成的,当typedArray1变动的时候,typedArray2并没有变动。
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。
const typedArray1 = new Int8Array(8);
typedArray1[0] = 32;
const typedArray2 = new Int8Array(typedArray1.buffer);
typedArray2[1] = 42;
console.log(typedArray1);
// Expected output: Int8Array [32, 42, 0, 0, 0, 0, 0, 0]
console.log(typedArray2);
// Expected output: Int8Array [32, 42, 0, 0, 0, 0, 0, 0]
注意:ArrayBuffer
传递给 TypedArray
构造函数的 byteLength
属性必需是构造函数 BYTES_PER_ELEMENT
的倍数。
const i32 = new Int32Array(new ArrayBuffer(3));
// RangeError: byte length of Int32Array should be a multiple of 4
const i32 = new Int32Array(new ArrayBuffer(4));
(5) 构造函数—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);
2.静态属性:TypedArray.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
3.静态方法:
TypedArray.of()
TypedArray 数组的所有构造函数,都有一个静态方法of
,用于将参数转为一个TypedArray实例,此方法几乎与 Array.of()相同。
Uint8Array.of(1); // Uint8Array [ 1 ]
Int8Array.of("1", "2", "3"); // Int8Array [ 1, 2, 3 ]
Float32Array.of(1, 2, 3); // Float32Array [ 1, 2, 3 ]
Int16Array.of(undefined); // IntArray [ 0 ]
TypedArray.from()
从一个类数组或者可迭代对象中创建一个新类型数组。这个方法和 Array.from()类似。
// 使用 Set (可迭代对象)
var s = new Set([1, 2, 3]);
Uint8Array.from(s);
// Uint8Array [ 1, 2, 3 ]
// 使用字符串
Int16Array.from("123");
// Int16Array [ 1, 2, 3 ]
// 使用箭头函数对数组元素进行映射
Float32Array.from([1, 2, 3], (x) => x + x);
// Float32Array [ 2, 4, 6 ]
// 生成一个数字序列
Uint8Array.from({ length: 5 }, (v, k) => k);
// Uint8Array [ 0, 1, 2, 3, 4 ]
4.实例属性:
TypedArray.prototype.buffer
TypedArray
实例的buffer
属性,返回整段内存区域对应的ArrayBuffer
对象。它是一个访问器属性,它的 set 访问器函数是 undefined
,意思是你只能够读取这个属性。它的值在TypedArray
构造时建立,不能被修改。
var buffer = new ArrayBuffer(8);
var uint16 = new Uint16Array(buffer);
uint16.buffer; // ArrayBuffer { byteLength: 8 }
TypedArray.prototype.byteLength
byteLength
访问器属性表示类型化数组的长度(字节数)。也是只能读取,不能被修改。
var buffer = new ArrayBuffer(8);
var uint8 = new Uint8Array(buffer);
uint8.byteLength; // 8 (符合 buffer 的 byteLength)
var uint8 = new Uint8Array(buffer, 1, 5);
uint8.byteLength; // 5 (在 Uint8Array 构造时指定)
var uint8 = new Uint8Array(buffer, 2);
uint8.byteLength; // 6 (根据被构造的 Uint8Array 的 offset)
TypedArray.prototype.byteOffset
byteOffset
访问器属性表示类型化数组距离其ArrayBuffer
起始位置的偏移(字节数)。只能读取,不能被修改。
var buffer = new ArrayBuffer(8);
var uint8 = new Uint8Array(buffer);
uint8.byteOffset; // 0 (没有指定 oddfet)
var uint8 = new Uint8Array(buffer, 3);
uint8.byteOffset; // 3 (在构造 Uint8Array 时指定)
TypedArray.prototype.length
TypedArray
实例的 length
访问器属性返回该类型化数组的长度(以元素为单位)。
也就是 TypedArray 数组含有多少个成员。注意将byteLength
属性和length
属性区分,前者是字节长度,后者是成员长度。
const a = new Int16Array(8);
a.length // 8
a.byteLength // 16
5.实例方法
TypedArray.prototype.set()
TypedArray 数组的set
方法用于复制数组(参数为普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。
var buffer = new ArrayBuffer(8);
var uint8 = new Uint8Array(buffer);
uint8.set([1, 2, 3], 3);
console.log(uint8); // Uint8Array [ 0, 0, 0, 1, 2, 3, 0, 0 ]
TypedArray.prototype.subarray()
返回一个新的、基于相同 ArrayBuffer
、元素类型也相同的类型化数组。开始的索引将会被包括,而结束的索引将不会被包括。
var buffer = new ArrayBuffer(8);
var uint8 = new Uint8Array(buffer);
uint8.set([1, 2, 3]);
console.log(uint8); // Uint8Array [ 1, 2, 3, 0, 0, 0, 0, 0 ]
var sub = uint8.subarray(0, 4);
console.log(sub); // Uint8Array [ 1, 2, 3, 0 ]
TypedArray.prototype.slice(begin, end)
方法将一个类型化数组的一部分浅拷贝到一个新的类型化数组对象中并返回。
begin 可选
从 0 开始的索引位置。可以使用负值索引,表示从数组末尾往前的偏移量。slice(-2)
表示提取数组中的末尾两个元素。如果没有设定起始位置,则将从开始位置开始截取
end 可选
从 0 开始到尾元素前的索引值。 slice
取出的元素到此位置之前,不包含该位置。例,slice(1,4)
表示读取第 2 个元素到第 4 个元素 (元素索引:1, 2, 3)。可以使用负值索引,表示从数组末尾往前的偏移量。 slice(2,-1)
表示取出数组中的第 3 个到倒数第 2 个元素。如果没有设定结束位置,则将从开始位置截取到序列尾部。(默认值为typedarray.length
)
const uint8 = new Uint8Array([1, 2, 3]);
uint8.slice(1); // Uint8Array [ 2, 3 ]
uint8.slice(2); // Uint8Array [ 3 ]
uint8.slice(-2); // Uint8Array [ 2, 3 ]
uint8.slice(0, 1); // Uint8Array [ 1 ]
三、复合视图
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
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);
new ArrayBuffer()构造函数分配一段指定字节的内存空间,new TypeArray()用来生成对应类型的数据实例存到内存空间中。
上面代码将一个 24 字节长度的ArrayBuffer
对象,分成三个部分:
- 字节 0 到字节 3:1 个 32 位无符号整数
- 字节 4 到字节 19:16 个 8 位整数
- 字节 20 到字节 23:1 个 32 位浮点数
这种数据结构可以用如下的 C 语言描述:
struct someStruct { unsigned long id; char username[16]; float amountDue; };
四、DataView
如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer
对象的复合视图以外,还可以通过DataView
视图进行操作。
DataView
视图提供更多操作选项,而且支持设定字节序。(因为如果一段数据是大端字节序,TypedArray 数组只能处理小端字节序,所以将无法正确解析,为了解决这个问题,JavaScript 引入DataView
对象,可以设定字节序)
1.构造函数
new DataView(buffer, byteOffset, byteLength)
byteOffset:新视图引用的上述缓冲区中第一个字节的偏移量(以字节为单位)。如果未指定,缓冲区视图将从第一个字节开始。
byteLength:字节数组中的元素数。如果未指定,视图的长度将与缓冲区的长度匹配。
const buffer = new ArrayBuffer(24);
const view = new DataView(buffer);
2.实例属性
DataView.prototype.buffer
:返回对应的 ArrayBuffer 对象
DataView.prototype.byteLength
:返回占据的内存字节长度
DataView.prototype.byteOffset
:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始
3.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
方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。
// 从第一个字节开始读取8位无符号整数
const v1 = view.getUint8(0);
// 从第2个字节开始读取16位有符号整数,占2个字节
const v2 = view.getInt16(1);
// 从第4个字节开始读取16位有符号整数,2个字节
const v3 = view.getInt16(3);
//读取了ArrayBuffer对象的前 5 个字节,其中有一个 8 位整数和两个十六位整数。
如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。
默认情况下,DataView
的get
方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get
方法的第二个参数指定true
。
// 小端字节序
const v1 = view.getUint16(1, true);
// 大端字节序
const v2 = view.getUint16(3, false);
// 大端字节序
const v3 = view.getUint16(3);
4.DataView 的写入
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位整数
view.setInt32(0, 25, false);
// 在第5个字节,以大端字节序写入值为25的32位整数
view.setInt32(4, 25);
// 在第9个字节,以小端字节序写入值为2.5的32位浮点数
view.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,就是大端字节序。
五、应用
AJAX
传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType
属性默认为text
。XMLHttpRequest
第二版XHR2
允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType
)设为arraybuffer
;如果不知道,就设为blob
。
let xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
let arrayBuffer = xhr.response;
// ···
};
xhr.send();
如果知道传回来的是 32 位整数,可以像下面这样处理。
xhr.onreadystatechange = function () {
if (req.readyState === 4 ) {
const arrayResponse = xhr.response;
const dataView = new DataView(arrayResponse);
const ints = new Uint32Array(dataView.byteLength / 4);
xhrDiv.style.backgroundColor = "#00FF00";
xhrDiv.innerText = "Array is " + ints.length + "uints long";
}
}
WebSocket
WebSocket
可以通过ArrayBuffer
,发送或接收二进制数据。
let socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';
socket.addEventListener('open', function (event) {
const typedArray = new Uint8Array(4);
socket.send(typedArray.buffer);
});
socket.addEventListener('message', function (event) {
const arrayBuffer = event.data;
// ···
});