【HTML5 API】类型化数组和ArrayBuffer

类型化数组和ArrayBuffer

JavaScript中的数组是包含多个数值属性和一个特殊的length属性的通用对象。数组元素可以是JavaScript中任意的值。数组可以动态地增长和收缩,也可以是稀疏数组。JavaScript的实现中对数组做了很多的优化,使得典型的数组操作可以变得很快。类型化数组就是类数组对象,它和常规的数组有如下重要的区别:

- 类型化数组中的元素都是数字。使用构造函数在创建类型化数组的时候决定了数组中数字(有符号或者无符号整数或者浮点数)的类型和大小(以位为单位)。
- 类型化数组有固定的长度。
- 在创建类型化数组的时候,数组中的元素总是默认初始化为0。

一共有8种类型化数组,每一种的元素类型都不同。可以使用如下所示的构造函数来创建这8种类型化数组:

构造函数 数字类型
Int8Array() 有符号字节
Uint8Array() 无符号字节
Int16Array() 有符号16位短整数
Uint16Array() 无符号16位短整数
Int32Array() 有符号32位整数
Uint32Array() 无符号32位整数
Float32Array() 32位浮点数值
Float64Array() 64位浮点数值:JavaScript中的常规数字

在创建一个类型化数组的时候,可以传递数组大小给构造函数,或者传递一个数组或者类型化数组来用于初始化数组元素。一旦创建了类型化数组,就可以像操作其他类数组对象那样,通过常规的中括号表示法来对数组元素进行读/写操作:

var bytes = new Uint8Array(1024); 
for(var i = 0; i < bytes.length; i++) 
  bytes[i] = i & 0xFF;
  var copy = new Uint8Array(bytes);
  var ints = new Int32Array([0,1,2,3]);

现代JavaScript语言实现对数组进行了优化,使得数组操作已经非常高效。不过,类型化数组在执行时间和内存使用上都要更加高效。下面的函数用于计算出比指定数值小的最大素数。它使用了埃拉托色尼筛选算法,该算法要求使用一个大数组来存储哪些数字是素数,哪些是合数。由于每个数组元素只要使用一位信息,因此这里使用Int8Array要比使用常规的JavaScript数组更加高效:

// 使用埃拉托色尼筛选算法,返回一个小于的最大素数
function sieve(n) { 
  var a new Int8Array(n+1);            // 如果x是合数,则a[x]为1
  var max = Math.floor(Math.sqrt(n));  // 因数不能比它大
  var p=2;                             // 2是第一个素数
  while(p < max) {                     // 对于小于max的素数
      for(var i = 2*p; i <= n; i += p) // 将p的倍数都标记为合数
          a[i]=1;
      while(a[++p]) /* empty */;       // 下一个未标记的索引值是素数
  }
  while(a[n]) n--;                     // 反向循环找到最大的素数
  return n;                            // 将它返回
}

如果将其中的Int8Array()构造函数替换成传统的Array()构造函数,sieve()函数依然可用,但是,处理过程中可能需要2~3倍的时间,而且需要更多的内存来存储大的参数n的值。当处理图形相关的数字或者数学相关的数字的时候,类型化数组也很有用:

var matrix = new Float64Array(9);     // 一个3*3的矩阵
var 3dPoint = new Int16Array(3);      // 3D空间中的一点
var rgba = new Uint8Array(4);         // 一个4字节的RGBA像素值
var sudoku = new Uint8Array(81);      // 一个9*9数独板

使用JavaScript的中括号表示法可以获取和设置类型化数组的单个元素。然而,类型化数组自己还定义了一些用于设置和获取整个数组内容的方法。其中set()方法用于将一个常规或者类型化数组复制到一个类型化数组中:

var bytes = new Uint8Array(1024)	       // 1KB缓冲区
var pattern = new Uint8Array([O,1,2,3]); // 一个4个字节的数组 
bytes.set(pattern);	                   // 将它们复制到另一个数组的开始
bytes.set(pattern, 4);	               // 在另一个偏移量处再次复制它们
bytes.set([O,1,2,3],8);	               // 或直接从一个常规数组复制值

类型化数组还有一个subarray()方法,调用该方法返回部分数组内容:

var ints = new Int16Array([0,1,2,3,4,5,6,7,8,9]);	      // 10个短整数
var last3 = ints.subaarray(ints.length-3, ints.length); // 最后三个
last3[0]                                                // => 7: 等效于ints[7]

要注意的是,subarray()方法不会创建数据的副本。它只是直接返回原数组的其中一部分内容:

ints[9] = -1; // 改变原数组中的元素值,然后......
last3[2]      // => -1: 同时也改变子数组中的元素值

subarray()方法返回当前数组的一个新视图,这一事实,说明了类型化数组中某些重要的概念:它们都是基本字节块的视图,称为一个ArrayBuffer。每个类型化数组都有与基本缓冲区相关的三个属性:

last3.buffer                // => 返回一个ArrayBuffer对象
last3.buffer == ints.buffer // => true: 两者都是同一缓冲区上的视图
last3.byteOffset            // => 14: 此视图从基本缓冲区的第14个字节开始
last.bytelength             // => 6: 该视图是6字节(3个16位整数)长

ArrayBuffer对象自身只有一个返回它长度的属性:

last3.byteLength        // => 6: 此视图6个字节长 
last3.buffer.byteLength // => 20: 但是基本缓冲区长度有20个字节长

ArrayBuffer只是不透明的字节块。可以通过类型化数组获取这些字节,但是ArrayBuffer自己并不是一个类型化数组。然而,要注意的是:可以像对任意JavaScript对象那样,使用数字数组索引来操作ArrayBuffer。但是,这样做并不能赋予访问缓冲区中字节的权限:

var bytes = new Uint8Array(8); // 分配8个字节
bytes[0] = 1;                  // 把第一个字节设置为1
bytes.buffer[0]                // => undefined: 缓冲区没有索引值0
bytes.buffer[1] = 255;         // 试着错误地设置缓冲区中的字节
bytes.buffer[1]                // => 255: 这只设置一个常规的JS属性
bytes[1]                       // => 0: 上面这行代码并没有设置字节

可以直接使用ArrayBuffer()构造函数来创建一个ArrayBuffer,有了ArrayBuffer对象后,可以在该缓冲区上创建任意数量的类型化数组视图:

var buf = new ArrayBuffer(1024*1024);	      // 1MB
var asbytes = new Uint8Array(buf);	      // 视为字节
var asints = new Int32Array(buf);	          // 视为32位有符号整数
var lastK = new Uint8Array(buf, 1023*1024); // 视最后1KB为字节
var ints2 = new Int32Array(buf, 1024, 256); // 视第二个1KB为256个整数

类型化数组允许将同样的字节序列看成8位、16位、32位或者64位的数据块。这里提到了“字节顺序”:字节组织成更长的字的顺序。为了高效,类型化数组采用底层硬件的原生顺序。在低位优先(little-endian)系统中,ArrayBuffer中数字的字节是按照从低位到高位的顺序排列的。在高位优先(big.endian)系统中,字节是按照从高位到低位的顺序排列的。可以使用如下代码来检测系统的字节顺序:

// 如果整数0x00000001在内存中表示成:01 00 00 00,
// 则说明当前系统是低位优先系统
// 相反,在高位优先系统中,它会表示成:00 00 00 01
var little_endian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;

如今,大多数CPU架构都采用低位优先。然而,很多的网络协议以及有些二进制文件格式,是采用高位优先的字节顺序的。通常,处理外部数据的时候,可以使用Int8Array和Uint8Array将数据视为一个单字节数组,但是,不应该使用其他的多字节字长的类型化数组。取而代之的是可以使用DataView类,该类定义了采用显式指定的字节顺序从ArrayBuffer中读/写其值的方法:

var data;
var view = DataView(data);     // 假设这是一个来自网络的ArrayBuffer
var int = view.getInt32(0);    // 从字节0开始的,高位优先顺序的32位有符号int整数
int = view.getInt32(4,false);  // 接下来的32位int整数也是高位优先顺序的
int = view.getInt32(8,true)    // 接下来的4个字节视为低位优先顺序的有符号int整数
view.setInt32(8,int,false);    // 以高位优先顺序格式将数字写回去

DateView为8种不同的类型化数组分别定义了8个get方法。名字诸如:getIntl6()getUint32()以及getFloat64()。这些方法的第一个参数指定了ArrayBuffer中的字节偏移量,表示从哪个值开始获取。除了getInt8()方法和getUInt8()方法之外,其他所有getter方法都接受第二个可选的布尔类型的参数。如果忽略该参数或者将该参数设置为false,则表示采用高位优先字节顺序,反之,则采用低位优先字节顺序。

DateView同时也定义了8个对应的set方法,用于将值写入到那个基本缓存区ArrayBuffer中。这些方法的第一个参数指定偏移量,表示从哪个值开始写。第二个参数指定要写入的值。除了setInt8()方法和setUint8()方法之外,其他每个方法都接受第三个可选的参数。如果忽略该参数或者将该参数设置为false,则将值以高位优先字节顺序写入;反之,则采用低位优先字节顺序写入。

上一篇:js中怎么把arrayBuffer转换成字符串


下一篇:MIPI DSI 【三】BRING UP