跳转到内容

ArrayBuffer

ArrayBuffer 对象是 JavaScript 操作二进制数据的接口之一,以数组的形式处理二进制数据,被统称为二进制数组。

二进制数组由3类对象组成:

  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 是一个构造函数,用来生成 ArrayBuffer 实例。

ArrayBuffer 的参数(即内存大小)单位是字节(byte),1 个字节是 8 位(bit)。

javascript
const buffer = new ArrayBuffer(8);

上面代码生成了一段 8 字节的内存,每个字节的值默认都是 0。

ArrayBuffer 实例的 byteLength 属性,表示该对象所包含的二进制数据的大小(即字节长度)。

javascript
const buffer = new ArrayBuffer(32);
buffer.byteLength // 32

想要读写这个ArrayBuffer 对象,需要指定一个视图,创建 DataView 视图,需要提供 ArrayBuffer 对象实例作为参数。

javascript
const buffer = new ArrayBuffer(32);
const view = new DataView(buffer);
view.getInt8(0) // 0 读取 1 个字节,返回一个 8 位整数。

TypedArray 视图与 DataView 视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的视图。比如,Uint8Array(无符号 8 位整数)数组视图,Int16Array(16 位整数)数组视图,Float32Array(32 位浮点数)数组视图等等。

javascript
const buffer = new ArrayBuffer(16);
// 创建一个 8 字节的 ArrayBuffer
const x1 = new Int8Array(buffer);
// 创建一个指向该 ArrayBuffer 的 Int8Array 视图
const x2 = new Uint8Array(buffer);
// 创建一个指向该 ArrayBuffer 的 Uint8Array 视图

上面代码对同一段内存,分别创建了两种视图。注意,同一个 ArrayBuffer 实例,可以根据不同的数据类型,生成不同的视图。

TypedArray 视图的构造函数可以接受一个表示长度的数值,或者一个类数组对象(array-like object)作为参数,直接分配内存生成底层的 ArrayBuffer 实例,同时完成对这段内存的赋值。 。

javascript
var typedArray = new Uint8Array([0, 1, 2]); 
typedArray.length // 3 
typedArray[O] = 5; 
typedArray // [5 , 1, 2 ]

ArrayBuffer.prototype.bytelength

ArrayBuffer 实例的 byteLength 属性返回分配的内存区域的字节长度。可以通过这个属性检查内存分配是否成功。

js
var buffer = new ArrayBuffer(32);
console.log(buffer.byteLength); // 输出: 32

如果尝试分配的内存区域过大,可能会因为缺乏连续的空余内存而分配失败,因此需要检查 byteLength 是否等于请求的字节长度来确认分配是否成功。

js
var buffer = new ArrayBuffer(32);
if (buffer.byteLength === 32) {
  // 成功
} else {
  // 失败
}

ArrayBuffer.prototype.slice()

ArrayBuffer 实例的 slice() 方法允许复制内存区域的一部分,生成一个新的 ArrayBuffer 对象。这个方法接受两个参数,分别表示复制开始和结束的字节序号。

js
var buffer = new ArrayBuffer(8);
var newBuffer = buffer.slice(0, 3);

ArrayBuffer.isView()

这是一个静态方法,返回一个布尔值,表示传入的参数是否为 ArrayBuffer 的视图实例,即是否为 TypedArray 实例或 DataView 实例。

js
var buffer = new ArrayBuffer(8);
console.log(ArrayBuffer.isView(buffer)); // 输出: false
var v = new Int32Array(buffer);
console.log(ArrayBuffer.isView(v)); // 输出: true

TypedArray视图

ArrayBuffer 是用于表示二进制数据缓冲区的类,可以存放多种类型的数据。TypedArray 是 ArrayBuffer 的一种视图,允许以特定格式读取 ArrayBuffer 中的数据。TypedArray 包括多种类型,如 Int8Array、Uint8Array、Float32Array 等。

TypedArray 提供多种构造函数,可以通过 ArrayBuffer 创建视图,也可以直接分配内存生成实例。构造函数可以接受 ArrayBuffer 对象、字节偏移量和长度等参数。

  • TypedArray.prototype.entries()
  • TypedArray.prototype.every(callbackfn, thisArg?)
  • TypedArray.prototype.fill(value, start=O, end=this.length)
  • TypedArray.pototype.filter(callbackfn, thisArg?)
  • TypedArray.prototype.find(predicate, thisArg?)
  • TypedArray.prototype.findindex(predicate, thisArg?)
  • TypedArray.prototype.forEach(callbackfn, thisArg?)
  • TypedArray.prototype.indexOf(searchElement, fromindex=O)
  • TypedArray.prototype.join(separator)
  • TypedArray.prototype.keys()
  • TypedArray.prototype.lastindexOf(searchElement, fromindex?)
  • TypedArray.prototype map(callbackf thisArg?)
  • TypedArray.prototype.reduce(callbackfn, initialValue?)
  • TypedArray.prototype.reduceRight(callbackfn, initialValue?)
  • TypedArray.prototype.reverse()
  • TypedArray.prototype.slice(start=O, end=this.length)
  • TypedArray.prototype.some(callbackfn, thisArg?)
  • TypedArray.prototype.sort(comparefn)
  • TypedArray.prototype.toLocaleString(reservedl?, reserved2?)
  • TypedArray.prototype.toString()
  • TypedArray.prototype.values()

字节序

TypedArray 数组采用本机操作系统设定的字节序读写数据,通常是小端字节序。

js
var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
}

BYTES_PER_ELEMENT 属性

每种 TypedArray 构造函数都有一个 BYTES_PER_ELEMENT 属性,表示数据类型占据的字节数。

js
Int8Array.BYTES_PER_ELEMENT // 1
Uint16Array.BYTES_PER_ELEMENT // 2
Float64Array.BYTES_PER_ELEMENT // 8

溢出处理

不同的视图类型所能容纳的数值范围是确定的,超出这个范围就会出现溢出。不同类型的 TypedArray 视图有不同的溢出处理规则。

js
var uint8 = new Uint8Array(1);
uint8[0] = 256; // 溢出,结果为 0
uint8[0] = -1;  // 按无符号整数解释,结果为 255

ArrayBuffer 与字符串的转换

ArrayBuffer 与字符串可以互相转换,前提是字符串的编码方法是确定的。

js
// ArrayBuffer 转为字符串
function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint16Array(buf));
}

// 字符串转 ArrayBuffer
function str2ab(str) {
  var buf = new ArrayBuffer(str.length * 2);
  var bufView = new Uint16Array(buf);
  for (var i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

TypedArray.prototype.set() 和 subarray()

set 方法用于复制数组,subarray 方法用于基于 TypedArray 数组的部分创建新的视图。

js
var a = new Uint8Array(8);
var b = new Uint8Array(8);
b.set(a); // 复制a到b

var a = new Uint16Array(8);
var b = a.subarray(2, 3); // 创建a的子视图,包含a[2]

TypedArray.of() 和 from()

TypedArray.of 是一个静态方法,用于将参数转为 TypedArray 实例。TypedArray.from 方法接受一个可遍历的数据结构,返回一个基于此结构的 TypedArray 实例。

js
Float32Array.of(0.151, -8, 3.7) // Float32Array [0.151, -8, 3.7]

Uint16Array.from([0, 1, 2]) // Uint16Array [0, 1, 2]

复合视图

复合视图是指在同一个ArrayBuffer对象上创建多个不同类型数据的视图。由于TypedArray的构造函数可以指定起始位置和长度,我们可以在一段连续的内存中存储不同类型的数据。

js
// 创建一个24字节长度的ArrayBuffer
var buffer = new ArrayBuffer(24);

// 在ArrayBuffer的第0个字节开始的位置创建一个Uint32Array视图,长度为1
var idView = new Uint32Array(buffer, 0, 1);

// 在ArrayBuffer的第4个字节开始的位置创建一个Uint8Array视图,长度为16
var usernameView = new Uint8Array(buffer, 4, 16);

// 在ArrayBuffer的第20个字节开始的位置创建一个Float32Array视图,长度为1
var amountDueView = new Float32Array(buffer, 20, 1);

DateView视图

DataView 是一个构造函数,接受一个 ArrayBuffer 对象作为参数,并可以指定字节起始位置和长度。与 TypedArray 视图不同,DataView 允许你指定字节序(大端或小端),这使得它非常适合处理来自网络或文件的二进制数据。

js
// 创建一个24字节的ArrayBuffer
var buffer = new ArrayBuffer(24);
// 创建一个DataView视图
var dv = new DataView(buffer);

DataView 实例有以下属性:

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

DataView 提供了一系列 get 和 set 方法来读取和写入内存:

  • getInt8, getUint8:读取一个字节,返回8位整数。
  • getInt16, getUint16:读取两个字节,返回16位整数。
  • getInt32, getUint32:读取四个字节,返回32位整数。
  • getFloat32, getFloat64:读取四个字节或八个字节,返回32位或64位浮点数。
js
// 读取操作
var v1 = dv.getUint8(0);  // 从第0个字节读取一个8位无符号整数
var v2 = dv.getUint16(1); // 从第1个字节读取一个16位无符号整数
var v3 = dv.getUint16(3); // 从第3个字节读取一个16位无符号整数

// 写入操作
dv.setInt32(0, 25, false); // 在第0个字节以大端字节序写入值为25的32位整数
dv.setFloat32(8, 2.5, true); // 在第8个字节以小端字节序写入值为2.5的32位浮点数

DataView 的 get 和 set 方法可以接受一个布尔值参数来指定字节序。默认情况下,这些方法使用大端字节序解读数据,如果需要使用小端字节序,必须在方法的第二个参数指定 true。可以通过以下方式判断当前系统的字节序:

js
var littleEndian = (function() {
  var buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true);
  return new Int16Array(buffer)[0] === 256;
})();
// 如果返回 true,则是小端字节序;如果返回 false,则是大端字节序。

二进制数组的应用

AJAX

在AJAX中,XMLHttpRequest 允许服务器返回二进制数据。如果知道返回的数据类型,可以设置 responseType 为 arraybuffer。

js
var xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
  let arrayBuffer = xhr.response;
  // 处理ArrayBuffer
};
xhr.send();

Canvas

Canvas元素输出的二进制像素数据是 TypedArray 数组。Uint8ClampedArray 是一种特殊类型的 TypedArray,用于Canvas元素,自动处理颜色值的溢出。

js
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var uint8ClampedArray = imageData.data;

WebSocket

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

js
var socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';
socket.addEventListener('open', function(event) {
  var typedArray = new Uint8Array(4);
  socket.send(typedArray.buffer);
});
socket.addEventListener('message', function(event) {
  var arrayBuffer = event.data;
  // 处理ArrayBuffer
});

Fetch API

Fetch API取回的数据可以是 ArrayBuffer 对象

js
fetch(url)
.then(function(request) {
  return request.arrayBuffer();
})
.then(function(arrayBuffer) {
  // 处理ArrayBuffer
});

File API

如果知道文件的二进制数据类型,可以使用 FileReader 将文件读取为 ArrayBuffer 对象。

js
var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function() {
  var arrayBuffer = reader.result;
  // 处理ArrayBuffer
};

SharedArrayBuffer

JavaScript是单线程的,但Web Worker允许在浏览器中创建多个线程,主线程用于与用户互动,而Worker线程用于处理计算任务。每个线程的数据是隔离的,它们通过postMessage方法进行通信。

js
/* 主线程: 创建Worker线程 */
var w = new Worker('myworker.js');

// 发送消息给Worker线程
w.postMessage('hi');

// 监听Worker线程的回应
w.onmessage = function(ev) {
  console.log(ev.data);
};


/* Worker线程: 监听主线程的消息 */
onmessage = function(ev) {
  console.log(ev.data);
  postMessage('ho');
};

Web Worker之间的数据交换可以是各种格式的,包括二进制数据。ES2017引入了SharedArrayBuffer,允许Worker线程与主线程共享同一块内存,提高了大数据通信的效率。

js
/* 主线程 */
// 新建1KB共享内存
var sharedBuffer = new SharedArrayBuffer(1024);

// 在共享内存上建立视图,供写入数据
const sharedArray = new Int32Array(sharedBuffer);


/* Worker线程 */
// 将共享内存的地址发送给Worker线程
w.postMessage(sharedBuffer);

var sharedBuffer;

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

Atomics对象

在多线程环境中,如果两个线程同时修改共享内存的同一个地址,可能会出现数据不一致的问题。为了解决这个问题,Atomics对象提供了一组方法来保证操作的原子性和线程间的同步。

Atomics对象提供的方法可以分为几类:

  1. 读写操作

    • Atomics.store():向共享内存写入数据,保证写入操作的原子性。
    • Atomics.load():从共享内存中读出数据,保证读取操作的原子性。
js
  // 主线程
  var sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
  var ia = new Int32Array(sab);
  Atomics.store(ia, 42, 314159); // 原子性写入

  // Worker线程
  while (Atomics.load(ia, 37) == 163); // 原子性读取,等待条件满足
  console.log(ia[37]); // 123456
  1. 等待和唤醒操作

    • Atomics.wait():使线程在特定条件下休眠。
    • Atomics.wake():唤醒休眠的线程。
    js
    // 线程1
    Atomics.store(ia, 37, 123456);
    Atomics.wake(ia, 37, 1); // 唤醒等待的线程
    
    // 线程2
    Atomics.wait(ia, 37, 163); // 等待ia[37]不等于163
    console.log(ia[37]); // 123456
  2. 运算方法

    Atomics对象还提供了一些原子运算方法,如add、sub、and、or和xor,这些方法在进行运算的同时保证操作的原子性。

    js
    // Worker线程
    Atomics.add(ia, 112, 1); // 原子性地将ia[112]增加1
  3. 比较和交换

    Atomics.compareExchange():比较并交换值,如果当前值等于预期值,则更新为新值。

    js
    // 比较并交换
     Atomics.compareExchange(ia, 112, oldVal, newVal);

总结

ArrayBuffer 是 JavaScript 中用于操作二进制数据的接口,它代表内存中的一段二进制数据,并通过视图进行操作。视图分为 TypedArray 视图和 DataView 视图,前者包括如 Uint8ArrayInt16ArrayFloat32Array 等9种类型的视图,用于读写简单类型的二进制数据;后者用于读写复杂类型的二进制数据,并允许自定义字节序。

ArrayBufferbyteLength 属性返回内存区域的字节长度,而 slice() 方法用于复制内存区域的一部分,生成新的 ArrayBuffer 对象。ArrayBuffer.isView() 静态方法用于检查参数是否为 ArrayBuffer 的视图实例。

TypedArray 视图的构造函数可以接受长度或类数组对象作为参数,直接分配内存并生成 ArrayBuffer 实例。BYTES_PER_ELEMENT 属性表示每种 TypedArray 构造函数的数据类型占据的字节数。

在多线程环境中,SharedArrayBuffer 允许主线程和 Worker 线程共享同一块内存。Atomics 对象提供了一系列方法来保证共享内存操作的原子性和线程间的同步,包括读写操作(store()load())、等待和唤醒操作(wait()wake())以及原子运算方法(add()sub()and()or()xor())。这些方法确保了在多线程共享内存时操作的安全性和数据的一致性。