前言
在现有的计算机中,二进制常常以字节数组的形式存在于程序当中。例如在C#里面,就用byte[],标准C里面没有byte类型,但可以通过typedef把byte定义为unsigned char的别名,效果是一样的。JS设计之初似乎就没想过要处理二进制,对于字节的概念可以说是非常非常的模糊。如果要表达字节数组,那么似乎只能用一个普通数组来表示。
然而随着业务需求的逐渐发展,出现了WebGL这样的技术。所谓WebGL,就是指浏览器与显卡之间的通信接口。为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。类型化数组(Typed Array)就是在这种背景下诞生的。而类型化数组是建立在ArrayBuffer对象的基础上的。下面介绍一下Arraybuffer。
一、Arraybuffer
1.1 基本概念
ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范,是 JavaScript 操作二进制数据的一个接口。ArrayBuffer 对象是以数组的语法处理二进制数据,也称二进制数组。它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写。
❝ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。❝
1.2 基本操作
「语法」
new ArrayBuffer(length)
- 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节;
- 返回值:ArrayBuffer 对象;
- 异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常;
「示例」
const buffer = new ArrayBuffer(32);
buffer.byteLength; // 32
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
const buffer2 = buffer.slice(0, 1);
上面代码表示实例对象 buffer 占用 32 个字节。
它有实例属性 byteLength ,表示当前实例占用的内存字节长度。
它拥有一个静态方法isView(),这个方法可以用来判断是否为TypedArray实例或DataView实例。
它拥有实例方法 slice(),用来复制一部分内存,使用方式同数组的slice方法。
除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。
二、视图
2.1 TypedArray
TypedArray一共包含九种类型,每一种都是一个构造函数。(DataView视图不支持Uint8ClampedArray,其他都支持)
__名称描述长度(字节)__Int8Array8位有符号整数1Uint8Array8位无符号整数1Uint8ClampedArray8位无符号整型固定数组(数值在0~255之间)1Int16Array16位有符号整数2Uint16Array16位无符号整数2Int32Array32位有符号整数4Uint32Array32 位无符号整数4Float32Array32 位 IEEE 浮点数4Float64Array64 位 IEEE 浮点数8
每一种视图都有一个BYTES_PER_ELEMENT常数,表示这种数据类型占据的字节数。
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8
这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,普通数组的操作方法和属性,对TypedArray 数组完全适用。 普通数组与 TypedArray 数组的差异主要在以下方面:
TypedArray和Array之间也可以互相转换
const typedArray = new Uint8Array([1, 2, 3, 4]);
const normalArray = Array.apply([], typedArray);
「建立TypedArray视图」
// 创建一个8字节的ArrayBuffer
const a = new ArrayBuffer(8);
// 创建一个指向a的Int32视图,开始于字节0,直到缓冲区的末尾
const a1 = new Int32Array(a);
// 创建一个指向a的Uint8视图,开始于字节4,直到缓冲区的末尾
const a2 = new Uint8Array(a, 4);
// 创建一个指向a的Int16视图,开始于字节4,长度为2
const a3 = new Int16Array(a, 4, 2);
上面代码在一段长度为 8 个字节的内存(a)之上,生成了三个视图:a1、a2和a3。
视图的构造函数可以接受三个参数:
- 第一个参数(必选):视图对应的底层ArrayBuffer对象;
- 第二个参数:视图开始的字节序号,默认从 0 开始;
- 第三个参数:视图包含的数据个数,默认直到本段内存区域结束;
建立了视图以后,就可以进行各种操作了。这里需要明确的是,视图其实就是普通数组,语法完全没有什么不同,只不过它直接针对内存进行操作,而且每个成员都有确定的数据类型。所以,视图就被叫做“类型化数组”。
「TypedArray视图操作」
const buffer = new ArrayBuffer(8);
const int16View = new Int16Array(buffer);
for (let i = 0; i < int16View.length; i++) {
int16View[i] = i * 2;
}
console.log(int16View) // [0, 2, 4, 6]
上面代码生成一个8字节的ArrayBuffer对象,然后在它的基础上,建立了一个16位整数的视图。由于每个字节占据8位,那么16位就占据了2个字节(1个字节等于8位),所以一共可以写入4个整数,依次为0,2,4,6。
如果在这段数据上接着建立一个8位整数的视图,则可以读出完全不一样的结果。
const int8View = new Int8Array(buffer);
for (let i = 0; i < int8View.length; i++) {
int8View[i] = i;
}
console.log(int8View) // [0, 0, 2, 0, 4, 0, 6, 0]
首先整个ArrayBuffer对象会被分成8段。然后,由于x86体系的计算机都采用小端字节序(具体概念理解请自主查询),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。还可以看到下面这个例子
const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 10;
v1[1] = 3;
v1[2] = 11;
v1[3] = 8;
console.log(v1) // [10, 3, 11, 8]
const uInt16View = new Uint16Array(buffer); // [0xa, 0x3, 0xb, 0x8]
console.log(uInt16View) // 计算机采用小端字节序 [0x030a, 0x080b] => [778, 2059]
如果一段数据是大端字节序(大端字节序主要用于数据传输),TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序。
2.2 DataView
DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
❝ 字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。> 字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。> 例如假设上述变量 x 类型为int,位于地址 0x100 处,它的值为 0x01234567,地址范围为 0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,…。而小端法将是:0x100: 67, 0x101: 45,…。❝
「语法」
new DataView(buffer [, byteOffset [, byteLength]])
相关的参数说明如下:
- buffer:ArrayBuffer 对象 或 SharedArrayBuffer 对象;
- byteOffset(可选):此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始;
- 异常:此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度;
「示例」
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer, 0);
view.setInt8(1, 68);
view.getInt8(1); // 68
如果一次操作(get或者set)两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。DataView的操作方法默认使用大端字节序解读数据,如果需要使用小端字节序解读,必须在操作方法中指定参数为true(get方法的第二个参数和set方法的第三个参数)。
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
// 1个字节,默认大端字节序
const v1 = dv.getUint8(0);
// 小端字节序
const v1 = dv.getUint16(1, true);
// 大端字节序
const v2 = dv.getUint16(3, false);
// 在第5个字节,以小端字节序写入值为11的32位整数
dv.setInt32(4, 11, true);
对于直接处理ArrayBuffer对象的业务场景不是特别多,特别是写页面比较多的同学。笔者深刻认识并运用的场景,主要是在处理比较复杂且数据量比较大的点云数据,前端接收到的点云数据已经是原始采集数据转换过的二进制数据,前端需要对二进制数据进行解析,运用的解析方法就是上述提到的各种方法。下面介绍一下业务场景中比较常见到的一种二进制表示类型——Blob。
三、Blob
3.1 基本介绍
Blob 对象比较常用于文件上传、文件读写操作等。在对文件读写的时候,我们更多的时候只是操作File对象,而File继承了所有Blob的属性。所以在我们看来,File对象可以看作一种特殊的Blob对象。
而Blob 对象与 ArrayBuffer 的区别在于,Blob 对象用于操作二进制文件, ArrayBuffer 用于直接操作内存,所以他们有如下图的关系:
「语法」
const blob = new Blob(array [, options]);
相关的参数说明如下:
- array:字符串或二进制对象,表示新生成的Blob实例对象的内容;
- options(可选):比较常用的属性 type,表示数据的 MIME 类型,默认空字符串;
「示例」
const array = ['Hello World!
'];
const blob = new Blob(array, {type : 'text/html'});
「属性和方法」
由上图可以看到,Blob对象拥有size和type两个属性,以及多种自有方法。比较常用的方法slice、arrayBuffer等;slice方法主要用来拷贝原来的数据,返回的也是一个Blob实例,这个方法可以用来做切片上传。arrayBuffer方法返回一个 Promise 对象,包含 blob 中的数据,并在 ArrayBuffer 中以二进制数据的形式呈现。
const blob = new Blob([]);
blob.slice(0, 1);
blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/);
3.2 运用场景
通过window.URL.createObjectURL方法可以把一个blob转化为一个Blob URL,并且用做文件下载或者图片显示的链接。
Blob URL所实现的下载或者显示等功能,仅仅可以在单个浏览器内部进行。而不能在服务器上进行存储,亦或者说它没有在服务器端存储的意义。
下面是一个Blob的例子,可以看到它很短
blob:d3958f5c-0777-0845-9dcf-2cb28783acaf
和冗长的Base64格式的Data URL相比,Blob URL的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的“引用“。从这个角度看,Blob URL是一个浏览器自行制定的一个伪协议。
「文件下载」
「图片显示」
「切片上传」
「本地文件读取」