由来
- 在webGL的早期版本中,因为JavaScript的数组和原生数组之间不匹配,所以出现了性能问题。图形驱动程序通常不需要以JavaScript默认双精度浮点格式传递他们的数值,而这恰恰是JavaScript的数组在内存中的格
- 因此在webGL与JavaScript运行时之间传递数组时,webGL绑定都需要在目标环境分配新数组,以其当前格式迭代数组,然后将数值转化为新数组中的适当格式,而这要花费很多时间
- 这当然是难以接受的,Mozilla为了解决这个问题而实现了CanvasFloatArray。这是一个提供JavaScript的接口,C语言风格的浮点值数组。JavaScript的运行时使用这个类型可以分配,读取,写入数组。这个数组可以直接传给底层图形,驱动程序也可以直接从底层获取到
- 最终CanvasFloatArray变成了Float32Array,这就是今天定型数组中第一个类型
- 而这只是一个起点,后面前辈们又在这块做了很多改进,我们来一一看看有哪些改进吧
ArrayBuffer
这是一个数组,不能设置值,不能直接访问的数组。设置或访问其中的数据,需要视图的帮助。上面提到的Aloat32Array就是一个视图。ArrayBuffer是视图操作的基本单位
我们来看下如何创建一个ArrayBuffer
// 在内存中分配了8个字节长度的ArrayBuffer
const buffer = new ArrayBuffer(8);
console.log(buffer.byteLength); //8
生成了之后,数组的每项都是0。不能给他设置上初始值
它除了可以访问长度之外,还可以切割生成的新的函数
// 可以通过slice,使用旧的ArrayBuffer来生成新的ArrayBuffer
const bufferNew = buffer.slice(2);
console.log(bufferNew.byteLength); //6
好像每种数组都有slice方法
这样的一个数组,存不了数据,也看不了数据,有什么用?
DataView
这是一个视图,有了它就可以对ArrayBuffer设置值,并且查看里面的数据了
创建
// 通过ArrayBuffer创建了一个视图
const bufferArray = new ArrayBuffer(8);
const dataView = new DataView(bufferArray);
看看它有哪些属性
视图可以看成一个观察和处理数据的窗口,这个窗口的长度是小于等于ArrayBuffer的长度的,并且起点也可以是任意的,不一定要从ArrayBuffer第0个位置开始
我们来看代码
// 打印出视图所能看见的ArrayBuffer的长度
console.log(dataView.byteLength); //8
// 视图查看ArrayBuffer的起点
console.log(dataView.byteOffset); //0
这个起点和查看的范围是创建的时候确定的,上面的创建过程并没有指定offset,和观察的范围,所以就默认用第0个位置开始,范围整个的ArrayBuffer
下面我在创建的时候设置一下
// 生成DataView的同时,指定查看的起点和长度
const dataView2 = new DataView(bufferArray, 2, 4);
// 结果是符合预期的
console.log(dataView2.byteLength); //4
console.log(dataView2.byteOffset); //2
构造器的第2个参数,指定开始的位置;第3个参数指定可观测的范围
OK,现在有了视图,我们再看看如何对ArrayBuffer的数据进行操作
修改和查看ArrayBuffer中的数据
// 通过视图,向ArrayBuffer写入数据,第0个位置填入1
dataView2.setInt8(0, 1);
// 这里以一个字节为单位,来读取:
// 二进制:0000 0001
console.log(dataView2.getInt8(0)); //1
这里有几个点需要注意:
-
用视图写入数据时的位置,是相对于视图来说的,并不是相对于ArrayBuffer。如果视图的offset是2,意味着视图开始观察的位置相对于ArrayBuffer是从下标为2的位置开始的。这时通过视图填入数据,填入数据的位置是2,那么相对于ArrayBuffer,只从下标为4的位置开始的
-
上面的dataView2填入数据时的位置偏移是0,那么相对于ArrayBuffer就是从下标为2的位置开始的。
所以当设值完成之后,dataView2对应的ArrayBuffer的数据内容就是:
console.log(dataView2);
// DataView {
// byteLength: 4,
// byteOffset: 2,
// buffer: ArrayBuffer {
// [Uint8Contents]: <00 00 01 00 00 00 00 00>,
// byteLength: 8
// }
// }
我们可以看到下标为2的字节的值变成了1
- 同理可得,我们读取数据时的位置,也是相对于视图开始观察的位置
- setInt8 和 getInt8 中的“Int8”的含义是指以一个字节的长度为单位操作数据。其他的还有getInt16,其中的“Int16”的含义是指,以两个字节的长度为单位操作数据
下面我们看下示例
// 这里以两个字节为单位,来读取:
// 二进制: 0000 0001 0000 0000
console.log(dataView2.getInt16(0)); // 256
这里还是从视图开始观察的位置读取数据,但是以两个字节为单位读取数据,那就读取到了0000 0001 0000 0000,值为2^8
我们继续来看几个例子,来加深你的理解
// 通过视图,以一个字节为单位,向ArrayBuffer写入数据
// 往第1个位置填入1
dataView2.setInt8(1, 1);
// 这里以两个字节为单位,
// 读取到: 0000 0001 0000 0001
console.log(dataView2.getInt16(0)); // 257
2^8 + 1 = 257
符合预期
// 通过视图,以2个字节为单位,向ArrayBuffer写入数据
// 第0个位置填入1
dataView2.setInt16(0, 1);
// 这里以两个字节为单位
// 读取到: 0000 0000 0000 0001
console.log(dataView2.getInt16(0)); // 1
// 这里以一个字节为单位
// 读取到: 0000 0000
console.log(dataView2.getInt8(0)); // 0
// 读取到: 0000 0001
console.log(dataView2.getInt8(1)); // 1
符合预期
不止int8、int16,还有int32,更多的信息可以查阅相关文档😁
这里插点大小端的知识点
大端字节序也被称为网络字节序,意思是最高有效位保存在第一个字节,最低有效位保存在最后一个字节。小端字节序正好相反,即最低有效位保存在第一个字节,最高有效位呢保存在最后一个字节。
注意:大小端是以字节为单位的,不是以字位为单位的
举个例子:
// 清空数据
dataView2.setInt16(0, 0);
// 默认大端
// 1000 0000 0000 0001
dataView2.setInt8(0, 0x80);
dataView2.setInt8(1, 0x01);
console.log(dataView2.getUint16(0)); // 32769
2^15 + 1 = 32769
不做任何处理的时候,是默认大端模式的。即,高位在前面字节,底位在后面字节
// 小端读取
// 大端:1000 0000 0000 0001 -> 小端: 0000 0001 1000 0000
console.log(dataView2.getUint16(0, true)); // 384
2^8 + 2^7 = 384
第二个参数是true的时候,就是小端模式
我们可以明显地看到,当变成了小端模式时,后面的字节被当成了高位,前面的字节被当成了低位
大小端不仅仅读取的时候区分,写入的时候也是区分的。我就不写例子了,同学们自己尝试吧
超过范围的读取或者写入
console.log(dataView2.getUint16(8));
//RangeError: Offset is outside the bounds of the DataView
读取的范围明显地超过了范围,所以报RangeError
在写入数据的时候,DataView会尽努力把不是number类型的值转成number类型
dataView2.setInt8(0, '4');
console.log(dataView2.getInt8(0)); // 4
定型数组
定型数组和DataView功能相同,也是用来操作ArrayBuffer的。不过它比DataView多了类型约束。DataView在写入或者读取数据的时候,需要指定格式:Int8、Int16。而定型数组在创建的时候,就已经定好了类型。
挺神奇的哈,我们往下看,好好了解一下😎
创建
创建有三种方式:数字,数组,散装参数
1. 数字
const unit16Array1 = new Uint16Array(3);
console.log(unit16Array1.length); // 3
console.log(unit16Array1); // Uint16Array(3) [ 0, 0, 0 ]
- 这里我们创建了一个视图,并且操作数据的单位被定成了两个字节的长度。
- Uint16表示,操作数据的时候,每个单位的数据范围为0 ~ 2^ 16-1。 U表示没有单位;
- Int16表示,每个单位的数据范围为-2^15 ~ 2^15-1。
负数的读取是以补码的格式
- 创建的定型数组里面的内容被初始化为0。
- 定型数组可以访问length属性来获取定型数组的单位。例子中的unit16Array1中有3个可供操作的单位,也可以说数组的长度为3。不过其中的每个单位都是2个字节的长度。所以如果换算成字节的长度,那么长度为6
console.log(unit16Array1.buffer.byteLength); //6
- 定型数组实例访问了buffer属性,这个属性指向视图所对应的ArrayBuffer
千万不要忘记,视图是依托于ArrayBuffer而存在的
- 拿到了对应的ArrayBuffer之后,再去访问其长度,就可以拿到定型数组换算成字节后的长度了
- buffer属性是视图共有的,所以DataView实例对象也有,只不过我上面没讲😁
2. 数组
数组有三种:普通数组、定型数组、ArrayBuffer
是的,定型数组也可以创建其他的定型数组
ArrayBuffer可以创建定型数组。开玩笑?定型数组和DataView一样,也是视图好不好,当然可以通过ArrayBuffer来创建了
- 普通数组
const unit16Array2 = new Uint16Array([1, 2, 3]);
console.log(unit16Array2.length); // 3
console.log(unit16Array2[1]); //2
unit16Array2[1] = 17;
console.log(unit16Array2[1]); // 17
这里我们通过普通数组创建了一个定型数组,长度为3。并且访问下标为1的项,值为2。而后修改其中的值,为17。
我们可以看到,有了定型数组,修改ArrayBuffer中的值,是如此的简单,就像普通数组一样
- 定型数组
// 定型数组
const unit16Array4 = new Uint16Array(unit16Array1);
console.log(unit16Array4.length); // 3
console.log(unit16Array4[1]); //17
const uint8Array3 = new Uint8Array(unit16Array4);
console.log(uint8Array3); //Uint8Array(3) [ 4, 5, 6 ]
我们通过之前创建好的定型数组,来创建新的定型数组,里面的数据都是一模一样的,相当于复制。
并且,可以用不同类型的定型数组来创建新的类型数组
- ArrayBuffer
const bufferArray = new ArrayBuffer(8);
// ArrayBuffer
const unit16Array = new Uint16Array(bufferArray);
// 两个字节一个单位,总共4个单位
console.log(unit16Array.length); // 4
// 也可以访问byteLength,byteOffset,像DataView一样
console.log(unit16Array.byteLength); // 8
我们通过ArrayBuffer实例来创建了一个定型数组,并且数组类型为uint16,所以length为4
这里我们看到了已字节为单位计算定型数组的长度,得到8。之前也有一种方法,只不过从buffer属性上面绕了一圈。
既然得到结果都是一样的,那之前的做法完全是画蛇添足
是的,我是为了讲buffer这个属性而这么干的😉
3. 散装参数
这个创建方式只能通过构造函数的of方法来实现
const unit16Array3 = Uint16Array.of(4, 5, 6);
console.log(unit16Array3.length); // 3
console.log(unit16Array3[1]); //5
创建过程,给of传了3个参数,而不是一个数组。所以我把这种方式称为散装函数😄
啊,好麻烦啊,定型数组的创建方式有3种之多。搞得我讲了这么久🥲
看看它有哪些属性
定型数组也是视图,所以属性和DataView一样。有byteLength,byteOffset,buffer。也有它没有的属性:length
并且定型数组可以向普通数组一样来访问和修改数据,这是DataView办不到的
还有,定型数组不能设置视图的观察偏移位置,和观察的范围。
也好,省得麻烦
定型数组和普通数组的区别
刚才我们看见了,定型数据和普通数组,在操作数据的时候是很像的。它们还有哪些区别或者相似的地方呢
-
创建方式的不同
定型数组创建的方式必须要通过构造函数,并且参数的花样也蛮多的,不过通过数组来创建是大头
-
定型数组不能改变数组的长度,而普通数组是可以的
-
定型数组支持的操作符方法和属性,和普通数组相同
像entries(), every(), fill(), filter(), map(), reduce(), reduceRight(), some()等等,这些方法都有一个特点,就是不会改变数组本身的长度。
所以可以这样记忆,凡是不会改变数组长度的方法,普通数组上面有的,定型数组上面也会有
两个定型数组之间如何快速复制数据
定型数组提供了set(),来实现复制操作
/批量将数组内的数据,导入到另外一个数组里面
const unit8Array1 = new Uint8Array(8);
unit8Array1.set([1, 2, 3, 4]);
console.log(unit8Array1); // Uint8Array(8) [ 1, 2, 3, 4, 0, 0, 0, 0 ]
这里新生成了一个长度为8的数组,然后将普通数组里的数据全部赋到定型数组里面
// 偏移从索引为4的位置开始
const unit8Array2 = new Uint8Array([5, 6, 7, 8]);
unit8Array1.set(unit8Array2, 4);
console.log(unit8Array1); // Uint8Array(8) [ 1, 2, 3, 4, 5, 6, 7, 8 ]
赋值的来源也可以是定型数组。 并且赋值的时候可以指定偏移位置
切割定型数组
定型数组提供了subarray(),来实现切割操作
const subArray1 = unit8Array1.subarray(0, 4);
console.log(subArray1); //Uint8Array(4) [ 1, 2, 3, 4 ]
const subArray2 = unit8Array1.subarray(4);
console.log(subArray2); //Uint8Array(4) [ 5, 6, 7, 8 ]
console.log(unit8Array1); // Uint8Array(8) [ 1, 2, 3, 4, 5, 6, 7, 8 ]
切割的时候需要指定切割的位置,然后生成新的定型数组并返回。而且不会影响原有的定型数组
看到这里,不知道你发现了没有,这个API和slice()很像啊
我立马把上面的代码换成了slice
const subArray1 = unit8Array1.slice(0, 4);
console.log(subArray1); //Uint8Array(4) [ 1, 2, 3, 4 ]
const subArray2 = unit8Array1.slice(4);
console.log(subArray2); //Uint8Array(4) [ 5, 6, 7, 8 ]
console.log(unit8Array1); // Uint8Array(8) [ 1, 2, 3, 4, 5, 6, 7, 8 ]
结果是完全一样的
更多
关于定型数组还有:上溢和下溢、实战两部分内容,这个我们放在下一章再讲,有点累了
你看看几点了,呜呜呜~~
总结:
学这些东西,在术的层面有三个点,抓住了就没问题了:如何创建实例、实例有哪些属性、实例有哪些方法;
在道的层面也有三个点:这个东西是用来解决什么问题的、是怎么解决的问题的、问题完全解决了吗,有没有产生新的问题。
- 定型数组的由来
- ArrayBufer
- DataView
- 定型数组
- 这篇是ArrayBuffer的应用篇--ArrayBuffer实战-识别文件类型