文章出自个人博客https://knightyun.github.io/2020/03/09/js-binary-data,转载请申明
编程江湖,终日血雨腥风,论及二进制数据,又有多少豪杰谈笑风生,风生水起,水起船高,高深莫测…
不扯远了,想必谈到二进制数据,大家联想到的就会是 1010110110001
或者 00000000 11111111 00000101
这样的数据流;而这武林之中,号称三剑客之一的 JavaScript,在其行走江湖之际(日常开发),可能厮杀(处理)最多的类型就是直观的数字、字符串或者对象等;那么与极少露面的隐士(二进制)狭路相逢之时,它又将作何应对(描述与处理二进制数据)呢?是波澜壮阔,还是全身而退,抑或是力挽狂澜,且听本文中分解。
ArrayBuffer
未曾识得英雄面,只缘身在此山中;先来了解第一个概念,ArrayBuffer
表示的是一个原始二进制数据缓冲区(buffer),长度固定,并且内容是只读的;如果需要执行写操作,那么需要使用 类型化数组(TypedArray
)或者 数据视图(DataView
) 来实现;
知己知彼,方可百战百胜;在 JavaScript 中与二进制数据接触最紧密的可能就是 ArrayBuffer
了,之前讲目的是要描述和操作二进制数据,那么就要把这些数据先存放到某个地方,然后才能对其进行操作,这里的 ArrayBuffer 缓冲区就可以被看成这么一种地方;当然,可能最直观的方式就是将其保存到字符串中,如 "101011011"
,又或者存入数组,如 [1,0,1,0,1,1,0,1]
,这样确实是方便人类了,但是机器执行的效率也降低了,因为毕竟字符和数组是另外两种基本类型,并且也不是专门为此设计的;所以,就出现了专门为缓冲数据设计的 ArrayBuffer
,并通过结合视图来提供一个访问和操作数据的接口;
语法
实例化 ArrayBuffer
构造函数时,只接受一个参数,就是要创建的 Arraybuffer 的大小,单位是字节,不指定的话默认为 0;同时它也提供了一个实例属性 byteLength
(只读),实现对当前 ArrayBuffer 字节值的访问;
举例:
var buffer = new ArrayBuffer(3);
console.log(buffer.byteLength); // 3
另外,由于 ArrayBuffer 只是负责创建这么一段数据区域,并没有提供初始化赋值的接口,所以这 n 字节的数据都为空,即都置 0;
方法
由于 ArrayBuffer 构造函数本身是用于创建数据缓冲区,并且数据只读,所以提供的属性和方法也只有少数几个;
.slice()
用于返回一个新的缓冲区,语法为 .slice(start, end)
,即以当前缓冲区为母本,从索引为 start 的位置开始,到 end 位置结束(end 位置不包含在内),然后复制并返回这一区段的数据;其用法大致与 Array.prototype.slice()
类似,举例说明:
var buffer1 = new ArrayBuffer(5);
var buffer2 = buffer1.slice(0, 3);
var buffer3 = buffer1.slice(2);
var buffer4 = buffer1.slice(1, -1);
console.log(buffer2.byteLength); // 3
console.log(buffer3.byteLength); // 3
console.log(buffer4.byteLength); // 3
ArrayBuffer.isView()
该方法用来判断所提供的参数值是否是一种 ArrayBuffer
视图,比如类型化数组(TypedArray)和数据视图(DataView),例如:
console.log(ArrayBuffer.isView()); // false
console.log(ArrayBuffer.isView([1, 2, 3])); // false
console.log(ArrayBuffer.isView(new ArrayBuffer(3))); // false
console.log(ArrayBuffer.isView(new Int8Array())); // true
console.log(ArrayBuffer.isView(new Uint32Array(3))); // true
console.log(ArrayBuffer.isView(new DataView(new ArrayBuffer()))); // true
类型化数组(TypedArray)
概述
工欲善其事必先利其器;前面提到操作 ArrayBuffer
创建的数据缓冲区需要使用视图(view)实现,类型化数组就是这么一个描述二进制数据缓冲区(buffer)的视图(view),这个视图是一个 类数组。另外,不存在 TypedArray()
这个构造函数,它指的是一类数组,因此它有多种实现,即多个类型化数组构造器函数;可以姑且理解为 水果 之于 苹果 和 香蕉,水果指的是一类食物,都知道并不存在名为 水果 的一种具体食物,但是 苹果 和 香蕉 是具体存在的;
有效的类型如下:
Int8Array(); // 8 位二进制有符号整数
Uint8Array(); // 8 位无符号整数(超出范围后从另一边界循环)
Uint8ClampedArray(); // 8 位无符号整数(超出范围后为边界值)
Int16Array(); // 16 位二进制有符号整数
Uint16Array(); // 16 位无符号整数signed
Int32Array(); // 32 位二进制有符号整数
Uint32Array(); // 32 位无符号整数
Float32Array(); // 32 位 IEEE 浮点数(7 位有效数字,如 1.1234567)
Float64Array(); // 64 位 IEEE 浮点数(16 有效数字,如 1.123...15)
BigInt64Array(); // 64 位二进制有符号整数
BigUint64Array(); // 64 位无符号整数
语法:
万法不离其宗,一招一式都有迹可循;后面就都以 Int8Array()
为例进行说明,以下代码展示了可以传入的参数类型:
new Int8Array(); // ES2017 中新增
new Int8Array(length);
new Int8Array(typedArray);
new Int8Array(object);
new Int8Array(buffer [, byteOffset [, length]]);
无参数
最好的招式是没有招式;实例化构造函数时不传入任何参数,则返回一个空的类型化数组:
var i8 = new Int8Array();
console.log(i8); // Int8Array []
console.log(i8.length); // 0
console.log(i8.byteLength); // 0
console.log(i8.byteOffset); // 0
length
一寸长一寸强;传入一个数字类型的参数,表示申明类型化数组中元素的个数:
var i8 = new Int8Array(3);
var _i8 = new Int8Array('3'); // 字符串会先被转换成数字
console.log(i8); // Int8Array(3) [0, 0, 0]
console.log(_i8); // Int8Array(3) [0, 0, 0]
console.log(i8.length); // 3
console.log(i8.byteLength); // 3
console.log(i8.byteOffset); // 0
typedArray
好招不怕效仿;当传入的一个参数同样是一个类型化数组时,则返回一个原类型数组的拷贝(不是引用):
var i8 = new Int8Array(3);
var _i8 = new Int8Array(i8);
console.log(i8 == _i8); // false
console.log(i8 === _i8); // false
console.log(_i8); // Int8Array(3) [0, 0, 0]
object
海纳百川有容乃大;使用该参数时类似于用 TypedArray.prototype.from()
方法创建的类型数组,同时该方法也和 Array.from()
方法类似,即这个 object
参数是一个类数组的对象,或者是可迭代的对象;举例:
// 数组
var i81 = new Int8Array([1, 2, 3]);
console.log(i81);
// Int8Array(3) [1, 2, 3]
// 等价的操作
var i81 = Int8Array.from([1, 2, 3]);
// Int8Array(3) [1, 2, 3]
// 类数组
var i82 = new Int8Array({
0: 1,
1: 2,
2: 3,
length: 3
});
console.log(i82);
// Int8Array(3) [1, 2, 3]
// 可迭代对象
var i83 = new Int8Array(new Set([1, 2, 3]));
console.log(i83);
// Int8Array(3) [1, 2, 3]
buffer, byteOffset, length
众人拾柴火焰高;该构造函数也支持同时提供三个参数,第一个 buffer
指的是数组缓冲区,是 ArrayBuffer
的实例,同时也是 Int8Array.prototype.buffer
这个属性的值;butyOffset
指的是元素的偏移值,表示从数组中第几个元素开始读取,默认是 0,也就是数组的第一个元素;length
指的是在设置了偏移值后,要读取的元素长度,默认是整个数组的长度;举例说明:
var buf = new Int8Array([1,2,3,4,5]).buffer;
var i8 = new Int8Array(buf, 1, 2);
console.log(i8);
// Int8Array(2) [2, 3]
也就是让申明的类型化数组在提供的 buffer
的基础上,从它的索引为 1 的元素(第二个元素)开始读取,然后向后读取 2 个元素;该操作一般用于对缓冲区数据的截取;
类型差异
存在即合理;根据前面的介绍,TypedArray
定义了多种类型,如 Int8Array
, Uint8Array
, Int16Array
等,这样做也是为了适应不同的应用场景,接下来大致了解一下几个典型的类型化数组之间的区别;
有无符号
以 Int8Array
和 Uint8Array
为例,其实 有符号 的意思是数组中的元素可以存在符号,即可以是负数;因此 无符号 的意思就是元素只能是非负数,举例:
var i8 = new Int8Array([1, 2, 3]);
var _i8 = new Int8Array([-