ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口。

二进制数组由三类对象组成。
(1)ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
(2)TypedArray视图:共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。
(3)DataView视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。

简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。

ArrayBuffer对象

ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。

ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。

  1. const buf = new ArrayBuffer(32); //生成了一段32个字节的内存区域,每个字节的值默认都是 0
  2. const dataView = new DataView(buf); // 为了读写这段内容,需要为它指定视图
  3. // 然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据
  4. dataView.getUint8(0) // 0

另一种TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。

  1. const buffer = new ArrayBuffer(12);
  2. const x1 = new Int32Array(buffer);
  3. x1[0] = 1;
  4. const x2 = new Uint8Array(buffer);
  5. x2[0] = 2;
  6. x1[0] // 2

上面代码对同一段内存,分别建立两种视图:由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图。

TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。

  1. const typedArray = new Uint8Array([0,1,2]);
  2. typedArray.length // 3
  3. typedArray[0] = 5;
  4. typedArray // [5, 1, 2]

ArrayBuffer.prototype.byteLength

可以用来检查是否分配成功

  1. const buffer = new ArrayBuffer(32);
  2. if (buffer.byteLength === 32) {
  3. // 成功
  4. } else {
  5. // 失败
  6. }

ArrayBuffer.prototype.slice()

ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。
slice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。

  1. const buffer = new ArrayBuffer(8);
  2. const newBuffer = buffer.slice(0, 3);

第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。
除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。

ArrayBuffer.isView()

ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。

  1. const buffer = new ArrayBuffer(8);
  2. ArrayBuffer.isView(buffer) // false
  3. const v = new Int32Array(buffer);
  4. ArrayBuffer.isView(v) // true

TypedArray 视图

ArrayBuffer对象作为内存区域,可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,这就叫做“视图”(view)。ArrayBuffer有两种视图,一种是TypedArray视图,另一种是DataView视图。前者的数组成员都是同一个数据类型,后者的数组成员可以是不同的数据类型。

TypedArray视图支持的数据类型一共有 9 种(DataView视图支持除Uint8C以外的其他 8 种)。

数据类型 字节长度 含义 对应的 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

这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,都能用方括号运算符([])获取单个元素,所有数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差异主要在以下方面。

  • TypedArray 数组的所有成员,都是同一种类型。
  • TypedArray 数组的成员是连续的,不会有空位。
  • TypedArray 数组成员的默认值为 0。比如,new Array(10)返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)返回一个 TypedArray 数组,里面 10 个成员都是 0。
  • TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。

DataView

如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。
DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。

  1. new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);

DataView实例有以下属性,含义与TypedArray实例的同名方法相同。

  • DataView.prototype.buffer:返回对应的 ArrayBuffer 对象
  • DataView.prototype.byteLength:返回占据的内存字节长度
  • DataView.prototype.byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始

DataView实例提供 8 个方法读取内存。

  • getInt8:读取 1 个字节,返回一个 8 位整数。
  • getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
  • getInt16:读取 2 个字节,返回一个 16 位整数。
  • getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
  • getInt32:读取 4 个字节,返回一个 32 位整数。
  • getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
  • getFloat32:读取 4 个字节,返回一个 32 位浮点数。
  • getFloat64:读取 8 个字节,返回一个 64 位浮点数。

AJAX

传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为textXMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob

  1. let xhr = new XMLHttpRequest();
  2. xhr.open('GET', someUrl);
  3. xhr.responseType = 'arraybuffer';
  4. xhr.onload = function () {
  5. let arrayBuffer = xhr.response;
  6. // ···
  7. };
  8. xhr.send();

如果知道传回来的是 32 位整数,可以像下面这样处理。

  1. xhr.onreadystatechange = function () {
  2. if (req.readyState === 4 ) {
  3. const arrayResponse = xhr.response;
  4. const dataView = new DataView(arrayResponse);
  5. const ints = new Uint32Array(dataView.byteLength / 4);
  6. xhrDiv.style.backgroundColor = "#00FF00";
  7. xhrDiv.innerText = "Array is " + ints.length + "uints long";
  8. }
  9. }

Canvas

  1. const canvas = document.getElementById('myCanvas');
  2. const ctx = canvas.getContext('2d');
  3. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  4. const uint8ClampedArray = imageData.data;

WebSocket

WebSocket可以通过ArrayBuffer,发送或接收二进制数据。

  1. let socket = new WebSocket('ws://127.0.0.1:8081');
  2. socket.binaryType = 'arraybuffer';
  3. // Wait until socket is open
  4. socket.addEventListener('open', function (event) {
  5. // Send binary data
  6. const typedArray = new Uint8Array(4);
  7. socket.send(typedArray.buffer);
  8. });
  9. // Receive binary data
  10. socket.addEventListener('message', function (event) {
  11. const arrayBuffer = event.data;
  12. // ···
  13. });

Fetch API

Fetch API 取回的数据,就是ArrayBuffer对象。

  1. fetch(url)
  2. .then(function(response){
  3. return response.arrayBuffer()
  4. })
  5. .then(function(arrayBuffer){
  6. // ...
  7. });

File API

如果知道一个文件的二进制数据类型,也可以将这个文件读取为ArrayBuffer对象。

  1. const fileInput = document.getElementById('fileInput');
  2. const file = fileInput.files[0];
  3. const reader = new FileReader();
  4. reader.readAsArrayBuffer(file);
  5. reader.onload = function () {
  6. const arrayBuffer = reader.result;
  7. // ···
  8. };

SharedArrayBuffer

JavaScript 是单线程的,Web worker 引入了多线程:主线程用来与用户互动,Worker 线程用来承担计算任务。每个线程的数据都是隔离的,通过postMessage()通信。下面是一个例子。

  1. // 主线程
  2. const w = new Worker('myworker.js');

上面代码中,主线程新建了一个 Worker 线程。该线程与主线程之间会有一个通信渠道,主线程通过w.postMessage向 Worker 线程发消息,同时通过message事件监听 Worker 线程的回应。

  1. // 主线程
  2. w.postMessage('hi');
  3. w.onmessage = function (ev) {
  4. console.log(ev.data);
  5. }

上面代码中,主线程先发一个消息hi,然后在监听到 Worker 线程的回应后,就将其打印出来。
Worker 线程也是通过监听message事件,来获取主线程发来的消息,并作出反应。

  1. // Worker 线程
  2. onmessage = function (ev) {
  3. console.log(ev.data);
  4. postMessage('ho');
  5. }

ES2017 引入SharedArrayBuffer,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer的 API 与ArrayBuffer一模一样,唯一的区别是后者无法共享数据。

  1. // 主线程
  2. // 新建 1KB 共享内存
  3. const sharedBuffer = new SharedArrayBuffer(1024);
  4. // 主线程将共享内存的地址发送出去
  5. w.postMessage(sharedBuffer);
  6. // 在共享内存上建立视图,供写入数据
  7. const sharedArray = new Int32Array(sharedBuffer);

上面代码中,postMessage方法的参数是SharedArrayBuffer对象。
Worker 线程从事件的data属性上面取到数据。

  1. // Worker 线程
  2. onmessage = function (ev) {
  3. // 主线程共享的数据,就是 1KB 的共享内存
  4. const sharedBuffer = ev.data;
  5. // 在共享内存上建立视图,方便读写
  6. const sharedArray = new Int32Array(sharedBuffer);
  7. // ...
  8. };

共享内存也可以在 Worker 线程创建,发给主线程。
SharedArrayBufferArrayBuffer一样,本身是无法读写的,必须在上面建立视图,然后通过视图读写。

  1. // 分配 10 万个 32 位整数占据的内存空间
  2. const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
  3. // 建立 32 位整数视图
  4. const ia = new Int32Array(sab); // ia.length == 100000
  5. // 新建一个质数生成器
  6. const primes = new PrimeGenerator();
  7. // 将 10 万个质数,写入这段内存空间
  8. for ( let i=0 ; i < ia.length ; i++ )
  9. ia[i] = primes.next();
  10. // 向 Worker 线程发送这段共享内存
  11. w.postMessage(ia);

Worker 线程收到数据后的处理如下。

  1. // Worker 线程
  2. let ia;
  3. onmessage = function (ev) {
  4. ia = ev.data;
  5. console.log(ia.length); // 100000
  6. console.log(ia[37]); // 输出 163,因为这是第38个质数
  7. };

Atomics 对象

多线程共享内存,最大的问题就是如何防止两个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。