二进制操作时性能蛮高的。

  • ArrayBuffer、Uint8Array、DataView、Blob、File 等。

ArrayBuffer 是基础的二进制对象——是一个指向一块连续内存区域的引用。

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

代码执行后,会分配一块连续的、占据 16 字节长度的内存区域,每个字节被 0 填充。

注意:ArrayBuffer 不是数组

ArrayBuffer 对应一块内存区域。

为了操作 ArrayBuffer,我们需要使用“view”对象。

  • Uint8Array——每一个字节用来表示一个数值,有效值从 0 到 255(一个字节是 8 位)。这些值被称为“8位无符号整数”
  • Uint16Array——用两个字节表示一个整数,有效值从 0 到 65535。这些值被称为“16位无符号整数”
  • Uint32Array——
  • Float64Array——用八个字节表示一个浮点数。取值范围从 5.0x10-324 到 1.8x10308。

因此,一个包含 16 个字节的 ArrayBuffer 可以表示为 16 个“很小的数字”,或者 8 个较大的数字(每个两个字节),或者 4 个更大的数字(每个四个字节),或者 2 个高精度的浮点数(每个八个字节)。

arraybuffer-views.svg
ArrayBuffer 是二进制文件操作中,最底层、最原始的核心对象。

对二进制数据的进行的几乎任何操作,比如写和遍历,基本上都要通过 view 来实现的。例如:

  1. let buffer = new ArrayBuffer(16); // 创建一个长度为 16 的 buffer
  2. let view = new Uint32Array(buffer) // 将 buffer 作为 32 位整数操作
  3. alert(Uint32Array.BYTES_PER_ELEMENT) // 每个整数占据 4 个字节
  4. alert(view.length) // 4,存储了这么多整数
  5. alert(view.byteLength) // 16,一共占据了这么多字节长度
  6. // 我们写入一个值
  7. view[0] = 123456
  8. // 遍历出 view 中的值
  9. for(let num of view) {
  10. alert(num); // 123456, 然后是 0, 0, 0 (一共 4 个值)
  11. }

TypedArray

上面讲的所有这些 view 的通用术语叫 TypedArray。它们共享同样的方法和属性。

它们类似于常规数组:有索引、可迭代。

TypedArray 构造器(比如 Int8Array 或者 Float64Array,这不重要)根据使用参数类型的不同而有不同的表现。

一共有五种创建方式:

  1. new TypedArray(buffer, [byteOffset], [length]);
  2. new TypedArray(object);
  3. new TypedArray(typedArray);
  4. new TypedArray(length);
  5. new TypedArray();
  1. 如果提供的参数是 ArrayBuffer,则会在其基础之上创建 view。我们前面已经使用过了上面的语法。

可选参数 byteOffset、length 表示创建 view 时的起始索引(默认为 0)和长度(默认到 buffer 的最后一个字节),如此,创建的 view 对象仅覆盖 buffer 的一部分。

  1. 如果是 Array,或者是类数组(array-like)对象,那么结果会创建一个等长的、复制了原对象的内容的 TypedArray。

我们可以借助这个特点,用数组实现数据的预填:

  1. let arr = new Uint8Array([0, 1, 2, 3])
  2. alert(arr.length) // 4, 创建了一个等长的二进制数组
  3. alert(arr[1]) // 1, 用 4 个字节填充规定的 4 个值(无符号 8 位整数)
  1. 如果提供的是 TypedArray,则所做的事情相同:结果会创建一个等长的、复制了原对象内容的 TypedArray。如果需要的话,这些值会被处理为新类型。
  1. let arr16 = new Uint16Array([1, 1000]);
  2. let arr8 = new Uint8Array(arr16);
  3. alert( arr8[0] ); // 1
  4. alert( arr8[1] ); // 232, 尝试复制 1000 的时候, 但 8 位字节无法表示 (后面解释)
  1. 对数值参数 length——会创建一个包含这么多元素的 TypedArray。而最终的字节长度等于 length 乘以每个元素的字节长度,可以由 TypedArray.BYTES_PER_ELEMENT 得到。
  1. let arr = new Uint16Array(4);
  2. alert(Uint16Array.BYTES_PER_ELEMENT); // 每个整数占据 2 个字节
  3. alert(arr.byteLength); // 8(字节长度)
  1. 无参数的话,会创建一个长度为 0 的 TypedArray。

我们可以不使用 ArrayBuffer 直接创建一个 TypeArray,但要清楚的是,每一个 view 底层都是 ArrayBuffer。上述除第一个的其他创建方式,会自动创建 ArrayBuffer 的。

访问 ArrauBuffer,可使用下列属性:

  • arr.buffer——对底层 ArrayBuffer 的引用。
  • arr.byteLength——底层 ArrayBuffer 占据的字节数。
  1. let arr8 = new Uint8Array([0, 1, 2, 3]);
  2. // 基于同一个底层 ArrayBuffer 数据的另一个 view
  3. let arr16 = new Uint16Array(arr8.buffer);
  • Uint8Array、Uint16Array、Uint32Array
    • Uint8ClampedArray
  • Int8Array、Int16Array、Int32Array
  • Float32Array、Float64Array

超出边界后的行为

如果我们尝试将 256 放入 Uint8Array。256 的二进制形式为 100000000(共 9 位),但是 Unit8Array 只有 8 位表示每个值,有效范围从 0 到 255。

对于超出范围的值,只会保留存储最右边的 8 位,剩下则会切除:

8bit-integer-256.svg
结果得到了 0。

那么 257 呢,它的二进制形式为 100000001(共 9 位),只会存储最右边的 8 位,因此数组里会保留一个 1:
8bit-integer-257.svg
就是说,最后保留下来的值是数字对 28 取模后的结果。

demo:

  1. let uint8array = new Uint8Array(16);
  2. let num = 256;
  3. alert(num.toString(2)); // 100000000 (二进制表示)
  4. uint8array[0] = 256;
  5. uint8array[1] = 257;
  6. alert(uint8array[0]); // 0
  7. alert(uint8array[1]); // 1

Uint8ClampedArray 与 Uint8Array 相比,特别的地方在于,对于大于 255 的值,保存为 255,对小于 0 的负值,保存的则是 0。

TypedArray 方法

TypedArray 具备有常规的数组方法。

我们可以遍历、map、slice、find、reduce 它。

但有些事情没法做:

  • 无法 splice
  • 无法使用 concat 方法

此外,还提供了额外的两个方法:

  • arr.set(fromArr, [offset])
  • arr.subArray([begin, end])

DateView

DateView 是一个超级灵活的“untyped”的 view。可以以任何格式和偏移量访问底层的 ArrayBuffer 数据。

  • 使用 TypedArray 创建对象时,从名字上即可得知其格式。整个数组的表示都是统一的。第 i 个数就是 arr[i]。
  • DateView 使用类似 .getUnit8(i) 或 .getUint16(i) 这些方法访问数据。我们是在调用的时候指定获取数据的格式,而非在创建的时候。

语法:

  1. new DataView(buffer, [byteOffset], [byteLength])
  • buffer——对象底层使用的 ArrayBuffer。不同于 TypedArray 的是,DataView 不能自行创建自己的 buffer,我们需要一个准备好的 buffer 创建一个 DataView 对象。
  • byteOffset
  • byteLength

例如,这里我们从同一个 buffer 中使用不同格式提取数字:

  1. // 包含 4 字节的二进制数组,存储最大值 255
  2. let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
  3. let dataView = new DataView(buffer);
  4. // 以 8 位数字、偏移量 0 访问数据
  5. alert( dataView.getUint8(0) ); // 255
  6. // 现在再以 16 位数字、偏移量 0 访问数据,每个数字由 3 字节组成,拼在一起就成 65535 了
  7. alert( dataView.getUint16(0) ); // 65535 (最大的 16 位无符号整数)
  8. // 现在再以 32 位数字、偏移量 0 访问数据
  9. alert( dataView.getUint32(0) ); // 4294967295 (最大的 32 位无符号整数)
  10. dataView.setUint32(0, 0); // 设置 4 字节数值 0,导致所有字节值都变为 0 了

总结

ArrayBuffer 是一个指向一块连续内存区域的引用,是二进制文件操作中,最底层、最原始的核心对象。

几乎所有对 ArrayBuffer 的操作,都是通过 view 对象实现的。

  • 可以是 TypedArray:
    • Uint8Array、Uint16Array、Uint32Array
    • Uint8ClampedArray
    • Int8Array、Int16Array、Int32Array
    • Float32Array、Float64Array
  • 或者 DataView——以方法的形式指定特定格式的数据。比如 getUint8(offset)。

这里还有另外两个术语,描述操作二进制数据时会用到的:

  • ArrayBufferView 是所有类型的 view 对象的总称
  • BufferSource 是 ArrayBuffer 和 ArrayBufferView 的统称。

我们会在下一章里看到这些术语。BufferSource 是最长使用的术语之一。它表示“任意类型的二进制数据”——ArrayBuffer 或者基于其上的 view 对象。

arraybuffer-view-buffersource.svg
(完)