在 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
执行几乎所有操作,我们需要一个视图。
- 它可以是
TypedArray
Uint8Array
、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…)