定型数组(typed arrays)

阅读 1.9k
标签: JavaScript

定型数组是一种用于处理数值类型数据的专用数组,最早是在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方法读取相应位置的值。

这里有几点要注意:

  1. set方法和get方法必须一一匹配,比如setInt8设置的值,必须通过getInt8来读取,如果通过getUint8来读取,会导致数据错误
  2. 设置值时要注意越界的问题,setInt8是有符号的8位整数,所以其范围为-128~127,超过这个范围,比如,数字129就超出Int8的范围了,这会导致存储错误,如果是这种情况,请使用更大范围的类型,如Int16来存储。
  3. 定型数组一共支持8种类型:int8(有符号的8位整数)、uint8(无符号的8位整数)、int16(有符号的16位整数)、uint16(无符号的16位整数)、int32(有符号的32位整数)、uint32(无符号的32位整数)、float32(32位浮点数)、float64(64位浮点数),请根据实际情况选择合适的类型。
  4. 小心越界问题,比如上面例子只申请了5个字节(即40位)的内存空间,如果调用setInt16(4, 130)会报错:Uncaught RangeError: Offset is outside the bounds of the DataView。这说明已经内存越界了。

定型数组的视图

上面的例子中,相信大家已经看出来了,如果操作不同的类型,DataView就是一种万金油,但如果只使用某个特定的数据类型,则显得很不方便,所以,定型数组根据其类型,封装好了相应的构造函数,如下:

构造函数字节说明等价的C语言类型
Int8Array18位二进制补码有符号整数有符号char类型
Uint8Array18位无符号整数无符号char类型
Uint8ClampedArray18位无符号整数(强制转换)无符号char类型
Int16Array216位二进制补码有符号整数short
Uint16Array216位无符号整数无符号short
Int32Array432位二进制补码有符号整数int
Uint32Array432位无符号整数int
Float32Array432位IEEE浮点数float
Float64Array864位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
最后编辑于: 2022-06-26

评论(0条)

(必填)
复制成功