ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。
// DataView视图
const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
console.log(dataView.getUint8(0));
// TypedView视图
const buffer = new ArrayBuffer(12);
const x1 = new Int8Array(buffer);
const x2 = new Uint8Array(buffer);
x1[0] = -1;
// 由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图
console.log(x2[0]); // 255
ArrayBuffer
// ArrayBuffer.prototype.byteLength返回所分配的内存区域的字节长度
// 如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。
if (buffer.byteLength === n) {
// 成功
} else {
// 失败
}
// 拷贝生成一个新的ArrayBuffer对象
// 除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。
const buffer = new ArrayBuffer(8);
const newBuffer = buffer.slice(0, 3);
const buffer = new ArrayBuffer(8);
// 表示参数是否为ArrayBuffer的视图实例
ArrayBuffer.isView(buffer) // false
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
TypedView
ArrayBuffer有两种视图,一种是TypedArray视图,另一种是DataView视图。前者的数组成员都是同一个数据类型,后者的数组成员可以是不同的数据类型。
// 创建一个8字节的ArrayBuffer
const b = new ArrayBuffer(8);
// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
const v1 = new Int32Array(b);
// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
const v2 = new Uint8Array(b, 2);
// 创建一个指向b的Int16视图,开始于字节2,长度为2
const v3 = new Int16Array(b, 2, 2);
const b = new ArrayBuffer(8);
const v1 = new Int32Array(b);
console.log(v1.length) // 2 b占8个字节, Int32Array一个单元在4个字节, 所以length为2
console.log(v1[0]) // 0
console.log(v1[2]) // undefined 下标超出范围为undefined
// new TypedArray(length)
const v1 = new Int32Array(8);
console.log(v1.length); // 8 与通过arryBuffer创建的不一致
普通数组的操作方法和属性,对 TypedArray 数组完全适用。
- TypedArray.prototype.copyWithin(target, start[, end = this.length])
- TypedArray.prototype.entries()
- TypedArray.prototype.every(callbackfn, thisArg?)
- TypedArray.prototype.fill(value, start=0, end=this.length)
- TypedArray.prototype.filter(callbackfn, thisArg?)
- TypedArray.prototype.find(predicate, thisArg?)
- TypedArray.prototype.findIndex(predicate, thisArg?)
- TypedArray.prototype.forEach(callbackfn, thisArg?)
- TypedArray.prototype.indexOf(searchElement, fromIndex=0)
- TypedArray.prototype.join(separator)
- TypedArray.prototype.keys()
- TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
- TypedArray.prototype.map(callbackfn, thisArg?)
- TypedArray.prototype.reduce(callbackfn, initialValue?)
- TypedArray.prototype.reduceRight(callbackfn, initialValue?)
- TypedArray.prototype.reverse()
- TypedArray.prototype.slice(start=0, end=this.length)
- TypedArray.prototype.some(callbackfn, thisArg?)
- TypedArray.prototype.sort(comparefn)
- TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
- TypedArray.prototype.toString()
- TypedArray.prototype.values()
字节序
字节序指的是数值在内存中的表示方式。
小端字节序 和 小端字节序
计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。
举例来说,数值0x2211使用两个字节储存:高位字节是0x22,低位字节是0x11。
- 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
- 小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。
首先我们需要注意两点:
- 我们这里讨论的是如何将一个数值(它的数据类型是确定的, 比如Int16, Uint16, Uint32等等)存储到计算机
- 大端字节序和小端字节序的基本单位是字节, 我们讨论的是字节的排序
关于一个字节的表示方式:
- 二进制: 00010001
- 16进制: 0x11 (我们16进制的一位相当于二进制的4位, 所以用16进制表示一个字节更加简洁)
我们上面提到的, 我们再讨论一个的数据的存储是大端字节序还是小端字节序的前提是我们知道它的数据类型, 我们才能说我们是以一个字节、两个字节、还是4个字节来存储这个数据, 这才涉及到是大端字节序还是小端字节序, 几个例子:
数据: 258 (数据类型为Int16)
|
根据Int16的规定用二进制表示: 0000 | 0001 | 0000 | 0010
|
简化一些, 用16进制表示: Ox0102
|
如果是小端字节序, 那它存储在计算机上就应该为: 0x0201
小技巧:
在js中十进制转2进制: (258).toString(2) // 100000010 在js中十进制转16进制: (258).toString(16) // 102
// 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;
const uInt16View = new Uint16Array(buffer);
// 计算机采用小端字节序
// 所以头两个字节等于258
if (uInt16View[0] === 258) {
console.log('OK'); // "OK"
}
// 赋值运算
// 255 => 0x00FF
uInt16View[0] = 255; // 字节变为[0xFF, 0x00, 0x03, 0x07]
uInt16View[0] = 0xff05; // 字节变为[0x05, 0xFF, 0x03, 0x07]
uInt16View[1] = 0x0210; // 字节变为[0x05, 0xFF, 0x10, 0x02]
buffer: [0x02, 0x01, 0x03, 0x07] uInt16View[0]: 0x0102
uInt16View[0]代表的数占据了两个字节(对应buffer[0], buffer[1]), 计算机采用小端字节序, 它在计算机上存储为0x0201 -> buffer[0]buffer[1], 但是它真正代表的是0x0102, 转换为10进制就为258.
x86 体系的计算机都采用小端字节序, 这并不意味大端字节序不重要,事实上,很多网络设备和特定的操作系统采用的是大端字节序.
下面的函数可以用来判断,当前视图是小端字节序,还是大端字节序。
const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');
function getPlatformEndianness() {
let arr32 = Uint32Array.of(0x12345678);
let arr8 = new Uint8Array(arr32.buffer);
switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8[3])) {
case 0x12345678:
return BIG_ENDIAN;
case 0x78563412:
return LITTLE_ENDIAN;
default:
throw new Error('Unknown endianness');
}
}
ArrayBuffer 与字符串的互相转换
utf8decoder = new TextDecoder('utf8'); //默认为'utf-8'或'utf8'
u8arr = new Uint8Array([240, 160, 174, 183]);
i8arr = new Int8Array([-16, -96, -82, -73]);
u16arr = new Uint16Array([41200, 47022]);
i16arr = new Int16Array([-24336, -18514]);
i32arr = new Int32Array([-1213292304]);
// utf8decoder.decode参数可以是ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array
console.log(utf8decoder.decode(u8arr));
console.log(utf8decoder.decode(i8arr));
console.log(utf8decoder.decode(u16arr));
console.log(utf8decoder.decode(i16arr));
console.log(utf8decoder.decode(i32arr));
const encoder = new TextEncoder()
const view = encoder.encode('€')
console.log(view); // Uint8Array(3) [226, 130, 172]
溢出
不同的视图类型,所能容纳的数值范围是确定的。超出这个范围,就会出现溢出。比如,8 位视图只能容纳一个 8 位的二进制值,如果放入一个 9 位的值,就会溢出。
TypedArray 数组的溢出处理规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。
const uint8 = new Uint8Array(1);
uint8[0] = 256; // 1 0000 0000
// 只会保留后 8 位,即00000000
uint8[0] // 0
// 负数在计算机内部采用“2 的补码”表示,也就是说,将对应的正数值进行否运算,然后加1。比如,-1对应的正值是1,进行否运算以后,得到11111110,再加上1就是补码形式11111111。
uint8[0] = -1;
uint8[0] // 255
一个简单转换规则,可以这样表示。
- 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
- 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值的绝对值,再加上 1。
const int8 = new Int8Array(1);
// 128 -> 10000000
int8[0] = 128;
// int8 10000000 代表-128
int8[0] // -128
// -129 负数在计算机内部采用“2 的补码”表示: 01111111 -> 127
int8[0] = -129;
int8[0] // 127
Uint8ClampedArray视图的溢出规则,与上面的规则不同。它规定,凡是发生正向溢出,该值一律等于当前数据类型的最大值,即 255;如果发生负向溢出,该值一律等于当前数据类型的最小值,即 0。
const uint8c = new Uint8ClampedArray(1);
uint8c[0] = 256;
uint8c[0] // 255
uint8c[0] = -1;
uint8c[0] // 0
复合视图
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);
这种数据结构可以用如下的 C 语言描述:
struct someStruct {
unsigned long id;
char username[16];
float amountDue;
};
DataView视图
如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。
DataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
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);
// 在第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);
二进制数组的应用
AJAX
let xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
// 传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
let arrayBuffer = xhr.response;
// ···
};
xhr.send();
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";
}
}
Canvas
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// 网页Canvas元素输出的二进制像素数据,就是 TypedArray 数组
// 它的视图类型是一种针对Canvas元素的专有类型Uint8ClampedArray
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;
举例来说,如果把像素的颜色值设为Uint8Array类型,那么乘以一个 gamma 值的时候,就必须这样计算:
u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));
但如果是Uint8ClampedArray类型:
pixels[i] *= gamma;
File API
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
const arrayBuffer = reader.result;
// ···
};
假定file变量是一个指向 bmp 文件的文件对象:
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = processimage;
function processimage(e) {
const buffer = e.target.result;
const datav = new DataView(buffer);
const bitmap = {};
// 具体的处理步骤
// 具体处理图像数据时,先处理 bmp 的文件头。具体每个文件头的格式和定义,请参阅有关资料。
bitmap.fileheader = {};
bitmap.fileheader.bfType = datav.getUint16(0, true);
bitmap.fileheader.bfSize = datav.getUint32(2, true);
bitmap.fileheader.bfReserved1 = datav.getUint16(6, true);
bitmap.fileheader.bfReserved2 = datav.getUint16(8, true);
bitmap.fileheader.bfOffBits = datav.getUint32(10, true);
// 接着处理图像元信息部分。
bitmap.infoheader = {};
bitmap.infoheader.biSize = datav.getUint32(14, true);
bitmap.infoheader.biWidth = datav.getUint32(18, true);
bitmap.infoheader.biHeight = datav.getUint32(22, true);
bitmap.infoheader.biPlanes = datav.getUint16(26, true);
bitmap.infoheader.biBitCount = datav.getUint16(28, true);
bitmap.infoheader.biCompression = datav.getUint32(30, true);
bitmap.infoheader.biSizeImage = datav.getUint32(34, true);
bitmap.infoheader.biXPelsPerMeter = datav.getUint32(38, true);
bitmap.infoheader.biYPelsPerMeter = datav.getUint32(42, true);
bitmap.infoheader.biClrUsed = datav.getUint32(46, true);
bitmap.infoheader.biClrImportant = datav.getUint32(50, true);
// 最后处理图像本身的像素信息。
const start = bitmap.fileheader.bfOffBits;
bitmap.pixels = new Uint8Array(buffer, start);
// 至此,图像文件的数据全部处理完成。下一步,可以根据需要,进行图像变形,或者转换格式,或者展示在Canvas网页元素之中。
}
ArrayBuffer vs Blob
Blob 对象表示一个二进制文件的数据内容,比如一个图片文件的内容就可以通过 Blob 对象读写。它通常用来读写文件,它的名字是 Binary Large Object (二进制大型对象)的缩写。它与 ArrayBuffer 的区别在于,它用于操作二进制文件,而 ArrayBuffer 用于操作内存。
关于Blob, 请访问Blob 对象