浏览器里的二进制

以下是几个主角的简介:

  • ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。大白话其实就是代表一块固定的连续内存数据,虽然名字中带有Array,其实和我们认知的数组没半毛钱关系。并不直接提供读取、写入接口,操作需要通类型数组 视图或过DataView 视图

  • TypedArray(类型数组),在JavaScript下可用的类型数组有:Int8ArrayUint8ArrayUint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64ArrayBigInt64ArrayBigUint64Array,提供内存数据的不同类数组(array-like)视图形式,底层还是ArrayBuffer存储数据。大白话就是用数组的形式表示内存数据,并提供按数组索引操作数据。什么作用呢?比如你的数据是单个字节为单位的,那么用Uint8Array描述一段内存,则可以通过索引操作每个单位的数据,同样,如果你的数据是双字节为单位的,那么你就可以通过Uint16Array描述这段内存。所以,new Uint8Array([255,255])new Uint16Array([255*255])表示的是一样的数据。

  • DataView,如果说TypedArray是以固定的单位读写二进制数据的话,DataView则灵活多了,在同一个视图中,可以使用多种字节混合的方式,甚至无需考虑不同平台的字节序问题。

  • Blob对象表示一个不可变、原始数据的类文件对象,不像之前的其他仨,Blob不属于ES标准,是浏览器API。Blob可以理解为ArrayBuffer + mime typeFile对象继承自Blob。

简单说,视图就是读、写二进制的形式。 ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。

下图可以直观理解视图
image
上图出处

下面开始详细介绍以上几种数据类型。

ArrayBuffer

ArrayBuffer仅仅代表一块缓冲区域,并不能读取、写入数据

// 创建10字节的缓冲区,每个二进制位填充为0
const buffer = new ArrayBuffer(10)
// 如果有个现成的buffer,可以通过byteLength读取其大小
buffer.byteLength // 10

ArrayBuffer的来源非常多,举几个常见的场景:

  • 来自FileReader
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
    // 看这里看这里看这里
    const buffer = reader.result;
};
  • 来自XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
// 声明需要的数据类型
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
    // 看这里看这里看这里
    const buffer = xhr.response;
};

xhr.send();
  • 来自canvas
const canvas = document.createElement('canvas')
canvas.height = 1
canvas.width = 1
const ctx = canvas.getContext('2d')
const buffer = ctx.getImageData(0,0,canvas.width,canvas.height).data.buffer
  • 来自Response/Fetch
const buffer =  await new Response("Hello World!").arrayBuffer()
// 或者
const response = await fetch('/')
const buffer = await response.arrayBuffer()
  • 来自WebSockets
const socket = new WebSocket('ws://127.0.0.1:8081');
// 声明数据交换类型
socket.binaryType = 'arraybuffer';
// Wait until socket is open
socket.addEventListener('open', function (event) {
    socket.send(new ArrayBuffer(10));
});
// Receive binary data
socket.addEventListener('message', function (event) {
    // 看这里看这里看这里
    const buffer = event.data;
});

TypedArray

ArrayBuffer自身是个黑盒,不可读取、写入,那么就需要一种工具来操作ArrayBuffer,这种工具就叫视图。本节的TypedArray(类型数组)就是视图的一种。

我们知道二进制最小单位为bit,只能表示两种状态:01,绝大多数情况下,我们去操作二进制是以字节(byte字节,1byte=8bit)为单位,还有些情况甚至是以两个字节或四个字节,所以TypedArray提供了一些列针对特定数据类型的类型化数组的构造函数:Int8ArrayUint8ArrayUint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64ArrayBigInt64ArrayBigUint64Array,用于满足不同场景下的需求。

const buffer = new ArrayBuffer(10)
const u8Buffer = new Uint8Array(buffer)
u8Buffer[0] = 200
u8Buffer[1] = 300 // 超过8位最大值256 溢出,值为44

const u16Buffer = new Uint16Array(buffer)
u16Buffer[1] = 300 // 不会溢出,索引对应的最大值为256*256-1

特别的,在颜色计算的场景,我们希望在某值在溢出后不要截断,而直接使用255,这时Uint8ClampedArray就派上了用场:

const buffer = new ArrayBuffer(10)
const u8Buffer = new Uint8ClampedArray(buffer)
u8Buffer[0] = 200 // 正常 200
u8Buffer[1] = 300 //  溢出,值为 255

每个类型数组都有两个静态方法,用于从数组及不定的参数快速得到对象:

  • TypedArray.from()

使用类数组(array-like)或迭代对象创建一个新的类型化数组。

  • TypedArray.of()

通过可变数量的参数创建新的类型化数组。

并且,无论哪种类型数组的实例,都有普通数组拥有的许多方法(不包含改变buffer长度的方法),比如:mapforEachreduce

DataView

DataView是除了TypedArray之外的另一种操作ArrayBuffer的选择。但是不像TypedArray提供各种不同的对象来满足不同的需求,DataView只有一个构造函数,实例化后提供getInt8getUint8getInt16getUint16getInt32getUint32getFloat32getFloat64等读方法与setInt8setUint8setInt16setUint16setInt32setUint32setFloat32setFloat64等写方法。在复杂的场景下,可以用一个视图实例达到不同类型读写目的。

另外,DataView读、写多字节二进制数据默认使用大端序,可以通过指定诸如getInt16setInt32之类的多字节操作方法最后一个可选参数为falseundefined值来使用小端序

示例:

const buffer = new ArrayBuffer(6)
const view = new DataView(buffer)
view.setUint8(0, 11) // 1
view.setUint16(2, 22) // 2
view.setUint16(4, 33, true) // 3

console.log(new Uint8Array(buffer)) // [11, 0, 0, 22, 33, 0]
console.log(new Uint16Array(buffer)) // [11, 5632, 33, 0, 0]
console.log(view.getUint16(2)) // 22
console.log(view.getUint16(4)) // 8448
console.log(view.getUint16(4, true)) // 33

为什么//2写入的22,在Uint16Array视图中是5632呢?
因为Uint16Array视图是按小端序读取数据的,而//2中我们是按大端序写入的数据,我们计算验证下:
22的二进制为10110,后面再拼接8位0,得到1011000000000,转换为十进制刚好是5632

view.getUint16(4)得到8848也是一样的道理。

Blob

Blob对象表示一个不可变、原始数据的类文件对象。我们日常使用的File对象继承了它。

由于Blob的不可变性,需要将其转换成ArrayBuffer然后通过视图方可读、写数据。

通过其构造函数,我们可以从如下四种方式得到一个Blob实例:

// 从ArrayBuffer
const buffer = new ArrayBuffer(8)
const blob1 = new Blob([buffer])
// 从ArrayBufferView
const view = new DataView(buffer)
const blob2 = new Blob([view])
// 从另一个Blob对象
const blob3 = new Blob([blob2])
// 从字符串,顺便指定mime
const blob4 = new Blob(["<h1>Hello World</h1>"], {type:"text/html"})

Blob对象的存在,为我们在浏览器上自行构建file-like(类文件)对象提供了可能,我们可以通过一定转换将客户端数据通过Blob生成DataURLObjectURL,从而可以用于需要url的场景。比如:动态构建文件并下载、本地选择的图片预览等。

转换

各种数据转换如下图:
转换图

我们列举一些常见的场景来尝试转换。

本地选择图片实现预览

路径:本地图片->Blob->ObjectURL/DataURL


const fr = new FileReader fr.onload = e=>{ img.src = e.target.result } // 以dataURL形式读取file fr.readAsDataURL(file) // blob转成ObjectURL img.src = URL.createObjectURL(file)

下载一段自定义文本

路径:文本->Blob->ObjectURL/DataURL

const text = "Hello World"
const blob = new Blob(\[text\],{type:"text/plain"})
const url = URL.createObjectURL(blob)
// 省略下载过程

参考: