# 二进制数据-缓冲和视图
在 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 位整数)
- 如果给定的是 ArrayBuffer 参数,则会在其上创建视图。我们已经用过该语法了。
- 如果给定的是 Array,或任何类数组对象,则会创建一个相同长度的类型化数组,并复制其内容
- 如果给定的是另一个 TypedArray,也是如此:创建一个相同长度的类型化数组,并复制其内容
- 对于数字参数 length —— 创建类型化数组以包含这么多元素。它的字节长度将是 length 乘以单个 TypedArray.BYTES_PER_ELEMENT 中的字节数
- 不带参数的情况下,创建长度为零的类型化数组
我们可以直接创建一个 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)
。