定型数组(typed arrays)
定型数组是一种用于处理数值类型数据的专用数组,最早是在WebGL
中使用的,其可为JavaScript提供快速的按位运算
。
为什么需要定型数组
在JavaScript中,数字是按照IEEE754
标准定义的格式存储,即用64位
来存储一个数字。这会导致一个问题,假如,我操作的数据,都是16位以内的数据(即0~65536之间),那么,每个数据都用64位来存储,那么会导致浪费
38位的存储空间,如果有一个数组,能够定义其中所有存储的数据都用16位来存储,将能更有效地利用存储空间。所以,定型数组出现了。
ArrayBuffer
定型数组将数据存储在内存
中,ArrayBuffer就是一段包含特定数量的字节的内存地址,它是定型数组的基础,可以通过ArrayBuffer来创建数组缓冲区:
let buffer = new ArrayBuffer(10) // 分配了10个字节
如上,创建了一个10个字节长度的缓冲区。它有一个属性byteLength
,表示其缓冲区的字节大小。
另一种创建数组缓冲区的方式是通过slice():
let buffer2 = buffer.slice(4, 6) // 分配2个字节
buffer2创建了从4到5索引提取的字节,所以,一共是两个字节。
仅创建数组缓冲区意义不大,我们的目的是将数据写入到这些缓冲区中,那怎么写入数据呢?
DataView
数组缓冲区是内存中的一段地址,为了将数据写入到内存的缓冲区中,就需要一个能够操作内存的接口:DataView。其有三个属性,buffer、byteOffset和byteLength。
let buffer = new ArrayBuffer(5),
view1 = new DataView(buffer)
view2 = new DataView(buffer, 1, 2)
console.log(view1.buffer === buffer) // true
console.log(view2.buffer === buffer) // true
console.log(buffer)
console.log(view1)
console.log(view2)
view1和view2的buffer是同一个,view1的byteOffset为0,byteLength为5;view2的byteOffset为1,byteLength为2。
写入和读取
既然有了DataView这个可以操作缓冲区的接口,那么就可以通过它来操作写入和读取了,写入用set方法,读取用get方法:
view1.setInt8(0, -128)
view1.setInt8(1, 68)
console.log(view1.getInt8(0))
console.log(view1.getInt8(1))
上面代码,将第一个字节(即索引0)设置为数字-128,第二个字节(即索引1)的设置为数字68,然后,通过get方法读取相应位置的值。
这里有几点要注意:
- set方法和get方法必须一一匹配,比如setInt8设置的值,必须通过getInt8来读取,如果通过getUint8来读取,会导致数据错误
- 设置值时要注意越界的问题,setInt8是有符号的8位整数,所以其范围为
-128~127
,超过这个范围,比如,数字129就超出Int8的范围了,这会导致存储错误,如果是这种情况,请使用更大范围的类型,如Int16来存储。 - 定型数组一共支持8种类型:int8(有符号的8位整数)、uint8(无符号的8位整数)、int16(有符号的16位整数)、uint16(无符号的16位整数)、int32(有符号的32位整数)、uint32(无符号的32位整数)、float32(32位浮点数)、float64(64位浮点数),请根据实际情况选择合适的类型。
- 小心越界问题,比如上面例子只申请了5个字节(即40位)的内存空间,如果调用
setInt16(4, 130)
会报错:Uncaught RangeError: Offset is outside the bounds of the DataView
。这说明已经内存越界了。
定型数组的视图
上面的例子中,相信大家已经看出来了,如果操作不同的类型,DataView就是一种万金油
,但如果只使用某个特定的数据类型,则显得很不方便,所以,定型数组根据其类型,封装好了相应的构造函数,如下:
构造函数 | 字节 | 说明 | 等价的C语言类型 |
---|---|---|---|
Int8Array | 1 | 8位二进制补码有符号整数 | 有符号char类型 |
Uint8Array | 1 | 8位无符号整数 | 无符号char类型 |
Uint8ClampedArray | 1 | 8位无符号整数(强制转换) | 无符号char类型 |
Int16Array | 2 | 16位二进制补码有符号整数 | short |
Uint16Array | 2 | 16位无符号整数 | 无符号short |
Int32Array | 4 | 32位二进制补码有符号整数 | int |
Uint32Array | 4 | 32位无符号整数 | int |
Float32Array | 4 | 32位IEEE浮点数 | float |
Float64Array | 8 | 64位IEEE浮点数 | double |
可以看出,其名称后面都是用Array
来结尾的,这是为了将和原有的Array类型来联系起来,也就是说,既然它也是一种数组(虽然有点特殊),那么数组中的一些属性和方法,上面的定型数组都是可以使用的。
那具体怎么使用呢?创建特定类型的定型数组,一共有三种方法,来看一个例子:
// 方法一: 传入buffer
let buffer = new ArrayBuffer(5)
view1 = new Int8Array(buffer, 1, 3) // view1为一个长度为3,开始位置为索引1的视图
console.log(view1)
// 方法二: 传入数字
let view2 = new Int16Array(2) // view2为长度为2,4个字节的视图
// 方法三: 传入数组
let view3 = new Int16Array([99, 215]) // view3为长度为2,4个字节的视图
let view4 = new Int32Array(view3) // view4为长度为2,8个字节的视图
console.log(view3[1])
在实际应用中,需要根据不同的场景,选择合适的方式。如果已经有一些普通数组,且需要将其转化为类型数组,那么选择第三种方式就很方便。
上面八种定型数组的构造函数,我们通过new
操作符来创建的定型数组,本质上是一种视图(和DataView类似),有趣的是,用这种方式创建的视图,也是一种数组,比用DataView方式操作起来更加人性化:
let buffer = new ArrayBuffer(5) // 5个字节长度的Buffer
let view2 = new Int8Array(buffer, 0, 3) // view2为一个长度为3,开始位置为索引0的视图
console.log(view2)
view2[2] = 98
console.log(view2[2])
view2有一个length属性,值为3,之后将索引为2的值设置为98,可以看出,这个view2的操作和数组及其相似,这说明了设计组在设计时的细心考虑。
普通数组和定型数组
既然定型数组和普通数组都是数组,他们之间有什么异同呢?先来看看相似之处吧。
相同之处
通过上面的例子,我们发现,类型数组和数组都有length
属性(表示元素的个数),都能通过索引访问
元素。除此之外,普通数组的很多方法,比如sort()、map()、filter()、find()、reverse()、indexof()、forEach()等等方法,定型数组也都可以使用。
let view1 = new Int16Array([198, -22])
view2 = view1.map(v => v * 2)
console.log(view1[0]) // 198
console.log(view2[0]) // 396
console.log(view1.buffer === view2.buffer) // false
上面代码中,创建了两个视图,view1通过map映射得到了view2,这种方式和普通使用数组的方式完全一致。另外,和普通数组一样,定型数组也可以使用展开运算符
、for-of
循环。由此可以看出,ECMAScript标准组在制定标准时,考虑得还是很周全的。
不同之处
let view1 = new Int16Array([198, -22])
console.log(view1 instanceof Array) // false
console.log(view1 instanceof Int16Array) // true
普通数组的大小可以灵活变化,但定型数组的大小是固定的,在创建时就指定了:
let view1 = new Int16Array([198, -22])
view1[2] = 999
console.log(view1[1]) // -22
console.log(view1[2]) // undefined
view1的长度在创建时就固定了,为2,最多容纳2个16位的整数值。
相互转换
两者之间可以相互转换:
let arr1 = [198, -22]
view1 = new Int16Array(arr1)
arr2 = [...view1]
将普通数组转换为特定的定型数组,可以通过new相应的构造函数来完成;而要将定型数组转换为普通数组,用展开运算符即可。
参考
- 深入理解ES6 / Nicholas C. Zakas