专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

第71节 类型化数组(ArrayBuffer和TypedArray及DataView)-前端开发

ins518 2024-09-30 21:28:05 技术文章 13 ℃ 0 评论

Array存储的对象能动态增多和减少,并且可以存储任何类型值,它的底层实现是比较复杂的,虽然Javascript引擎在内部已经做一些优化,以便更快速的操作数组,但是,随着Web应用程序越来越强大,尤其是一些新增加的功能,例如:canvas、WebGL、音视频的编辑、访问WebSocket的原始数据等,在这些应用中,如果使用传统的数组,就显得力不从心了;此时,可以使用新增的TypedArray类型化数组来操作这些原始的二进制数据,会大大的提高性能。

类型化数组架构:缓冲(Buffer)和视图(View)

为了达到最大的灵活性和效率,JavaScript类型化数组(TypedArray)将它的实现拆分为缓冲(Buffer)和视图(View)两部分;

缓冲(由ArrayBuffer对象实现,也称为缓冲器)描述的是一个数据块,是一个连续的内存区域;缓冲没有格式可言,并且不提供访问其内容的API;为了能访问缓冲对象中所包含的内存内容,需要使用视图,视图提供了上下文,即数据类型、起始偏移量和元素数,可以将缓冲数据转换为实际有类型的数组;

ArrayBuffer:
ArrayBuffer是一种数据类型,用来表示通用的、固定长度的原始二进制数据缓冲区,通常在其他语言中被称为“byte array”;

构造函数:ArrayBuffer(byteLength)
参数byteLength指定了要创建的ArrayBuffer的大小,单位为字节;返回一个ArrayBuffer对象,其内容被初始化为0;

var buffer = new ArrayBuffer(8);
console.log(buffer); // ArrayBuffer(8)

byteLength属性,是只读的,返回ArrayBuffer对象所包含的字节数;其在创建ArrayBuffer对象时就已经被指定了,不可改变;

console.log(buffer.byteLength); // 8

注意,不要把ArrayBuffer对象与正常的数组混淆;

ArrayBuffer和Array区别:

  • Array可以保存任何类型的值,但ArrayBuffer只能保存0和1组成的二进制数据;
  • Array存放在堆中,而ArrayBuffer存储在栈中;
  • ArrayBuffer构建后,大小是固定的,但数组则可以自由增减;
  • Array提供一系列操作数组、元素的API,但ArrayBuffer没有;

ArrayBuffer静态方法:
isView(arg):
如果参数arg是ArrayBuffer的视图实例则返回true,例如TypedArray对象或DataView对象;否则返回false;

var buffer = new ArrayBuffer(8);
var int8Array = new Int8Array(buffer);
console.log(ArrayBuffer.isView(int8Array)); // true
transfer(oldBuffer [, newByteLength]):

返回一个新的ArrayBuffer对象,其内容取自oldBuffer中的数据,并且根据newByteLength 的大小对数据进行截取或补0;

ArrayBuffer实例方法:
slice(begin[, end]):返回一个新的ArrayBuffer;其基于源ArrayBuffer对象数据,从begin(包括)开始,到end(不包括)结束这段区域内容,拷贝并创建一个新ArrayBuffer并返回;
参数:begin:从零开始的字节索引;end:可选,结束的字节索引,但不包含end;
如果没指定end,新的ArrayBuffer将包含源ArrayBuffer从头到尾的所有字节;

var buffer = new ArrayBuffer(16);
var slicedBuffer = buffer.slice(4,12); // 从4到11,不包括12
console.log(slicedBuffer); // ArrayBuffer(8)
console.log(buffer.byteLength); // 16
console.log(slicedBuffer.byteLength); 8

如果begin或end是负数,则指的是从数组末尾开始的索引,而不是从头开始;如果新ArrayBuffer的长度在计算后为负,它将强制为0;

不能直接操作ArrayBuffer的内容,而是要通过类型化数组视图TypedArray对象或一个描述缓冲数据格式的DataView对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读、写ArrayBuffer缓冲区的内容;

操作ArrayBuffer数据的视图,包括两种,一是TypedArray类型化数组视图,另一个是DataView数据视图;

TypedArray类型化数组视图:
在读取或存储ArrayBuffer内存空间中的数据,可以使用不同的方式,这就叫做视图;
一个类型化数组(TypedArray)描述了一个底层的二进制数据缓冲区的一个类数组对象,其提供了一种用于操作原始二进制数据的机制;
其也是数组,是一个字节数组,其元素被设置为特定类型的值;

没有名为TypedArray的构造函数,它是若干个具体特定类型的构造函数、具有描述性的名字和所有常用的数值类型的视图组成的;
类型 值范围 大小(bytes) 说明
Int8Array -128 to 127 1 8位有符号整数
Uint8Array 0 to 255 1 8位无符号整数
Uint8ClampedArray 0 to 255 1 8位无符号整数
Int16Array -32768 to 32767 2 16位有符号整数
Uint16Array 0 to 65535 2 16位无符号整数
Int32Array -2147483648 to 2147483647 4 32位有符号整数
Uint32Array 0 to 4294967295 4 16位无符号整数
Float32Array -3.4E38 to 3.4E38 4 32位IEEE浮点数(7位有效数字)
Float64Array -1.8E308 to 1.8E308 8 64位IEEE浮点数(16位有效数字)
BigInt64Array -2^63 to 2^63 - 1 8 64位有符号整数
BigUint64Array 0 to 2^64 - 1 8 64位无符号整数

其中Uint8ClampedArray是一种特殊类型的数组,它仅操作0到255之间的数值;

这些构造函数,在实例化时有多种形式,最简单的是传入一个length参数,表示包含的元素个数,如:

var int8 = new Int8Array(8);
console.log(int8); // Int8Array(8)
var int16 = new Int16Array(8);
console.log(int16); Int16Array(8)

一旦创建了类型化数组,就可以像操作数组一样,操作类型化数组,例如使用方括号对类型化数组进行读写操作,如:

var int16 = new Int16Array(4);
console.log(int16); // Int16Array(4),4个元素均为0
int16[0] = 10;
int16[1] = 20;
int16[2] = 30;
int16[3] = int16[0] + int16[1];
console.log(int16[3]); // 30
// 还可以使用for循环
var int32 = new Int32Array(4);
for(var i=0; i<int32.length; i++){
int32[i] = i * 2;
}
// 4个元素分别为0、2、4、6,每个元素4个字节,总大小是16
console.log(int32);
var bytes = new Uint16Array(1024); // 2KB
for(var i=0; i<bytes.length; i++){
// bytes[i] = i;
bytes[i] = i & 0xFF; // 设置为索引的低8位值 0xFF为255
}
console.log(bytes);

类型化数组虽然是数组,但是它和常规数组还是有一定的区别,如:类型化数组中的元素都是数字,使用构造函数在创建类型化数组时,就决定了数组中的数字(有符号或无符号或浮点数)的类型和大小;
类型化数组视图有固定的长度,因此它的length属性为只读,并且缺少pop()、push()、shift()等能更改数组长度的方法;

在创建类型化数组视图的时候,数组中的元素总是默认初始化为0;
每种类型化数组视图都以不同的方式表示数据,而同一数据视选择的类型的不同有可能会占用一或多个字节;例如8Byte的内存空间,可以保存8个Int8Array或Uint8Array,或者4个Int16Array或Uint16Array,或者2个Int32Array、Uint32Array或Float32Array,或者1个Float64Array;

var int8 = new Int8Array(8);
var uint8 = new Uint8Array(8);
var int16 = new Int16Array(4);
var uint16 = new Uint16Array(4);
var int32 = new Int32Array(2);
var uint32 = new Uint32Array(2);
var float32 = new Float32Array(2);
var float64 = new Float64Array(1);

在为类型化数组视图对象元素赋值时,如果指定的字节数放不下相应的值,则实际保存的值是与最多表示的数的个数的模;
例如,无符号16位整数所能表示得最多的数的个数是65536,如果你想保存65536,那实际保存的值是0,如果你想保存65537,那实际保存的值是1,以此类推;如:

var uint16 = new Uint16Array(10);
uint16[0] = 65537;
console.log(uint16[0]); // 1

数据类型不匹配时并不会抛出错误,所以必须要保证所赋的值不会超过相应元素的字节限制;

转换为普通数组:
在处理完一个类型化数组后,有时需要把它转为普通数组,以便可以像普通数组一样去操作;可以调用Array.from实现转换,如果不支持Array.from的话,还可以:

var typedArray = new Uint8Array([1,2,3,4]),
normalArray = Array.prototype.slice.call(typedArray);
console.log(normalArray); // Array

Uint8ClampedArray类型:
由于各个类型化数组的元素都会有规定的数值范围,因此超出该范围就会溢出;其中Uint8ClampedArray的溢出处理较为特殊,它的数值范围在0到255之间,如果缓冲区所存的值超出该范围,那么就会替换这个值,例如小于0的数值被转换成0,而大于255的数值则被转换成255,如:

var int8 = new Int8Array(2);
int8[0] = 256;
console.log(int8[0]); // 0
var clamped = new Uint8ClampedArray(2);
clamped[0] = 256;
console.log(clamped[0]); //2255

例如,网页Canvas元素输出的二进制像素数据,就是Uint8ClampedArray,如:

<canvas id="myCanvas" width="300" height="300"></canvas>
<script>
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.rect(20,20,200,100);
ctx.closePath();
ctx.strokeStyle = "#F00";
ctx.fillStyle = "#CCC";
ctx.stroke();
ctx.fill();
var imageData = ctx.getImageData(0,0, 200, 100);
console.log(imageData); // imageData
// imageData里面有个data属性,其是一个Uint8ClampedArray对象
var uint8 = imageData.data;
console.log(uint8);
</script>

当处理与图形相关的数字,或者与数学相关的数字的时候,类型化数组也很有用:

var matrix = new Float64Array(3); // 一个3X3的矩阵
var point3d = new Int16Array(3); // 3D空间中的一点
var rgba = new Uint8Array(4); // 一个4字节的RGBA像素值
var sudoku = new Uint8Array(81); // 一个9X9的数独板

所有的类型化数组都继承自TypedArray类,因而类型化数组可以使用相同的构造函数参数来实例化,形如:

new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);

第一种形式:当传入length参数时,一个内部的数组缓冲区会被创建在内存中,该缓存区的大小是传入的length乘以数组中每个元素所占用的字节数,每个元素的值都被初始化0,,如:

var int8 = new Int8Array(8);
console.log(int8);
var int16 = new Int16Array(10);
console.log(int16);

创建类型化数组视图的同时,会自动创建合适容量的buffer缓冲区

第二种形式:typedArray:可以传入一个任意类型化数组视图typedArray对象作为参数,该typedArray对象数据会被复制到一个新的类型化数组中;

var int16Array = new Int16Array(20);
var newInt16Array = new Int16Array(int16Array);
console.log(newInt16Array); // Int16Array(20)

新的类型化数组视图将会和原数组视图具有相同的长度;
typedArray中的每个值在被复制到新的数组视图之前,会被转化为相应类型的构造函数;

// ...
var uint8Array = new Uint8Array(int16Array);
console.log(uint8Array); // Uint8Array(20)

第三种形式:传入一个object作为参数时,此object必须实现length属性,且如果指定元素的话,下标必须是数值,不能是字符串,值也只能为数字,或者能隐式转换成数字;

var int16Array = new Int16Array({});
console.log(int16Array); // byteLength: 0
var int16Array = new Int16Array({0:0,3:true,5:"5",8:25,length:20});
console.log(int16Array); // byteLength: 40

数组也是对象,所以也可以基于普通数组创建类型化数组,如:

var view = new Uint8Array([1,2,3,4,260]);
console.log(view);

第四种形式:buffer[, byteOffset[, length]]:
buffer表示一个ArrayBuffer对象,byteOffset是指作为起点的字节偏移量,length参数指定了元素个数;如果后两者都未传入,那么整个buffer都会被读取,如果仅仅忽略length,那么会从byteOffset处读取到buffer的末尾;
用这种形式,就可以基于ArrayBuffer创建TypedArray对象,如:

var buffer = new ArrayBuffer(16); // 16个字节空间
// 读取整个buffer
// buffer: ArrayBuffer(16), byteLength: 16, byteOffset: 0, length: 16
var int8 = new Int8Array(buffer);
// 读取从第11个字节到末尾的buffer
// buffer: ArrayBuffer(16), byteLength: 6, byteOffset: 10, length: 6
var int8 = new Int8Array(buffer, 10);
// 读取从第5个字节,长度为8的元素个数
// buffer: ArrayBuffer(16), byteLength: 8, byteOffset: 4, length: 8
var int8 = new Int8Array(buffer, 4, 8);
console.log(int8);

基于同一个ArrayBuffer,创建不同的类型化数组视图,如:

var buffer = new ArrayBuffer(16);
// Int32Array(4) byteLength: 16, byteOffset: 0, length: 4
var int32 = new Int32Array(buffer);
// 创建一个Uint8Array视图,开始于字节2,直到缓冲区的末尾
// Uint8Array(14) byteLength: 14, byteOffset: 2, length: 14
var uint8 = new Uint8Array(buffer, 2);
// 创建一个Int16Array视图,开始于字节2,2个元素
// Int16Array(2) byteLength: 4, byteOffset: 2, length: 2
var int16 = new Int16Array(buffer, 2, 2);

以上的两个不同类型化数组都是同一数据的以不同格式展示出来的视图;除此之外,可以用以上所说的任何一种视图;
基于同一个ArrayBuffer,创建的不同的类型化数组视图,使用了同一个数据缓冲区(ArrayBuffer),如:

console.log(int32.buffer === uint8.buffer); // true
console.log(uint8.buffer === int16.buffer); // true
console.log(int16.buffer === buffer); // true

只要任何一个视图对内存有所修改,就会在其它几个视图上反应出来,另外,由于这些视图的类型不同的,所以操作ArrayBuffer后,结果也是不同的,如:

// ...
int16[0] = 32;
console.log(int32[0]); // 2097152

两个相互重叠的视图所占据的内存空间,操作其中的值以最后一次写入的为主(其实前面的操作已经说明了这个问题了,也就是后面的会覆盖前面的,再看看吧),如:

var buffer = new ArrayBuffer(4); // 4个字节
var int8 = new Int8Array(buffer); // 从0到末尾,4个元素
var int16 = new Int16Array(buffer, 2); // 从2到末尾,1个元素
int8[0] = 1;
int8[1] = 2;
int8[2] = 3;
int8[3] = 4;
console.log(int8);
console.log(int16);
int16[0] = 500;
console.log(int16);
console.log(int8[2]); // -12
console.log(int8[3]); // 1

复合视图:
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”;如:

var buffer = new ArrayBuffer(24);
var idView = new Uint32Array(buffer, 0, 1);
var usernameView = new Uint8Array(buffer, 4, 16);
var amountDueView = new Float32Array(buffer, 20, 1);

buffer属性:
只读,返回该TypedArray(类型化数组视图对象)的内存区域对应的ArrayBuffer对象;

var buffer = new ArrayBuffer(16);
var view = new Uint8Array(buffer);
console.log(view.buffer === buffer); // true

创建类型化数组的同时,会创建一个数据缓冲区,即ArrayBuffer,由其buffer属性所引用;如:

var int8 = new Int8Array([1,2,3,4]);
console.log(int8.buffer); // ArrayBuffer(4)

类型化数组中所有数据的读写,都是针对该buffer属性所引用的ArrayBuffer,如:

var int8 = new Int8Array([1,2,3,4]);
console.log(int8[0]); // 1
var buffer = int8.buffer;
var t1 = new Int8Array(buffer);
var t2 = new Int16Array(buffer);
console.log(t1[0], t2[0]); // 1 513
t1[0] = 20;
console.log(int8[0]); // 20
console.log(t2[0]); // 532

byteLength属性和byteOffset属性:
byteLength属性返回类型化数组占据的内存总长度,单位为字节;
byteOffset属性返回类型化数组从底层ArrayBuffer对象的哪个字节开始;
这两个属性都是只读属性;

类型化数据视图对象还具有一个常量BYTES_PER_ELEMENT,表示这种数据类型的元素所占据的字节数,如:

var int8Array = new Int8Array(16);
console.log(int8Array.BYTES_PER_ELEMENT); // 1
console.log(new Float32Array(16).BYTES_PER_ELEMENT); // 4

可以利用这个属性来辅助初始化,如:

var buffer = new ArrayBuffer(30); // 30个字节
console.log(buffer);
var int8 = new Int8Array(buffer, 4, 10);
console.log(int8); // Int8Array(10), byteLength: 10
var uint16 = new Uint16Array(buffer, int8.byteOffset + int8.byteLength,
(buffer.byteLength - int8.byteLength - int8.byteOffset) /
Uint16Array.BYTES_PER_ELEMENT);
console.log(uint16);

TypedArray类型化数组视图的方法:
TypedArray视图对象拥有常规的Array对象的大部分方法,如:

var int16 = new Int16Array(20);
for(var i=0; i<int16.length; i++){
int16[i] = i;
}
console.log(int16);
int16.forEach(function(v, i, arr){
arr[i] = v * 2;
});
console.log(int16);
var a = int16.map(function(v, i, arr){
return ++v;
});
console.log(a);
console.log(int16.join());

类型化数组有固定的长度,因此它的length属性为只读,所以类似于pop()、push()、shift()等会更改数组长度的方法,不能使用,如:

int16.pop(); // int16.pop is not a function

除此这些,它还定义了一些用于设置和获取整个数组内容的方法;

set(array | typedarray [, offset])方法:
用于复制数组,也就是将一段内容完全复制到另一段内存区域中;
参数:
array,拷贝数据的源数组,源数组的所有值都会被复制到目标数组中,除非源数组的长度加上偏移量超过目标数组的长度,而在这种情况下会抛出异常;
typedarray,如果源数组是一个类型化数组,会将其的指向的内存区域内容拷贝到目标内存区域中;
offset,可选,指定从自己的什么位置开始使用源数组的值;如果忽略该参数,则默认为0。

如:

// 从数组中拷贝
var uint8 = new Uint8Array(20);
var arr = new Array(0,1,2,3); // 一个4个字节的数组
uint8.set(arr); // 将arr复制到开始
uint8.set(arr, 4); // 在另一个偏移量处再次复制它们
uint8.set([5,6,7,8], 8); // 或直接从一个常规数组中复制值
console.log(uint8);
uint8.set(arr,18);
// 从TypedArray中拷贝
var a = new Uint8Array(8); // 8个元素
for(var i=0,len=a.length; i<len; i++){
a[i] = i * 2; // 给个值
}
var b = new Uint8Array(8); // 8个元素
b.set(a);
console.log(b); // 0,2,4...
var c = new Uint16Array(10);
c.set(a, 2);
console.log(c); // 0,0,0,2,4,6,8...
var d = new Uint16Array(10);
// 抛出RangeError: offset is out of bounds
d.set(a,4);

subarray([begin [,end]])方法:
基于数组缓冲器的子集,创建一个新的视图,参数为开始元素的索引和结束元素的索引,返回的类型与源视图类型相同;
参数:
begin,可选,元素开始的索引,开始索引的元素将会被包括,若该值没有传入,将会返回一个拥有全部元素的数组;
end,可选,元素结束的索引,结束索引的元素将不会被包括,若该值没有传入,从 begin 所指定的那一个元素到数组末尾的所有元素都将会被包含进新数组中,如:

var uint8 = new Uint8Array(16);
// 设点值
for(var i=0; i<uint8.length; i++){
uint8[i] = i;
}
console.log(uint8);
var a = uint8.subarray();
console.log(a); // byteLength: 16, byteOffset: 0, length: 16
var b = uint8.subarray(2);
console.log(b); // byteLength: 14, byteOffset: 2, length: 14
var c = uint8.subarray(4,8);
console.log(c); // byteLength: 4, byteOffset: 4, length: 4

subarray()方法不会创建数据的副本,它只是直接返回原数组的其中一部分内容,所以新数组视图和源数组视图共享同一个ArrayBuffer,所以,改动数组的内容将会影响到原数组,反之亦然,如:

// ...
console.log(v1.buffer === v2.buffer); // true
v2[0] = 18;
console.log(v1[3]); // 18

ArrayBuffer与字符串的互相转换:

// ArrayBuffer转为字符串,参数为ArrayBuffer对象
function uint162str(buffer) {
// fromCharCode()方法可以返回由指定的UTF-16代码单元序列创建的字符串
return String.fromCharCode.apply(null, new Uint16Array(buffer));
}
var uint16 = new Uint16Array([97,98,99,100]);
console.log(uint162str(uint16)); // abcd
var uint8 = new Uint8Array([97,98,99,100]);
console.log(uint162str(uint8)); // abcd
var str = "大师哥王唯";
var codeArr = [];
for(var i=0,len=str.length; i<len; i++){
// charCodeAt() 方法返回一个数字,指示给定索引处的字符的Unicode值
codeArr.push(str.charCodeAt(i));
}
console.log(codeArr);
// 这样的Unicode值可以直接拿过来使用
console.log(uint162str(codeArr)); // 大师哥王唯
// 字符串转为ArrayBuffer或Unit16Array对象,参数为字符串
function str2uint16(str) {
var buffer = new ArrayBuffer(str.length*2); // 每个字符占用2个字节
var view = new Uint16Array(buffer);
for (var i=0, strLen = str.length; i<strLen; i++) {
view[i] = str.charCodeAt(i);
}
// return buffer; // 或return view;
return view;
}
var str = "Web前端开发";
console.log(str2uint16(str));

XHR2中的ArrayBuffer:
ArrayBuffer的应用特别广泛,无论是WebSocket、WebAudio还是Ajax等,前端方面只要是处理大数据或者想提高数据处理性能,就会用到ArrayBuffer;
在XHR2中,增加了responseType属性,用于设置响应的数据格式,可选的值有“text”、“arraybuffer”、“blob”、“document”和“json”,如:
后端buffer.php:

<?php
// echo 1234567; // 每个数字用一个字节,共7个字节
// echo "大师哥王唯人"; // 每个中文用了3个字节,共18字节
echo "wang大师哥18王唯"; // 中文用了3个字节,共21个字节

如:

function ab2str(buffer,callback){
// 这里借用了我们马上要使用Blob对象和FileReader对象
var blob = new Blob([buffer]);
var reader = new FileReader();
reader.readAsText(blob, 'UTF-8');
reader.onload = function(event){
if(callback){
callback.call(null, reader.result);
}
}
}
function handler(result){
console.log(result);
}
var xhr = new XMLHttpRequest();
xhr.open('GET', 'buffer.php', true);
xhr.onload = function(event){
var buffer = this.response;
ab2str(buffer, handler);
};
xhr.responseType = "arraybuffer";
xhr.send(null);

或者使用TextDecoder对象,如:

xhr.onload = function(event){
var buffer = this.response;
var decoder = new TextDecoder("UTF-8");
var uint8 = new Uint8Array(buffer);
console.log(decoder.decode(uint8));
};

Endianness(字节序):

端序又称字节序(Endianness),表示多字节中的字节排列方式;

大端字节序:高位字节在前,低位字节在后,也就是按照从高位到低位的顺序排列的,也称为高位优先(或简称为大端序);这是一种最符合人类阅读习惯的方式;

小端字节序与大端字节序正好相反,是指字节的最低有效位在最高有效位之前,也就是按照从低到高位的顺序排列,也称为低位优先(或简称为小端序);

例如:十进制数:16 909 060,对应的32位二进制数原码为:
00000001 00000010 00000011 00000100

使用大端序表示,就等于它的原码;
使用小端序表示,即:
00000100 00000011 00000010 00000001

例如数字10,如果用16位二进制表示,为00000000 00001010(原始);
如果用大端序存储的话,为00000000 00001010(从左往右存)
如果用小端序存储的话,为00001010 00000000(从右往左存)
如果用大端序读取的话,为00000000 00001010(从左往右读)
如果用小端序读取的话,为00001010 00000000(从右往左存)

换算成16进制就是(0000 0000 0000 1010)000A;
用大端序存储的话,该值表示为:000A;
用小端序存储的话,该值表示为:0A00;

英特尔CPU处理器和多数浏览器采用的都是小端序(低位优先),这也是我们本地默认的方式;
对于Int8Array和Uint8Array,由于它们是单字节的,所以涉及不到字节序的问题;

如果不确定正在使用的计算机的字节序,可以采用下面的判断方式,如:

var littleEndian = (function() {
var buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true);
return new Int16Array(buffer)[0] === 256;
})();
console.log(littleEndian); // true
// 或者,用更简单的方式:
var little_endian = new Int8Array(new Int32Array([1]).buffer)[0] === 1;
console.log(little_endian); // true

虽然大多数CPU架构都采用小端序(低位优先),但是,很多的网络协议以及有些二进制文件格式,采用的是大端序(高位优先)的字节顺序;所以,当从文件中读取或从网络中下载字节时,就需要考虑平台的字节顺序;

通常,处理外部数据的时候,可以使用Int8Array和Uint8Array将数据视为一个单字节数组,不要使用其他的多字节字长的类型化数组;
取而代之的是可以使用DataView类,这个类定义了采用显式指定的字节顺序从ArrayBuffer中读写其值的方法;

DataView数据视图:
是一种底层接口,它提供有可以操作缓冲区中任意数据的读写方法;其是为了解决各种硬件设备、数据传输等对默认字节序的设定不一样而导致解码时会发生的混乱问题,其可以让开发者在对内存进行读写时,手动设定字节序的类型;

构造函数:
DataView(buffer [, byteOffset [, byteLength]]):返回一个表示指定数据缓存区的DataView对象;
参数:
buffer:一个已经存在的ArrayBuffer或SharedArrayBuffer对象,即DataView对象的数据源;
byteOffset:可选,buffer中的字节偏移量);如果未指定,则默认从第一个字节开始;
byteLength:可选,字节长度;如果未指定,这个视图的长度将匹配buffer的长度,如:

var buffer = new ArrayBuffer(16);
// 使用整个ArrayBuffer
var view = new DataView(buffer);
console.log(view); // DataView(16)
// 从9开始的新视图
var view1 = new DataView(buffer, 9);
console.log(view1);
// 从12开始,长度为4的字节的新视图
var view2 = new DataView(buffer, 12, 4);
// 设置索引12位置值为42,并打印出来
// view2.setInt8(0, 42); // 42
// view2.setInt16(0,42,true); // 42
// view2.setInt16(0,42,false); // 0
view2.setInt16(0,42); // 0
console.log(view2.getInt8(0)); // 42 或 0

DataView实例属性:

  • buffer:只读,此视图引用的ArrayBuffer对象,即在构建时传入的buffer;
  • byteLength:只读,此视图从原ArrayBuffer开始的长度(字节),即在构建时传入的ByteLength;
  • byteOffset:只读,此视图从其ArrayBuffer开始的偏移量(字节),也是在构建时指定;
// ...
console.log(view1.byteOffset); // 0
console.log(view1.byteLength); // 16
console.log(view1.buffer === buffer); // true

DataView实例方法:
读取或写入DataView的时候,要根据实现操作的数据类型,选择相应的getter和setter方法;
getter方法:

  • getInt8(byteOffset):获取一个有符号8位整数(字节),该整数位于距视图开头的指定字节偏移量byteOffset处;
  • getUint8(byteOffset):(读取1个字节),返回一个无符号的8位整数;
  • getInt16(byteOffset, littleEndian):(读取2个字节),返回一个16位整数;
  • getUint16(byteOffset, littleEndian):(读取2个字节),返回一个无符号的16位整数;
  • getInt32(byteOffset, littleEndian):(读取4个字节),返回一个32位整数;
  • getUint32(byteOffset, littleEndian):(读取4个字节),返回一个无符号的32位整数;
  • getFloat32(byteOffset, littleEndian):(读取4个字节),返回一个32位浮点数;
  • getFloat64(byteOffset, littleEndian):(读取8个字节),返回一个64位浮点数;
  • getBigInt64(byteOffset, littleEndian):
  • getBigUint64(byteOffset, littleEndian):

以上一系列get方法的byteOffset参数都是一个字节偏移量,表示从哪个字节开始读取;

var buffer = new ArrayBuffer(16);
var dataView = new DataView(buffer);
console.log(dataView.getInt8(1)); // 0
// 从第1个字节读取一个16位无符号整数
var v1 = dataView.getUint16(1);
// 从第4个字节读取一个16位无符号整数
var v1 = dataView.getUint16(3);

如果一次读取两个或两个以上字节,就必须明确数据的存储方式:小端字节序还是大端字节序;默认情况下,DataView的get方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须指定第二个参数为true,如:

// 小端字节序
var v1 = dataView.getUint16(1, true);
// 大端字节序
var v2 = dataView.getUint16(3, false);
// 大端字节序
var v3 = dataView.getUint16(3);

setter方法:
setInt8(byteOffset, value):在距视图开头的指定字节偏移byteOffset处,存储有符号8位整数值(字节);

  • setUInt8(byteOffset, value):写入1个字节的8位无符号整数;
    setInt16(byteOffset, value[, littleEndian]):
    setUInt16(byteOffset, value[, littleEndian]):
    setInt32(byteOffset, value[, littleEndian]):
    setUInt32(byteOffset, value[, littleEndian]):
    setFloat32(byteOffset, value[, littleEndian]):
    setFloat64(byteOffset, value[, littleEndian]):
    setBigInt64(byteOffset, value[, littleEndian]):
    setBigUint64(byteOffset, value[, littleEndian]):

以上的setter方法,接受两个参数,第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据;

dataView.setInt8(1, 3);
console.log(dataView.getInt8(1)); // 3

不同类型的数据,大小是不一样的,例如Uint8的整数需要1Byte,而Float32则要用4Byte;

var buffer = new ArrayBuffer(16); // 16Byte
var view = new DataView(buffer);
// byteLength: 16, byteOffset: 0
console.log(view);
// 使用16位无符号整型设置索引为0的字节
view.setUint16(0, 25);
console.log(view.getInt8(0)); // 0
console.log(view.getUint16(0)); // 25
// 不能从字节1开始,因为16位整数要用2Byte
// view.setUint16(1, 50); // 如果这样的话,打印getUint16(0)的结果就为0了
// console.log(view.getInt8(0)); // 0
// console.log(view.getUint16(0)); // 0
view.setInt8(0, 18);
view.setInt8(1, 28);
console.log(view.getUint16(0)); // 4636

值得注意的是,在DataView视图中,读写超出其实例化时的范围的值时,都会发生错误,这跟的类型化数组视图不一样,所以在使用时需要更加谨慎,如:

view.setInt8(101,11); // 异常 RangeError
view.getInt8(101); // 异常 RangeError

对于那些写入两个或两个以上字节的方法,需要指定第三个参数littleEndian,其是个布尔值,表示在读写数值时是否采用小端字节序而不是大端字节序(即将数据的最低有效位保存在高内存地址中),默认为false,即表示使用大端字节序写入,true表示使用小端字节序写入,如:

var buffer = new ArrayBuffer(16);
var view = new DataView(buffer);
// 在索引为0字节,以大端字节序写入值为25的32位整数
view.setInt32(0, 25);
// 证明一下,大端读取1个字节
console.log(view.getInt8(3)); // 25
// 32位有符号整数,大端读取,一次读4个字节
console.log(view.getInt32(0)); // 25
// 小端序(低位优先)读取4个字节
// 读的顺序为:00011001 00000000 00000000 00000000
console.log(view.getInt32(0, true)); // 419430400
// 在小端序写入18的32位整数;
// 小端序写入顺序:00011001 00000000 00000000 00000000
view.setInt32(0, 18, true);
// 大端序读取1个字节,读到的是00011001
console.log(view.getInt8(0)); // 18
// 大端序读取4个字节,读取的顺序为:00011001 00000000 00000000 00000000
console.log(view.getInt32(0)); // 301989888
// 在第5个字节,以默认(大端序)写入值为35的32位整数
// 大端序与原码一致;
view.setInt32(4, 35, false);
// 以大端读,索引为7的字节,即00100011
console.log(view.getInt8(7)); // 35
// 以大端读,索引为4的字节,读4个字节,即:
// 00000000 00000000 00000000 00100011
console.log(view.getInt32(4)); // 35
// 以小端读,索引为4的字节,读4个字节,即:
// 00100011 00000000 00000000 00000000
console.log(view.getInt32(4, true)); // 587202560
// 在索引为8的字节,以小端字节序写入值为2.5的32位浮点数
view.setFloat32(8, 2.5, true);
// 以小端读,结果是:
// 01000000 00100000 00000000 00000000
console.log(view.getFloat32(8, true)); // 2.5
// 以大端读索引为10和11的字节,即00100000和01000000
console.log(view.getInt8(10),view.getInt8(11)); // 32 64
// 以大端从索引为8开始读4个字节,结果是:
// 00000000 00000000 00100000 01000000
console.log(view.getInt32(8)); // 8256
console.log(view.getFloat32(8)); // 1.156912012146569e-41

64位整数值:
因为JavaScript目前不包含对64位整数值支持的标准,所以DataView不提供原生的64位操作;作为变通,可以实现一个getUint64()函数,以获得精度高达Number.MAX_SAFE_INTEGER的值,可以满足某些特定情况的需求,如:

function getUint64(dataview, byteOffset, littleEndian) {
// 将 64 位整数值分成两份 32 位整数值
var left = dataview.getUint32(byteOffset, littleEndian); // 前4个字节
var right = dataview.getUint32(byteOffset+4, littleEndian); // 后4个字节
var combined = littleEndian? left + 2**32*right : 2**32*left + right;
// 判断一下,是否超过最大安全数的范围
if (!Number.isSafeInteger(combined))
console.warn(combined, '超过最大安全整数,精度可能会降低');
return combined;
}
var buffer = new ArrayBuffer(8);
var dataview = new DataView(buffer);
dataview.setInt32(0,18);// 保存了一个32位整数,用了4个字节
dataview.setInt32(4,25);// 在其后的索引为4的字节开始存一个32位整数,用了4个字节
console.log(getUint64(dataview,0,false)); // 77309411353

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表