作者:阮一峰
本文仅用于学习记录,不存在任何商业用途,如侵删
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
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
对象,分成三个部分:
这种数据结构可以用如下的 C 语言描述:
struct someStruct {
unsigned long id;
char username[16];
float amountDue;
};
如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer
对象的复合视图以外,还可以通过DataView
视图进行操作。
DataView
视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer
对象的各种TypedArray
视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView
视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
DataView
视图本身也是构造函数,接受一个ArrayBuffer
对象作为参数,生成视图。
new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
下面是一个例子。
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
DataView
实例有以下属性,含义与TypedArray
实例的同名方法相同。
DataView.prototype.buffer
:返回对应的 ArrayBuffer 对象DataView.prototype.byteLength
:返回占据的内存字节长度DataView.prototype.byteOffset
:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始DataView
实例提供 8 个方法读取内存。
这一系列get
方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。
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);
上面代码读取了ArrayBuffer
对象的前 5 个字节,其中有一个 8 位整数和两个十六位整数。
如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。默认情况下,DataView
的get
方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get
方法的第二个参数指定true
。
// 小端字节序
const v1 = dv.getUint16(1, true);
// 大端字节序
const v2 = dv.getUint16(3, false);
// 大端字节序
const v3 = dv.getUint16(3);
DataView 视图提供 8 个方法写入内存。
这一系列set
方法,接受两个参数,第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据。对于那些写入两个或两个以上字节的方法,需要指定第三个参数,false
或者undefined
表示使用大端字节序写入,true
表示使用小端字节序写入。
// 在第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
,就是大端字节序。
大量的 Web API 用到了ArrayBuffer
对象和它的视图对象。
传统上,服务器通过 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";
}
}
网页Canvas
元素输出的二进制像素数据,就是 TypedArray 数组。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;
需要注意的是,上面代码的uint8ClampedArray
虽然是一个 TypedArray 数组,但是它的视图类型是一种针对Canvas
元素的专有类型Uint8ClampedArray
。这个视图类型的特点,就是专门针对颜色,把每个字节解读为无符号的 8 位整数,即只能取值 0 ~ 255,而且发生运算的时候自动过滤高位溢出。这为图像处理带来了巨大的方便。
举例来说,如果把像素的颜色值设为Uint8Array
类型,那么乘以一个 gamma 值的时候,就必须这样计算:
u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));
因为Uint8Array
类型对于大于 255 的运算结果(比如0xFF+1
),会自动变为0x00
,所以图像处理必须要像上面这样算。这样做很麻烦,而且影响性能。如果将颜色值设为Uint8ClampedArray
类型,计算就简化许多。
pixels[i] *= gamma;
Uint8ClampedArray
类型确保将小于 0 的值设为 0,将大于 255 的值设为 255。注意,IE 10 不支持该类型。
WebSocket
可以通过ArrayBuffer
,发送或接收二进制数据。
let socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';
// Wait until socket is open
socket.addEventListener('open', function (event) {
// Send binary data
const typedArray = new Uint8Array(4);
socket.send(typedArray.buffer);
});
// Receive binary data
socket.addEventListener('message', function (event) {
const arrayBuffer = event.data;
// ···
});
Fetch API 取回的数据,就是ArrayBuffer
对象。
fetch(url)
.then(function(response){
return response.arrayBuffer()
})
.then(function(arrayBuffer){
// ...
});
如果知道一个文件的二进制数据类型,也可以将这个文件读取为ArrayBuffer
对象。
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
const arrayBuffer = reader.result;
// ···
};
下面以处理 bmp 文件为例。假定file
变量是一个指向 bmp 文件的文件对象,首先读取文件。
const reader = new FileReader();
reader.addEventListener("load", processimage, false);
reader.readAsArrayBuffer(file);
然后,定义处理图像的回调函数:先在二进制数据之上建立一个DataView
视图,再建立一个bitmap
对象,用于存放处理后的数据,最后将图像展示在Canvas
元素之中。
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
网页元素之中。