在 Web 开发中,我们主要在处理文件(创建、上传、下载)时遇到二进制数据。另一个典型的用例是图像处理。
所有这些在 JavaScript 中都是可能的,并且二进制操作的性能很高。
不过,这里有点混乱,因为有很多类。举几个例子:
ArrayBuffer、Uint8Array、DataView、Blob、File等。
与其他语言相比,JavaScript 中的二进制数据以非标准的方式实现。但是,当我们理清思路后,一切都变得相当简单。
基本的二进制对象是 ArrayBuffer - 对固定长度连续内存区域的引用。
我们是这样创建它的
let buffer = new ArrayBuffer(16); // create a buffer of length 16
alert(buffer.byteLength); // 16
这会分配一个连续的 16 字节内存区域,并用零预先填充它。
ArrayBuffer 不是一个数组让我们消除一个可能的混淆来源。ArrayBuffer 与 Array 没有任何共同之处
- 它具有固定长度,我们不能增加或减少它。
- 它在内存中占用恰好那么多的空间。
- 要访问单个字节,需要另一个“视图”对象,而不是
buffer[index]。
ArrayBuffer 是一个内存区域。里面存储了什么?它不知道。只是一系列原始字节。
要操作 ArrayBuffer,我们需要使用“视图”对象。
视图对象本身不存储任何东西。它是“眼镜”,可以解释存储在 ArrayBuffer 中的字节。
例如
Uint8Array– 将ArrayBuffer中的每个字节视为一个独立的数字,可能的取值范围为 0 到 255(一个字节是 8 位,因此它只能容纳那么多)。这样的值被称为“8 位无符号整数”。Uint16Array– 将每 2 个字节视为一个整数,可能的取值范围为 0 到 65535。这被称为“16 位无符号整数”。Uint32Array– 将每 4 个字节视为一个整数,可能的取值范围为 0 到 4294967295。这被称为“32 位无符号整数”。Float64Array– 将每 8 个字节视为一个浮点数,可能的取值范围为5.0x10-324到1.8x10308。
因此,16 字节 ArrayBuffer 中的二进制数据可以解释为 16 个“小数字”,或 8 个更大的数字(每个 2 字节),或 4 个更大的数字(每个 4 字节),或 2 个具有高精度的浮点数(每个 8 字节)。
ArrayBuffer 是核心对象,一切的根源,原始二进制数据。
但是,如果我们要写入它,或者遍历它,基本上对于几乎所有操作,我们都必须使用视图,例如
let buffer = new ArrayBuffer(16); // create a buffer of length 16
let view = new Uint32Array(buffer); // treat buffer as a sequence of 32-bit integers
alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer
alert(view.length); // 4, it stores that many integers
alert(view.byteLength); // 16, the size in bytes
// let's write a value
view[0] = 123456;
// iterate over values
for(let num of view) {
alert(num); // 123456, then 0, 0, 0 (4 values total)
}
TypedArray
所有这些视图(Uint8Array、Uint32Array 等)的通用术语是 TypedArray。它们共享相同的集合方法和属性。
请注意,没有名为 TypedArray 的构造函数,它只是一个常见的“总称”,用于表示 ArrayBuffer 上的视图之一:Int8Array、Uint8Array 等等,完整的列表将在后面给出。
当您看到类似 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();
-
如果提供了
ArrayBuffer参数,则会在其上创建视图。我们已经使用过这种语法。我们可以选择提供
byteOffset来指定起始位置(默认值为 0)和length(默认值为缓冲区的末尾),然后视图将只覆盖buffer的一部分。 -
如果给出了
Array或任何类似数组的对象,它将创建一个具有相同长度的类型化数组并复制内容。我们可以用它来预先填充数组数据。
let arr = new Uint8Array([0, 1, 2, 3]); alert( arr.length ); // 4, created binary array of the same length alert( arr[1] ); // 1, filled with 4 bytes (unsigned 8-bit integers) with given values -
如果提供了另一个
TypedArray,它也会执行相同的操作:创建一个具有相同长度的类型化数组并复制值。如果需要,值将在过程中转换为新的类型。let arr16 = new Uint16Array([1, 1000]); let arr8 = new Uint8Array(arr16); alert( arr8[0] ); // 1 alert( arr8[1] ); // 232, tried to copy 1000, but can't fit 1000 into 8 bits (explanations below) -
对于数字参数
length- 创建一个包含该数量元素的类型化数组。它的字节长度将是length乘以单个项目TypedArray.BYTES_PER_ELEMENT中的字节数。let arr = new Uint16Array(4); // create typed array for 4 integers alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer alert( arr.byteLength ); // 8 (size in bytes) -
没有参数,创建一个零长度的类型化数组。
我们可以直接创建一个 TypedArray,而无需提及 ArrayBuffer。但是,视图不能没有底层的 ArrayBuffer 存在,因此在所有这些情况下(除了第一个情况,即提供 ArrayBuffer 时)都会自动创建。
要访问底层的 ArrayBuffer,TypedArray 中有以下属性:
buffer- 引用ArrayBuffer。byteLength-ArrayBuffer的长度。
因此,我们始终可以从一个视图移动到另一个视图。
let arr8 = new Uint8Array([0, 1, 2, 3]);
// another view on the same data
let arr16 = new Uint16Array(arr8.buffer);
以下是类型化数组的列表:
Uint8Array、Uint16Array、Uint32Array- 用于 8 位、16 位和 32 位的整数。Uint8ClampedArray- 用于 8 位整数,在赋值时会“钳位”(见下文)。
Int8Array、Int16Array、Int32Array- 用于带符号整数(可以为负数)。Float32Array、Float64Array- 用于 32 位和 64 位的带符号浮点数。
int8 或类似的单值类型。请注意,尽管有像 Int8Array 这样的名称,但 JavaScript 中没有像 int 或 int8 这样的单值类型。
这是合乎逻辑的,因为 Int8Array 不是这些单个值的数组,而是对 ArrayBuffer 的视图。
越界行为
如果我们尝试将越界值写入类型化数组会怎样?不会出现错误。但额外的位会被截断。
例如,让我们尝试将 256 放入 Uint8Array。在二进制形式中,256 是 100000000(9 位),但 Uint8Array 每个值只提供 8 位,这使得可用范围从 0 到 255。
对于更大的数字,只有最右边的(最低有效)8 位被存储,其余的被截断。
所以我们会得到零。
对于 257,二进制形式是 100000001(9 位),最右边的 8 位被存储,所以我们将在数组中得到 1。
换句话说,保存了该数字模 28 的值。
以下是演示
let uint8array = new Uint8Array(16);
let num = 256;
alert(num.toString(2)); // 100000000 (binary representation)
uint8array[0] = 256;
uint8array[1] = 257;
alert(uint8array[0]); // 0
alert(uint8array[1]); // 1
Uint8ClampedArray 在这方面很特殊,它的行为不同。它将大于 255 的任何数字保存为 255,将任何负数保存为 0。这种行为对图像处理很有用。
TypedArray 方法
TypedArray 具有常规的 Array 方法,但有一些显著的例外。
我们可以迭代、map、slice、find、reduce 等。
不过,有些事情我们做不到。
- 没有
splice- 我们不能“删除”一个值,因为类型化数组是缓冲区的视图,而这些缓冲区是内存中固定、连续的区域。我们所能做的就是分配一个零。 - 没有
concat方法。
还有两种额外的方法。
arr.set(fromArr, [offset])将fromArr中的所有元素复制到arr中,从位置offset(默认值为 0)开始。arr.subarray([begin, end])从begin到end(不包括end)创建一个相同类型的新视图。这类似于slice方法(也受支持),但不会复制任何内容 - 只会创建一个新视图,用于操作给定的数据片段。
这些方法允许我们复制类型化数组、混合它们、从现有数组创建新数组等等。
DataView
DataView 是一个特殊的超灵活的“无类型”视图,用于 ArrayBuffer。它允许以任何格式访问任何偏移量的数据。
- 对于类型化数组,构造函数决定了格式。整个数组应该是一致的。第 i 个数字是
arr[i]。 - 使用
DataView,我们可以通过方法(如.getUint8(i)或.getUint16(i))访问数据。我们可以在方法调用时选择格式,而不是在构造时选择。
语法
new DataView(buffer, [byteOffset], [byteLength])
buffer– 底层的ArrayBuffer。与类型化数组不同,DataView不会自行创建缓冲区。我们需要准备好它。byteOffset– 视图的起始字节位置(默认值为 0)。byteLength– 视图的字节长度(默认值为到buffer末尾)。
例如,这里我们从同一个缓冲区中以不同的格式提取数字
// binary array of 4 bytes, all have the maximal value 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
let dataView = new DataView(buffer);
// get 8-bit number at offset 0
alert( dataView.getUint8(0) ); // 255
// now get 16-bit number at offset 0, it consists of 2 bytes, together interpreted as 65535
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)
// get 32-bit number at offset 0
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)
dataView.setUint32(0, 0); // set 4-byte number to zero, thus setting all bytes to 0
当我们在同一个缓冲区中存储混合格式的数据时,DataView非常有用。例如,当我们存储一系列对(16 位整数,32 位浮点数)时,DataView允许我们轻松地访问它们。
总结
ArrayBuffer是核心对象,是对固定长度连续内存区域的引用。
要对ArrayBuffer执行几乎所有操作,我们需要一个视图。
- 它可以是
TypedArrayUint8Array、Uint16Array、Uint32Array– 用于 8 位、16 位和 32 位无符号整数。Uint8ClampedArray– 用于 8 位整数,在赋值时对其进行“钳位”。Int8Array、Int16Array、Int32Array- 用于带符号整数(可以为负数)。Float32Array、Float64Array- 用于 32 位和 64 位的带符号浮点数。
- 或者
DataView– 使用方法指定格式的视图,例如getUint8(offset)。
在大多数情况下,我们直接创建和操作类型化数组,将ArrayBuffer隐藏在幕后,作为“共同点”。我们可以将其访问为.buffer,并在需要时创建另一个视图。
还有两个额外的术语,用于描述对二进制数据进行操作的方法
ArrayBufferView是所有这些类型视图的总称。BufferSource是ArrayBuffer或ArrayBufferView的总称。
我们将在下一章中看到这些术语。BufferSource是最常见的术语之一,因为它表示“任何类型的二进制数据” – 一个ArrayBuffer或对其的视图。
这是一个速查表
评论
<code>标签,对于多行代码,请将其包装在<pre>标签中,对于超过 10 行的代码,请使用沙盒(plnkr,jsbin,codepen…)