JavaScript 之 Typed Array 入门

Typed Array 基本介绍

随着 Web 应用的成长越来越快,而且复杂度越来越高,增加了一些新特性:

  • 直接对 audio 和 video 进行操作;
  • 使用 WebSocket 收发未经过加工的二进制数据。

因此,JavaScript 提供了 Typed Array 类数组对象(也称为类型数组),可以在更深层次或者说更细粒度对数据做控制。Typed Array 具有以下特点:

  • 元素为对象的数组,是非常灵活的,可以动态收缩并且可以存放任意 js 类型的数据。因为js引擎会做优化,所以这些数组才会足够快
  • Typed Array 不能与普通的 Array 混淆,通过 Array.isArray() 检测 Typed Array 的话,会返回 false
  • Typed Array 不会继承所有 Array 的方法,例如 pushpop 不能在 Typed Array 中使用

Typed Array 架构基础:Buffers 和 Views

为了最大程度提高灵活性和有效性,JavaScript 中的 Typed Array 分为 BuffersViews 两种:

  • Buffer(通过 ArrayBuffer 类实现)指的是一个数据块对象,没有固定的格式;并且 Buffer 中的内容是不能访问到的。
  • 为了获得 Buffer 中内存的访问权限,Typed Array 提供了 View,View 包含一个上下文(包括数据类型,初始位置,元素数量),通过这个上下文将数据转换为 Typed Array。

通过下面这张图可以发现:同一个 Buffer 可以通过不同的 view 来反应数据存储情况:

同一个buffer可以通过不同的view来反应数据存储情况

ArrayBuffer

ArrayBuffer 是一种特殊的数据类型,代表着一个通用的、固定长度的二进制数据缓冲区。

不能直接修改 ArrayBuffer 中的内容,需要通过创建一个 Typed Array 的 view 或者 DataView 去修改。因为 view 或者 DataView 是一种特殊格式的 buffer,从而读写 buffer 中的内容。

Typed Array views

Typed Array views 有自描述名字,为 Int8Uint32Float64 等多种常见数字类型提供了 view。

有一种特殊的 Typed Array view - Uint8ClampedArray,它将数值严格限制在 0 到 255 之间,这个类型在 Canvas Data Processing 方面很有用,因为 Canvas 的 getImageData() 返回 ImageData,而 ImageData 实例是由 Uint8ClampedArray 和 image 的大小组成,在 node 标准库常用语法中有相关资料。

目前主要有 11 种 Typed Array view,不同类型的 Typed Array views,主要在 4 个方面有差异:Value Range,Size in bytes,Web IDL types 以及等价的 C 语言中的数据类型。

例如:Int8Array Typed Array view,Value Range 是 -128 到 127,字节大小是 1 byte;Web IDL type 是 byte,等价 C 语言中的 int8_t 类型。

扩展知识:什么是 Web IDL type

 IDL 是 Interface Definition Language 的简称,用来描述打算在 Web 浏览器中实现的 interface。

DataView

DataView 是一个更底层的接口,它提供了读写缓冲区中二进制数据的 gettersetter 方法。

DataView 用来处理不同类型数据很有用,例如 Typed Array views 采用平台的原生字节顺序(可以查看 Endianness)但是通过 DataView,我们可以调整字节顺序,默认情况字节顺序是大端模式,可以通过 DataView 的 api 调整为小端模式。

ArrayBuffer 的构造函数 DataView(buffer [, byteOffset [, byteLength]]),其中:buffer 是 ArrayBuffer 实例,byteOffset 是新 DataView 的偏移量,byteLength 是新 DataView 的字节长度。

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

// Create a couple of views
var view1 = new DataView(buffer);
var 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(view1.buffer);
console.log(view2.buffer);
console.log(view1.getInt8(12));
console.log(view2.getInt8(0));

扩展知识:什么是 Endianness?

Endianness 是指内存存储的大小端模式。

  • 大端模式,指的是低位数据高地址,0x12345678,12存buf[0],78(低位数据)存buf[3](高地址),也就是常规的正序存储。
  • 小端模式与大端模式相反。0x12345678,78 存在 buf[0],存在低地址。

Typed Arrays 的 Web API

使用了 Typed Array 的 Web API 有以下这些:

FileReader.prototype.readAsArrayBuffer()

之前接触过 FileReader 的 readAsDataURL(),该方法用于读取 Blob 或者 File 对象,在 reader.result 上输出 base64 格式的字符串。readAsArrayBuffer() 方法也是读取 File 或者 Blob 对象的内容。

readAsDataUR() 类似,读取 Blob 或者 File 对象完毕后,readyState 值变为 DONE,同时触发 loadend 事件,在 reader.result 属性上包含代表 file data 的 ArrayBuffer。也可以直接用 blob.arrayBuffer() 得到 blob 的二进制数据并且以 Promise 的方式处理。

blob.arrayBuffer().then(buffer => /* 处理ArrayBuffer */)

XMLHttpRequest.prototype.send()

该方法支持发送 Typed Array 和 ArrayBuffer 对象作为入参。

方法 XMLHttpRequest.send(body) 中的 body 参数支持多种类型的数据,包括:

  • 序列化之后的 Document
  • BodyInit:包括 Blob,BufferSource,FormData,URLSearchParams,ReadableStream 或者 USVString object。

规范中写明了其算法:https://fetch.spec.whatwg.org/#bodyinit。其中有一步是设置对象类型,切换 Content-Type 为对象的 MIME 类型

IDL 的定义如下:

interface mixin Body {
  readonly attribute ReadableStream? body;
  readonly attribute boolean bodyUsed;
  [NewObject] Promise<ArrayBuffer> arrayBuffer();
  [NewObject] Promise<Blob> blob();
  [NewObject] Promise<FormData> formData();
  [NewObject] Promise<any> json();
  [NewObject] Promise<USVString> text();
};

通过 send 方法发送二进制数据的最好方式是:ArrayBufferView 或 Blob。

IDL 通常用 JavaScript 或者 Java 实现。此处的 interface mixin 是 Java 实现。

如何将 Typed Array 转换为普通数组?

在处理完 Typed Array 以后,很多情况下会将其转换为普通数组。

可以使用 Array.from() 方法把 Typed Array 转换成普通数组:

let typedArray = new Uint8Array([1, 2, 3, 4]);
let normalArray = Array.from(typedArray);

或者,使用以下代码转换:

let typedArray = new Uint8Array([1, 2, 3, 4]),
    normalArray = Array.prototype.slice.call(typedArray);

normalArray.length === 4;
normalArray.constructor === Array;

Typed Array 使用例子

1、使用带 buffer(缓冲区)的 view(视图)

// 创建一个 16 字节定长的 buffer
let buffer = new ArrayBuffer(16);

这里用到了通信原理中的 1byte = 8bit(1 字节等于 8 个比特)

上图中的 Int8Int16Int32Uint8 中的数字都是以 bit 为单位的,其总空间为 16 * 8 = 128bit。

因此:

  • Int8Array 和 Uint8Array 类型的 ArrayBuffer 长度为16;
  • Int16Array 类型的 ArrayBuffer 长度为 8
  • Int32Array 类型的 ArrayBuffer 长度为 4

位数越大,说明 ArrayBuffer 中的一个 0 占用的内存空间越大。

// 通过 byteLength 属性,可以获得一个 buffer 占用的字节内存空间
console.log(buffer.byteLength); // 将输出 16
// 以下代码会创建一个将 buffer 中的数据转换为 32bit 的有符号整型数组
let int32View = new Int32Array(buffer);

这里除了可以创建 Int32Array 的 ArrayBufferView,还可以创建 Int8Array,Uint8Array,Int16Array 等等类型的 ArrayBufferView。

// 可以像普通数组一样去遍历 ArrayBufferView 数组
int32View.forEach((e, i, arr) => { arr[i] = i; }) // 0, 1, 2, 3

这个数组有 4 个 entry,每个 entry 占用 4 个字节,所以总计 16 个字节。

2、处理复杂的数据结构

用多个不同类型的 view 联接 1 个单独的 buffer,需要从一个 buffer 的不同偏移量开始。

可以与一个包含了多数据类型的 Data Object 交互。例如 WebGL 交互复杂数据结构、data files 或者 C 数据结构。

struct someStruct {
  unsigned long id; // long 32bit
  char username[16]; // char 8bit
  float amountDue; // float 32bit
};

上面的 C 结构在 js 中可以这样定义:

let buffer = new ArrayBuffer(24);
// ... read the data into the buffer ...
let idView = new Uint32Array(buffer, 0, 1);
let usernameView = new Uint8Array(buffer, 4, 16);
let amountDueView = new Float32Array(buffer, 20, 1);

需要注意 C 语言的数据结构是与平台相关的。因此,C 语言中的 long,char,float 具体占用多少 bit,与运行平台有关。

扩展知识:

1、Uint32Array(buffer, 0, 1),其中的 0,1 代表什么?

  • 0 代表从 ArrayBuffer 开始位置的偏移量。构造时固定,且只读。
  • 1 表示 ArrayBuffer 中元素的个数。构造时固定,只读。

2、length 与 byteLength 区别是什么?

直接看代码:

var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
int32View.length; // 4
int32View.byteLength; // 16
分享