类型化数组和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,则将值以高位优先字节顺序写入;反之,则采用低位优先字节顺序写入。