# 二进制数据-缓冲和视图

在 Web 开发中,当我们处理文件时(创建,上传,下载),经常会遇到二进制数据。另一个典型的应用场景是图像处理。

这些都可以通过 JavaScript 进行处理,而且二进制操作性能更高。

不过,在 JavaScript 中有很多种二进制数据格式,会有点容易混淆。仅举几个例子:

ArrayBuffer,Uint8Array,DataView,Blob,File 及其他。

# ArrayBuffer

基本的二进制对象是 ArrayBuffer —— 对固定长度的连续内存空间的引用。

我们这样创建它:

let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
alert(buffer.byteLength); // 16

它会分配一个 16 字节的连续内存空间,并用 0 进行预填充。

ArrayBuffer 不是数组

让我们先澄清一个可能的误区。ArrayBuffer 与 Array 没有任何共同之处:

  • 它的长度是固定的,我们无法增加或减少它的长度。
  • 它正好占用了内存中的那么多空间。
  • 要访问单个字节,需要另一个“视图”对象,而不是 buffer[index]。

TIP

Array 存储的对象能动态增多和减少,并且可以存储任何 JavaScript 值。JavaScript 引擎会做一些内部优化,以便对数组的操作可以很快.

如要操作 ArrayBuffer,我们需要使用“视图”对象。

视图对象本身并不存储任何东西。它是一副“眼镜”,透过它来解释存储在 ArrayBuffer 中的字节。

例如:

  • Uint8Array —— 将 ArrayBuffer 中的每个字节视为 0 到 255 之间的单个数字(每个字节是 8 位,因此只能容纳那么多)。这称为 “8 位无符号整数”。
  • Uint16Array —— 将每 2 个字节视为一个 0 到 65535 之间的整数。这称为 “16 位无符号整数”。

因此,一个 16 字节 ArrayBuffer 中的二进制数据可以解释为 16 个“小数字”,或 8 个更大的数字(每个数字 2 个字节),或 4 个更大的数字(每个数字 4 个字节),或 2 个高精度的浮点数(每个数字 8 个字节)。

ArrayBuffer 是核心对象,是所有的基础,是原始的二进制数据。

但是,如果我们要写入值或遍历它,基本上几乎所有操作 —— 我们必须使用视图(view),例如:

let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer

let view = new Uint32Array(buffer); // 将 buffer 视为一个 32 位整数的序列

alert(Uint32Array.BYTES_PER_ELEMENT); // 每个整数 4 个字节

alert(view.length); // 4,它存储了 4 个整数
alert(view.byteLength); // 16,字节中的大小

# TypedArray

一个类型化数组(TypedArray)对象描述了一个底层的二进制数据缓冲区(binary data buffer)的一个类数组视图(view)。事实上,没有名为 TypedArray 的全局属性,也没有一个名为 TypedArray 的构造函数。TypedArray 只是对视图的一个通用总称术语。

属性 含义
arr.BYTES_PER_ELEMENT 构造函数创建的数组里面元素分配的字内存字节数
arr.byteLength 数组所有元素占用的内存空间,单位:字节 Byte
arr.length 数组内含元素个数
方法 含义
arr.get(index) 返回索引 index 对应的元素值
arr.set(index, value) 更改类型数组索引 index 对应的元素值为 value
构造函数 位数 字节 类型描述 C 语言等价类型
Int8Array 8 1 有符号 8 位整型 int8_t
Uint8Array 8 1 无符号 8 位整型 uint8_t
Int16Array 16 2 有符号 16 位整型 int16_t
Uint16Array 16 2 无符号 16 位整型 int16_t
Int32Array 32 4 有符号 32 位整型 int32_t
Uint32Array 32 4 无符号 32 位整型 uint32_t
Float32Array 32 4 单精度 32 位浮点数 float
Float64Array 64 8 双精度 64 位浮点数 double

几种不同数组创建方式对应的内存分配图

arr1 = [10, 12, 13];
arr2 = new Uint16Array([10, 12, 13]);
arr3 = new Uint8Array([10, 12, 13]);

注意:10 对应的二进制是 1010

00000000000000000000000000001010
0000000000001010
00001010

# 使用 TypedArray

下文中当你看到 new TypedArray 之类的内容时,它表示 new Int8Array、new Uint8Array 及其他中之一。

类型化数组的行为类似于常规数组:具有索引,并且是可迭代的。

一个类型化数组的构造器(无论是 Int8Array 或 Float64Array,都无关紧要),其行为各不相同,并且取决于参数类型。

参数有 5 种变体:

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
let arr = new Uint8Array([0, 1, 2, 3]);
alert(arr.length); // 4,创建了相同长度的二进制数组
alert(arr[1]); // 1,用给定值填充了 4 个字节(无符号 8 位整数)
  1. 如果给定的是 ArrayBuffer 参数,则会在其上创建视图。我们已经用过该语法了。
  2. 如果给定的是 Array,或任何类数组对象,则会创建一个相同长度的类型化数组,并复制其内容
  3. 如果给定的是另一个 TypedArray,也是如此:创建一个相同长度的类型化数组,并复制其内容
  4. 对于数字参数 length —— 创建类型化数组以包含这么多元素。它的字节长度将是 length 乘以单个 TypedArray.BYTES_PER_ELEMENT 中的字节数
  5. 不带参数的情况下,创建长度为零的类型化数组

我们可以直接创建一个 TypedArray,而无需提及 ArrayBuffer。但是,视图离不开底层的 ArrayBuffer,因此,除第一种情况(已提供 ArrayBuffer)外,其他所有情况都会自动创建 ArrayBuffer。

如要访问 ArrayBuffer,可以用以下属性:

  • arr.buffer —— 引用 ArrayBuffer。
  • arr.byteLength —— ArrayBuffer 的长度。

# DataView

DataView视图是一个可以从 二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。

// create an ArrayBuffer with a size in bytes
const buffer = new ArrayBuffer(16);

// Create a couple of views
const view1 = new DataView(buffer);
const view2 = new DataView(buffer, 12, 4); //from byte 12 for the next 4 bytes
view1.setInt8(12, 42); // put 42 in slot 12

console.log(view2.getInt8(0));
// expected output: 42

# 缓冲和视图的关系

JavaScript 类型数组(Typed Arrays)将实现拆分为缓冲和视图两部分。一个缓冲(由 ArrayBuffer 对象实现)描述的是一个数据块。缓冲没有格式可言,并且不提供机制访问其内容。为了访问在缓冲对象中包含的内存,你需要使用视图。视图提供了上下文 — 即数据类型、起始偏移量和元素数 — 将数据转换为实际有类型的数组。

ArrayBuffer 缓冲是一种数据类型,用来表示一个通用的、固定长度的二进制数据缓冲区。你不能直接操纵一个 ArrayBuffer 中的内容;你需要创建一个类型化数组的视图。

DataView 视图是一个可以从 二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。

TIP

可以认为 buffer 是分配内存,view 决定操作这段内存时以何种方式来对待它。

# 使用视图和缓冲

// 首先,我们创建一个16字节固定长度的缓冲
const buffer = new ArrayBuffer(16);
// 我们将创建一个视图,此视图将把缓冲内的数据格式化
const int32View = new Int32Array(buffer);
console.log(int32View.length); // 4
// 现在我们可以像普通数组一样访问该数组中的元素
for (var i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
}
console.log(int32View); // [0,2,4,6,buffer,byteLength:16,length:4]

# 越界行为

如果我们尝试将越界值写入类型化数组会出现什么情况?不会报错。但是多余的位被切除。

例如,我们尝试将 256 放入 Uint8Array。256 的二进制格式是 100000000(9 位),但 Uint8Array 每个值只有 8 位,因此可用范围为 0 到 255。

对于更大的数字,仅存储最右边的(低位有效)8 位,其余部分被切除:

因此结果是 0。

257 的二进制格式是 100000001(9 位),最右边的 8 位会被存储,因此数组中会有 1

换句话说,该数字对 28 取模的结果被保存了下来。

# 总结

ArrayBuffer 是核心对象,是对固定长度的连续内存区域的引用。

几乎任何对 ArrayBuffer 的操作,都需要一个视图。

它可以是 TypedArray:

  • Uint8Array,Uint16Array,Uint32Array —— 用于 8 位、16 位和 32 位无符号整数。
  • Uint8ClampedArray —— 用于 8 位整数,在赋值时便“固定”其值。
  • Int8Array,Int16Array,Int32Array —— 用于有符号整数(可以为负数)。
  • Float32Array,Float64Array —— 用于 32 位和 64 位的有符号浮点数。 或 DataView —— 使用方法来指定格式的视图,例如,getUint8(offset)