类型化数组(TypedArrays)

正如我们在本系列的 类型与文法 中讲到过的,JS确实拥有一组内建类型,比如numberstring。看到一个称为“类型化的数组”的特性,可能会诱使你推测它意味着一个特定类型的值的数组,比如一个仅含字符串的数组。

然而,类型化数组其实更多的是关于使用类似数组的语义(索引访问,等等)提供对二进制数据的结构化访问。名称中的“类型”指的是在大量二进制位(比特桶)的类型之上覆盖的“视图”,它实质上是一个映射,控制着这些二进制位是否应当被看作8位有符号整数的数组,还是被看作16位有符号整数的数组,等等。

你怎样才能构建这样的比特桶呢?它被称为一个“缓冲(buffer)”,而你可以用ArrayBuffer(..)构造器直接地构建它:

  1. var buf = new ArrayBuffer( 32 );
  2. buf.byteLength; // 32

现在buf是一个长度为32字节(256比特)的二进制缓冲,它被预初始化为全0。除了检查它的byteLength属性,一个缓冲本身不会允许你进行任何操作。

提示: 有几种web平台特性都使用或返回缓冲,比如FileReader#readAsArrayBuffer(..)XMLHttpRequest#send(..),和ImageData(canvas数据)。

但是在这个数组缓冲的上面,你可以平铺一层“视图”,它就是用类型化数组的形式表现的。考虑如下代码:

  1. var arr = new Uint16Array( buf );
  2. arr.length; // 16

arr是一个256位的buf缓冲在16位无符号整数的类型化数组的映射,意味着你得到16个元素。

字节顺序

明白一个事实非常重要:arr是使用JS所运行的平台的字节顺序设定(大端法或小端法)被映射的。如果二进制数据是由一种字节顺序创建,但是在一个拥有相反字节数序的平台被解释时,这就可能是个问题。

字节顺序指的是一个多字节数字的低位字节(8个比特位的集合) —— 比如我们在早先的代码段中创建的16位无符号整数 —— 是在这个数字的字节序列的左边还是右边。

举个例子,让我们想象一下用16位来表示的10进制的数字3085。如果你只有一个16位数字的容器,无论字节顺序怎样它都将以二进制表示为0000110000001101(十六进制的0c0d)。

但是如果3085使用两个8位数字来表示的话,字节顺序就像会极大地影响它在内存中的存储:

  • 0000110000001101 / 0c0d (大端法)
  • 0000110100001100 / 0d0c (小端法)

如果你从一个小端法系统中收到表示为00001101000011003085,但是在一个大端法系统中为它上面铺一层视图,那么你将会看到值3340(10进制)和0d0c(16进制)。

如今在web上最常见的表现形式是小端法,但是绝对存在一些与此不同的浏览器。你明白一块二进制数据的生产者和消费者的字节顺序是十分重要的。

在MDN上有一种快速的方法测试你的JavaScript的字节顺序:

  1. var littleEndian = (function() {
  2. var buffer = new ArrayBuffer( 2 );
  3. new DataView( buffer ).setInt16( 0, 256, true );
  4. return new Int16Array( buffer )[0] === 256;
  5. })();

littleEndian将是truefalse;对大多数浏览器来说,它应当返回true。这个测试使用DataView(..),它允许更底层,更精细地控制如何从你平铺在缓冲上的视图中访问二进制位。前面代码段中的setInt16(..)方法的第三个参数告诉DataView,对于这个操作你想使用什么字节顺序。

警告: 不要将一个数组缓冲中底层的二进制存储的字节顺序与一个数字在JS程序中被暴露时如何被表示搞混。举例来说,(3085).toString(2)返回"110000001101",它被假定前面有四个"0"因而是大端法表现形式。事实上,这个表现形式是基于一个单独的16位视图的,而不是两个8位字节的视图。上面的DataView测试是确定你的JS环境的字节顺序的最佳方法。

多视图

一个单独的缓冲可以连接多个视图,例如:

  1. var buf = new ArrayBuffer( 2 );
  2. var view8 = new Uint8Array( buf );
  3. var view16 = new Uint16Array( buf );
  4. view16[0] = 3085;
  5. view8[0]; // 13
  6. view8[1]; // 12
  7. view8[0].toString( 16 ); // "d"
  8. view8[1].toString( 16 ); // "c"
  9. // 调换(好像字节顺序一样!)
  10. var tmp = view8[0];
  11. view8[0] = view8[1];
  12. view8[1] = tmp;
  13. view16[0]; // 3340

类型化数组的构造器拥有多种签名。目前我们展示过的只是向它们传递一个既存的缓冲。然而,这种形式还接受两个额外的参数:byteOffsetlength。换句话讲,你可以从0以外的位置开始类型化数组视图,也可以使它的长度小于整个缓冲的长度。

如果二进制数据的缓冲包含规格不一的大小/位置,这种技术可能十分有用。

例如,考虑一个这样的二进制缓冲:在开头拥有一个2字节数字(也叫做“字”),紧跟着两个1字节数字,然后跟着一个32位浮点数。这是你如何在同一个缓冲,偏移量,和长度上使用多视图来访问数据:

  1. var first = new Uint16Array( buf, 0, 2 )[0],
  2. second = new Uint8Array( buf, 2, 1 )[0],
  3. third = new Uint8Array( buf, 3, 1 )[0],
  4. fourth = new Float32Array( buf, 4, 4 )[0];

类型化数组构造器

除了前一节我们检视的(buffer,[offset, [length]])形式之外,类型化数组的构造器还支持这些形式:

  • [constructor](length):在一个长度为length字节的缓冲上创建一个新视图
  • [constructor](typedArr):创建一个新视图和缓冲,并拷贝typedArr视图中的内容
  • [constructor](obj):创建一个新视图和缓冲,并迭代类数组或对象obj来拷贝它的内容

在ES6中可以使用下面的类型化数组构造器:

  • Int8Array(8位有符号整数),Uint8Array(8位无符号整数)
    • Uint8ClampedArray(8位无符号整数,每个值都被卡在0 - 255范围内)
  • Int16Array(16位有符号整数),Uint16Array(16位无符号整数)
  • Int32Array(32位有符号整数),Uint32Array(32位无符号整数)
  • Float32Array(32位浮点数,IEEE-754)
  • Float64Array(64位浮点数,IEEE-754)

类型化数组构造器的实例基本上和原生的普通数组是一样的。一些区别包括它有一个固定的长度并且值都是同种“类型”。

但是,它们共享绝大多数相同的prototype方法。这样一来,你很可能将会像普通数组那样使用它们而不必进行转换。

例如:

  1. var a = new Int32Array( 3 );
  2. a[0] = 10;
  3. a[1] = 20;
  4. a[2] = 30;
  5. a.map( function(v){
  6. console.log( v );
  7. } );
  8. // 10 20 30
  9. a.join( "-" );
  10. // "10-20-30"

警告: 你不能对类型化数组使用没有意义的特定Array.prototype方法,比如修改器(splice(..)push(..),等等)和concat(..)

要小心,在类型化数组中的元素被限制在它被声明的位长度中。如果你有一个Uint8Array并试着向它的一个元素赋予某些大于8为的值,那么这个值将被截断以保持在相应的位长度中。

这可能造成一些问题,例如,如果你试着对一个类型化数组中的所有值求平方。考虑如下代码:

  1. var a = new Uint8Array( 3 );
  2. a[0] = 10;
  3. a[1] = 20;
  4. a[2] = 30;
  5. var b = a.map( function(v){
  6. return v * v;
  7. } );
  8. b; // [100, 144, 132]

在被平方后,值2030的结果会位溢出。要绕过这样的限制,你可以使用TypedArray#from(..)函数:

  1. var a = new Uint8Array( 3 );
  2. a[0] = 10;
  3. a[1] = 20;
  4. a[2] = 30;
  5. var b = Uint16Array.from( a, function(v){
  6. return v * v;
  7. } );
  8. b; // [100, 400, 900]

关于被类型化数组所共享的Array.from(..)函数的更多信息,参见第六章的“Array.from(..)静态方法”一节。特别地,“映射”一节讲解了作为第二个参数值被接受的映射函数。

一个值得考虑的有趣行为是,类型化数组像普通数组一样有一个sort(..)方法,但是这个方法默认是数字排序比较而不是将值强制转换为字符串进行字典顺序比较。例如:

  1. var a = [ 10, 1, 2, ];
  2. a.sort(); // [1,10,2]
  3. var b = new Uint8Array( [ 10, 1, 2 ] );
  4. b.sort(); // [1,2,10]

就像Array#sort(..)一样,TypedArray#sort(..)接收一个可选的比较函数作为参数值,它们的工作方式完全一样。