JS的定型数组

定型数组(typed array)是ECMAScript新增的结构,目的是提升向原生库传输数据的效率。实际上,JavaScript并没有”TypedArray“类型,它所指的其实是一种特殊的包含数值类型的数组。为理解如何使用定型数组,有必要先了解一下它的用途。

 

①ArrayBuffer

  Float32Array实际上是一种”视图“,可以允许JavaScript运行时访问一块名为ArrayBuffer的预分配内存。ArrayBuffer是所有定型数组及视图引用的基本单位。

注意:SharedArrayBuffer是ArrayBuffer的一个变体,可以无需复制就在执行上下文传递它。

 

ArrayBuffer()是一个普通的JavaScript构造函数,可用于在内存中分配特定数量的字节空间。

const buf=new ArrayBuffer(16);   //在内存中分配16字节
alert(buf.byteLength);    //16

ArrayBuffer一经创建就不能再调整大小,不过,可以使用slice()复制其全部或部分到一个新实例中:

const buf1=new ArrayBuffer(16);
const buf2=buf1.slice(4,12);
alert(buf2.byteLength);      //8

ArrayBuffer某种程度上类似于C++的malloc(),但也有几个明显的区别。

1)malloc()在分配失败时候会返回一个null指针。ArrayBuffer在分配失败时会抛出错误。

2)malloc()可以利用虚拟内存,因此最大可分配尺寸只受可寻址系统内存限制。ArrayBuffer分配的内存不能超过Number.Max_SAFE_INTEGER(2^53-1)字节

3)malloc()调用成功不会初始化实际的地址。声明ArrayBuffer,则会将所有二进制位初始化位0.

4)通过malloc()分配的堆内存除非调用free()或程序退出,否则系统不能再使用。而通过声明ArrayBuffer分配的堆内存可以被当成垃圾回收,不同手动释放。

不能仅通过对ArrayBuffer的引用就读取或写入其内容。要读取或写入ArrayBuffer,就必须通过视图。视图有不同的类型,但引用的都是ArrayBuffer中存储的二进制数据。

 

一、DataView

  第一种允许你读写ArrayBuffer的视图是DataView。这个视图专为文件I/O和网络I/O设计,其API支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些。DataView对缓冲内容没有任何预设,也不能迭代。

  必须在对已有的ArrayBuffer读取或者写入时才能创建DataView实例。这个实例可以使用全部或者部分ArrayBuffer,且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置。

复制代码
const buf=new ArrayBuffer(16);


//DataView默认使用整个ArrayBuffer
const fullDataView=new DataView(buf);
alert(fullDataView.byteOffset);   //0
alert(fullDataView.byteLength);  //16
alert(fullDataView.buffer===buf);       //true


//构造函数接收一个可选的字节偏移量和字节长度
//byteOffset=0表示视图从缓冲起点开始
//byteLength=8 限制视图为前8个字节

const firstHalfDataView=new DataView(buf,0,8);
alert(firstHalfDataView.byteOffset);     //0
alert(firstHalfDataView.byteLength);    //8
alert(firstHalfDataView.buffer===buf);   //true


//如果不指定,则DataView会使用剩余的缓冲
//byteOffset=8 表示视图从缓冲的第九个字节开始
// byteLength未指定,默认为剩余缓冲
const secondHalfDataView=new DataView(buf,8);
alert(secondHalfDataView.byteOffset);        //8
alert(secondHalfDataView.byteLength);        //8
alert(secondHalfDataView.buffer===buf);        //true
alert(secondHalfDataView.byteLength);        //8
alert(secondHalfDataView.buffer===buf); //true
复制代码

要通过DataView读取缓冲,还需要几个组件。

首先是要读或写的字节偏移量。可以看成DataView中的某种”地址“。

DataView应该使用ElementType来实现JavaScript的Number类型到缓冲内二进制格式的转换。

最后是内存中值的字节序。默认为大端字节序。

(1)ElementType

DataView对存储在缓冲内的数据类型没有预设。它暴露的API强制开发者在读、写时指定一个ElementType,然后DataView就会忠实地为读、写而完成相应的转换。

ES6支持八种不同的ElementType(如下表)

ElementType 字节 说明 等价的C类型 值的范围
Int8 1 8位有符号整数 signed char -127~127
Uint8 1 8位无符号整数 unsigned char 0~255
Int16 2 16位有符号整数 short -32768~32767
Uint16 2 16位无符号整数 unsigned short 0~65535
Int32 4 32位有符号整数 int -2147483648~2147483647
Uint32 4 32位无符号整数 unsigned int 0~4294967295
Float32 4 32位IEEE-754浮点数 float -3.4e+38~+3.4e+38
Float 8 64位IEEE-754浮点数 double -1.7e+308~+1.7e+308

DataView为上表中的每种类型都暴露了get和set方法,这些方法使用byteOffset(字节偏移量)定位要读取或者写入值的位置。类型是可以互换使用的,如下例所示:

//在内存中分配两个字节并声明一个DataView

const buf=new ArrayBuffer(2);

const view=new DataView(buf);

 

//说明整个缓冲确实所有二进制位都是0

//检查第一个和第二个字符

alert(view.getInt8(0));  //0

alert(view.getInt8(1));  //0

//检查整个缓冲

alert(view.getInt(16));   //0

//将整个缓冲都设置为1

//255的二进制表示是11111111

view.setUint8(0,255);

//DataView会自动将数据转换为特定的ElementType

//255的十六进制表示为0xFF

view.setUint8(1,0xFF);

//现在、缓冲里都是1了

//如果把它当成二补数的有符号整数,则应该是-1

alert(view.getInt16(0));    //-1

二、定型数组

  定型数组是另一种形式的ArrayBuffer视图。虽然概念上与DataView接近,但定型数据的区别在于,它特定于一种ElementType且遵循系统原生的字节序。相应地,定型数组提供了适用而更广的API和更高的性能。设计定型数组的目的就是提高与WebGL等原生库交换二进制数据的效率。由于定型数组的二进制表示对操作系统而言是一种容易使用的格式,JavaScripte引擎可以重读优化算术运算、按位运算和其他对定型数组的常见操作,因此使用它们速度极快。

  创建定型数组的方式包括读取已有的缓冲、使用自由缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。另外,通过<ElementType>.from()和<ElementType>.of()也可以创建定型数组:

复制代码
//创建一个12字节的缓冲
const buf=new ArrayBuffer(12);

//创建一个引用该缓冲的Int32Array

const ints=new Int32Array(buf);

//这个定型数组知道自己的每个元素需要4字节
//因此长度为3
alert(ints.length);        //3

//创建一个长度为6的Int32Array
const ints2=new Int32Array(6);
//每个数值使用4字节,因此ArrayBuffer是24字节
allert(ints2.length);
//类似DataView,定型数组也有一个指向关联缓冲的引用
alert(ints2.buffer.byteLength);//24

//创建一个包含[2,4,6,8]的Int32Array
const ints3=new Int32Array([2,4,6,8]);
alert(ints3.length);             //4
alert(ints3.buffer.byteLength);   //16
alert(ints3[2]);                    //6

//通过复制ints3的值创建一个Int16Array
const ints4=new Int16Array(int3);

//这个新类型数组会分配自己的缓冲
//对应索引的每个值会相应转换为新格式

alert(ints4.length);       //4
alert(ints4.buffer.byteLength);  //8
alert(ints4[2]);           //6


//基于普通数组来创建一个Int16Array
const ints5=Int16Array.from([3,5,7,9]);
alert(ints5.length);    //4
alert(ints5.buffer.byteLength)   //8
alert(ints5[2]);           //7

//基于传入的参数创建一个Float32Array
const floats=Float32Array.of(3.14,2.718,1.618);
alert(floats.length);      //3
alert(floats.buffer.byteLength);   //12
alert(floats[2]);            //1.6180000305175781

定型数组的构造函数和实例都有一个BYTES_PER_ELEMENT属性,返回该类型数组中每个元素的大小。

alert(Int16Array.BYTES_PER_ELEMENT);   //2
alert(Int32Array.BYTES_PER_ELEMENT);   //4

const ints=new Int32Array(1),
        floats=new Float64Array(1);

alert(ints.BYTES_PER_ELEMENT);    //4
alert(floats.BYTES_PER_ELEMENT);   //8

如果定型数组没有用任何值初始化,则其关联的缓冲会以0填充:

const ints=new Int32Array(4);
alert(ints[0]);       //0
alert(ints[1]);       //0
alert(ints[2]);       //0
alert(ints[3]);       //0
复制代码

三、定型数组的行为

1.[]

2.copyWithin()

3.entries()

4.every()

5.fill()

6.filter()

7.find()

8.findIndex()

9.forEach()

10.indexOf()

11.join()

12.keys()

13.lastIndexOf()

14.length()

15.map()

16.reduce()

17.reduceRight()

18.reverse()

19.slice()

20.some()

21.sort()

22.toLocaleString()

23.toString()

24.values()

 

其中,返回新数组的方法也会返回包含同样元素类型(element type)的新定型数组:

const ints=new Int16Array([1,2,3]);

const doubleints=ints.map(x=>2*x);

alert(doubleints instanceof Int16Array);  //true

定型数组有一个Symbol.iterator符号属性,因此可以通过for..of循环和扩展操作符来操作:

复制代码
const ints=new Int16Array([1,2,3]);

for(const int of ints){

  alert(int);

}

//1

//2

//3

复制代码
alert(Math.max(...ints));   //3

三、定型数组的合并、复制和修改

定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小。因此。下列方法不适用于定型数组:

1)concat()

2)pop()

3)push()

4)shift()

5)splice()

6)unshift()

不过定型数据也提供了两个新方法,可以快速向外或向内复制数据:set()和subarray()。set()从提供的数组或定型数组中把值复制到当前定型数组中指定的索引位置:

复制代码
//创建长度为8的ini16数组
const container=new Int16Array(8);

//把定型数组复制为前四个值
//偏移量默认为索引0
container.set(Int8Array.of(1,2,3,4));
console.log(container);         //[1,2,3,4,0,0,0,0]

//把普通数组复制为后4个值
//偏移量4表示从索引4开始插入
container.set([5,6,7,8],4);
console.log(container);          //[1,2,3,4,5,6,7,8]

//溢出会抛出错误
container.set([5,6,7,8],7);
//RangeError
复制代码

 

subarray()执行与set()相反的操作,他会基于从原始定型数组中复制的值返回一个新定型数组,复制值时的开始索引和结束索引是可选的:

复制代码
const source =Int16Array.of(2,4,6,8)

//把整个数组复制为一个同类型的新数组
const fullCopy=source.subarray();
console.log(fullCopy);      //[2,4,6,8]

//从索引2开始复制数组
const halfCopy=source.subarray(2);
console.log(halfCopy);    [6,8]

//从索引1开始复制到索引3
const partialCopy=source.subarray(1,3);
console.log(partialCopy);   //[4,6]
复制代码

定型数组没有原生的拼接能力,但使用定型数组API提供的很多工具可以手动构建:

复制代码
//第一个参数是应该返回的数组类型
//其余参数是应该拼接在一起的定型数组
function typedArrayConcat(typedArrayConstructor,...typedArrays){
  //计算所有数组中包含的元素总数
  const numElements=typedArrays.reduce((x,y)=>(x.length||x)+y.length);

  //按照提供的类型创建一个数组,为所有元素留出空间
  const resultArray=new typedArrayConstructor(numElements);
 
  //依次转移数组
  let currentOffset=0;
  typedArrays.map(x=>{
      resultArray.set(x,currentOffset);
      currentOffset+=x.length;  
});      
  return resultArray;                
}

const concatArray=typedArrayConcat(Int32Array,Int8Array.of(1,2,3),Int16Array.of(4,5,6),Float32Array.of(7,8,9));

console.log(concatArray);    //[1,2,3,4,5,6,7,8,9]
console.log(concatArray instanceof Int32Array);        //true
复制代码

四、上溢和下溢

定型数组中值的下溢和上溢不会影响到其他索引,但仍然需要考虑数组的元素应该是什么类型。定型数组对于可以存储的每个索引只接受一个相关位,而不考虑它们对实际数值的影响。以下代码演示了如何处理下溢和上溢:

 

复制代码
// 长度为2 的有符号整数数组
// 每个索引保存一个二补数形式的有符号整数(范围是 -1 * 2^7 ~ (2^7 - 1)
const ints = new Int8Array(2);

// 长度为2 的无符号整数数组
// 每个索引保存一个无符号整数 (0~255) // (2^7 - 1)
const unsignedInts = new Uint8Array(2);

// 上溢的位不会影响相邻索引
// 索引只取最低有效位上的 8 位
unsignedInts[1] = 256; //0x100
console.log(unsignedInts);//[0, 0]
unsignedInts[1] = 511; // 0x1FF
console.log(unsignedInts);// [0,255]

// 下溢的位会被转换为其无符号的等价值 
// 0xFF 是以二补数形式表示的-1(截取到 8 位), 
// 但 255 是一个无符号整数 
unsignedInts[1] = -1        // 0xFF (truncated to 8 bits) 
console.log(unsignedInts);  // [0, 255] 
 
// 上溢自动变成二补数形式 
// 0x80 是无符号整数的 128,是二补数形式的-128 
ints[1] = 128;        // 0x80 
console.log(ints);    // [0, -128] 
 
// 下溢自动变成二补数形式 
// 0xFF 是无符号整数的 255,是二补数形式的-1 
ints[1] = 255;        // 0xFF 
console.log(ints);    // [0, -1]
复制代码

除了 8 种元素类型, 还有一种 “夹板” 数组类型 : Uint8ClampedArray, 不允许任何方向溢出。 超出最大值255的值会被向下舍入为255, 而小于最小值的 0 会被向上舍入为0。

const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256]);
console.log(clampedInts); // [0, 0, 255, 255]

 

posted @   Jaetyn  阅读(443)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示