1

很多前端童鞋对二进制有一种天生的恐惧,觉得遥不可及,本文将带你从基础到实践,全面掌握日常可能面临的二进制操作。文中穿插的有些知识点,可以多琢磨一下,相信会对你有所帮助~

二进制数组

一种类似数组的对象,提供了一种用于访问原始二进制数据的机制。对于音频视频编辑,访问WebSockets的原始数据等,通过类型化数组来操作二进制数据将会非常有帮助。

二进制数组由三类对象组成:ArrayBuffer对象TypedArray视图DataView视图。下面将仅对前两者做介绍。

ArrayBuffer对象

表示一个通用的、固定长度的二进制数据缓冲区,没有格式可言,并且不提供机制访问其内容。只能通过TypeArray视图读写。

//参数为开辟空间的字节长度
const buffer = new ArrayBuffer(32);

上面代码分配了一段连续的32字节的内存空间,每个字节的值默认为0。
image.png
支持的方法:

//buffer所占的字节长度
buffer.byteLength  //32

//拷贝前两个字节返回一个新的ArrayBuffer对象
//先创建16字节长度的内存,再将前两个字节拷贝过去。
buffer.slice(0, 2) 

TypedArray视图

视图提供了上下文,即数据类型、起始偏移量和元素数,将数据转换为实际有类型的数组,以指定格式解读二进制数据。
包含多种视图类型,通式为<type><位数>Array

Int8Array、Unit8Array、Int16Array、Uint16Array...
视图的区别

一个视图窗口占多少位,如Int16Array,表示视窗占2个字节。
822c07ff0f8a0151efc708b5adeb4b86_typed_arrays.png

如何建立视图

方式1: TypedArray(buffer, byteOffset=0, length?)

const buffer = new ArrayBuffer(12); // 12个字节
const view16 = new Int16Array(buffer, 2, 2) //每个视窗是2个字节

创建一个指向buffer的Int16视图,开始于字节2,长度为2。
注意,byteOffset必须是视窗所占字节数的整数倍,比如上例,只能是2,4。
方式2:TypedArray(length)

const view16 = new Int16Array(3);

直接分配6个字节(2*3)的内存空间。
方法3:TypedArray(typedArray)

const x = new Int8Array([1, 1]);
const y = new Int8Array(x);
x[0] // 1
y[0] // 1

x[0] = 2;
y[0] // 1

y会先创建一段新的内存(2个字节),再讲x的值拷贝到新内存空间中。
方法4:TypedArray(arrayLikeObject)

//可以接收一个数组作为参数
const view16 = new Int16Array([1, 2, 3]);

同样的会重新开辟内存,不会共享参数数组对应的内存。

对同一buffer操作,共享内存

TypedArray视图并不会存储数据,只是定义了读写规则,修改的是底层ArrayBuffer,看示例:

//开辟一段内存空间
buff = new ArrayBuffer(32);
//一次读取16位,即2个字节
x16 = new Int16Array(buff);
x16[0] = 1;
//使用Int8Array作为视窗操作
x8 = new Int8Array(buff);
x8[0] = 2;
x16[0] //2  

可以看到不同视图对同一段内存操作,是内存共享的。

Symbol.iterator
const view8 = new Int8Array([1]);

打印下view8对象:
image.png
view8的原型链:Int8Array -> TypeArray -> Object
可以看到length属性(注意区分length和byteLength)、Symbol.toStringTag,同时还有Symbol.iterator属性。这里介绍下遍历器接口。
我们知道一个对象部署了Symbol.iterator(不清楚的可以看ES6精读【划重点系列】(一)),即表示可以被遍历,因此就可以使用for...of:

//将类型数组转为普通数组
const normalArray = [...typedArray];
// or
const normalArray = Array.from(typedArray);
// or
const normalArray = Array.prototype.slice.call(typedArray);
支持的方法

普通数组的操作方法和属性,对 TypedArray 数组基本都适用。
1) 由TypedArray获取ArrayBuffer

const array = new Int16Array([1, 2, 3]);
array.buffer   //TypedArray -> ArrayBuffer

2) 复制TypeArray

//在位移offset位置放置typedarray
typedarray.set(typedarray, offset); 

3)length 和 byteLength

const a = new Int16Array(8);

a.length // 8
a.byteLength // 16

二进制读取顺序

先看下例中v16[0]是多少?

buffer = new ArrayBuffer(2);
v8 = new Int8Array(buffer);
v16 = new Int16Array(buffer);
v8[0] = 1;
v16[0] //??

有的同学可能会觉得下面这种:

//此时buffer中的值为:
0000 0001 0000 0000
//V8视窗是1个字节,那么其值为:
[0000 0001] [0000 0000] = > [1, 0]
//由于V16一个视窗2个字节,那么很简单
[0000 0001 0000 0000] => [256]

事实上,说明你陷入误区,机器和人是不一样的,它是从左往右读取的:地址空间中的高位字节存真正数据的高位,地址空间中的低位存数据低位,因此:

//此时buffer中的值为:
1000 0000 0000 0000
//V8视窗是1个字节,那么其值为:
[1000 0000] [0000 0000] = > [1, 0]
//由于V16一个视窗2个字节,那么很简单
[1000 0000 0000 0000] => [1]

应用

1) XMLHttpRequest

//AJAX默认返回文本数据,即responseType = 'text'
xhr.responseType = 'arraybuffer'

返回的response就是ArrayBuffer类型。
2)WebSockets
可直接发送或接收二进制数据。

const typedArray = new Uint8Array(4);
socket.send(typedArray.buffer);

3)Fetch API
从response直接读取arrayBuffer:

response.arrayBuffer()

Response的下列方法返回Promise对象:

arrayBuffer()
blob()
json()
text()
formData()

4)File API
需要使用FileReader:

const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
  const arrayBuffer = reader.result;
  // ···
};

主要方法:

readAsText()   
readAsDataURL() //dataUrl形式
readAsArrayBuffer()
abort()

5)Canvas

Blob

表示一个不可变、原始数据的类文件对象
注意:File 接口基于Blob,继承了Blob的功能并将其扩展使其支持用户系统上的文件。

语法
var aBlob = new Blob(array, options);

array: [ArrayBuffer | ArrayBufferView | Blob | String]
options:{type};type指定mineType,即mediaType
举例:

//使用字符串创建blob
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});
Blob与ArrayBuffer

blob除了原始字节外,提供了mine type作为元数据。

//arrayBuffer转blob
let v8 = new Int8Array([1,2]);
let blob = new Blob([v8]);

// blob转arrayBuffer 
blob.arrayBuffer().then(buffer=> console.log(buffer))
//or
new Response(blob).arrayBuffer().then(buffer=>console.log(buffer))
//or
var reader = new FileReader();
reader.addEventListener("loadend", function() {});
reader.readAsArrayBuffer(blob);
Blob方法及属性
size //所包含数据的字节数
type //文件mediaType

.slice([start[, end[, contentType]]])  //返回一个新的Blob
//返回一个Promise
.stream()
.text()
.arrayBuffer()
createObjectURL
//会产生一个类似 blob:d3958f5c-0777-0845-9dcf-2cb28783acaf 这样的URL字符串
let url = URL.createObjectURL(myBlob);
//可以当做普通url使用
img.src = url;

参数object:File对象Blob对象或者mediaSource对象.

实践

示例1:上传本地图片并在网页上展示(预览)

本地上传图片 -> input[type=file]获取的File对象 -> FileReader 
1)-> Blob -> createObjectURL //ObjectUrL
2)-> readAsDataURL()  //DataUrL

示例2:如何拼接两个音频

fetch请求音频资源 -> ArrayBuffer -> TypeArray -> set方法拼接成一个TypeArray -> ArrayBuffer -> Blob -> Object URL

示例3:json数据转化为demo.json并下载文件


1) Text -> DataURL //设置mediaType为application/octet-stream
2) Text -> Blob -> Object URL  //使用a标签download

示例4:大文件分片上传

//获取文件
let blob = document.getElementById("file").files[0];
获取文件大下
let fileSize = blob.size;
//假设每片2M
let perSize = 1024*1024*2;
//得到分片数
totalPieces = Math.ceil(filesize/perSize);

//分割文件发送
start = 0;
while(start < filesize) {
    end = start + perSize;
    //边界处理
    if(end > filesize) { end = filesize };
    let chunk = blob.slice(start,end);
    start = end;
    formData.append(name, chunk, blob.name);
    
    //发送文件chunk...
}

示例5:node层如何解析出文件
node层如何从multipart/form-data类型的request读文件,关键点还是使用开始分隔符:--boundary和结束分隔符:--boundary--做内容分割,然后使用Content-Disposition识别文件。

如果对你有帮助,请点赞or在看,谢谢~
获取更多技术干货,欢迎【扫码关注】~
1584413667(1).jpg

参考:
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://es6.ruanyifeng.com/#d...


夜暮sky
97 声望5 粉丝