Javascript 中的二进制数据

ArrayBuffer 类

表示连续的,固定长度的,一块内存区域,存储着原始二进制数据, 类似其他语言中的 “bytes array”。

// 创建一块长度为 8 bytes 的内存区域。
var buff = new ArrayBuffer(8);
console.log(buff.byteLength); // 8

你无法直接通过 ArrayBuffer 类访问或修改它的数据, 如果需要读取/修改 ArrayBuffer 中的数据,需要是一个「视图」即 ArrayBufferView :

ArrayBufferView 概念

ArrayBufferView 不是一个真实的类,而是对 ArrayBuffer 视图的总称。 ArrayBufferView 包含 TypedArray 和 DataView。

TypedArray 类

TypedArray 为 ArrayBuffer 提供与数组类似的视图。你不能直接实例化 TypedArray 类, 它只为各种格式的 TypedArray 对象提供原型,即公用的属性和方法。 如果在浏览器执行 new TypedArray() 会看到 TypedArray 未定义的报错。

new TypedArray(); // TypedArray is not defined

创建一个 TypedArray 对象需要通过实例化 TypedArray 的某个子类,它们有: Uint8Array Int8Array Uint16Array Int16Array 等等。 其中 Uint8Array 比较常见。

// Uint8Array 继承了 TypedArray 的原型链。
Object.getPrototypeOf(Uint8Array); // TypedArray

// 创建一个 6 bytes 长度的视图。
// 这个过程中,会自动创建一个 6 bytes 的 ArrayBuffer。
// 视图是不能单独存在的。
var arr8 = new Uint8Array(6);
// 也就是说以上代码相当于:
var buff = new ArrayBuffer(6);
var arr8 = new Uint8Array(buff);

var arr16 = new Uint16Array(6);
// 相当于:
var buff = new ArrayBuffer(12);
var arr16 = new Uint16Array(buff);
// 上面之所以需要创建一个 12 bytes 的 ArrayBuffer
// 是因为一个 Uint16Array 元素会占用 2 个 bytes
new ArrayBuffer(12).byteLength === new Uint16Array(6).byteLength; // true

// 有了「视图」后就可以修改数据了。
arr8[0] = 258;
console.log(arr8); // [2, 0, 0, 0, 0, 0]
// 之所以第一个元素是 2 而不是 258,
// 是因为 258 超出了 1 个 byte 能表示的整数范围。
// 258 的二进制是:
Number(258).toString(2); // 100000010
// Uint8Array 会取最后 8 位即:00000010
// 10进制表示为:
parseInt('00000010', 2); // 2

Uint8Array 中的 Uint 是指 “unsigned int” ,它只能表示正数, 表示范围从 0000000011111111 即 0 ~ 255 共 256 个数字。 相对应的是 Int8Array,它的元素可以表示负数,由于需要用第一位表示正负, 因此只能用余下的 7 位表示数值,表示的范围为 -128 ~ 127 。

TypedArray 的方法与普通数组基本一致,比如都有 slice find reduce 等。 但没有 splice 方法,因为 TypedArray 背后的 ArrayBuffer 长度是固定的。 除此之外 TypedArray 也没有 concat 方法。

TypedArray 有 2 个普通数组没有的方法:setsubarray

  • arr.set(fromArr, [offset]) 复制 fromArr 中所有元素到 arr , arr 的开始位置为 offset,默认为 0.

  • arr.subarray([begin, end])slice 方法类似,但是只复制视图, 复制出来的视图与 arr 对应同一 ArrayBuffer。

DataView 类

DataView 是一种比 TypedArray 更灵活的 ArrayBufferView。 它与 TypedArray 有 2 点最大的不同:

  • 实例化 DataView 时,不会像 TypedArray 那样自动创建一个对应的 ArrayBuffer, DataView 的 buffer 必须在实例化时以参数的形式指定。
var buffer = new ArrayBuffer(16);
var dv = new DataView(buffer);
  • DataView 不需要在实例化时指定格式,而是在读取时。
// 创建一个 4 bytes 每一位都是 1 的 ArrayBuffer。
var buffer = new Uint8Array([255, 255, 255, 255]).buffer;
var dv = new DataView(buffer);

// 获得第一个 byte
console.log(dv.getUint8(0)); // 255
// 获得前两个 bytes
console.log(dv.getUint16(0)); // 65535 Math.pow(2, 16)
// 将 4 个 bytes 全都设置为 0.
dv.setUint32(0, 0);
console.log(((dv.getUint8(0) == dv.getUint16(0)) == dv.getUint32()) == 0);

BufferSource 概念

BufferSource 是 ArrayBuffer 以及它的视图的总称。 BufferSource = ArrayBuffer + ArrayBufferView

Blob/File 类

BufferSource 是 javascript 标准的一部分,而 Blob 以及扩展出来的 File 是浏览器的原生对象, 是应用于 BufferSource 的高级接口。

Blob 是由 MIME 类型与 Blob 数据组成,Blob 数据可以是由字符串,Blob,或者 BufferSource 组成的数组。可以把 Blob 理解成标明数据类型的二进制数据。

// 由字符串数组创建一个 html 类型的 Blob。
var blob = new Blob(['<html><body><h1>Blob</h1></body></html>'], {
    type: 'text/html'
});
// 通过由 BufferSource和字符串组成的数组创建一个文本类型的 Blob
// [72, 101, 108, 108, 111] 是 Hello 的码位(code point)。
var blob = new Blob([new Uint8Array([72, 101, 108, 108, 111]), ' ', 'world'], {
    type: 'text/plain'
});

将 Blob 作为 URL 资源使用

Blob 可以作为 aimg 标签的 url 使用,在 http 协议传输中,Blob 对象的 type 属性将成为 http header 中 Content-Type 的值。下面的例子将上面生成的文本类型 Blob 作为 a 标签的 href 属性,用户通过点击链接可以将 Blob 作为文本类型文件下载到本地。

<html>
    <body>
        <a download>Download a txt file</a>
        <script>
            var blob = new Blob(
                [new Uint8Array([72, 101, 108, 108, 111]), ' ', 'world'],
                {
                    type: 'text/plain'
                }
            );
            document.querySelector('a').href = URL.createObjectURL(blob);
        </script>
    </body>
</html>

URL.createObjectUrl() 会生成这种格式的 url:blob:<origin>/<uuid> , 这个地址会映射到内存中的 blob 资源。就像变量一样,如果引用数不为 0 , 变量引用的资源就不会被释放,除非网页关闭。因此如果我们使用 URL.createObjectUrl 生成的 url 不在需要时,需要手动调用 URL.revokeURLObject() 方法释放掉内存资源 (如果 blob 还被其他变量引用,则不会立即释放)。

除了通过 URL.createObjectUrl 方法将 Blob 转为 url 外,还可以通过 FileReader 类将 Blob 对象转为 Data URLs。Data URL 的格式为: data:[<meidiatype>][;base64],<data>

var blob = new Blob([new Uint8Array([72, 101, 108, 108, 111]), ' ', 'world'], {
    type: 'text/plain'
});
var reader = new FileReader();

reader.readAsDataURL(blob);
reader.onload = function () {
    // data:text/plain;base64,SGVsbG8gd29ybGQ=
    console.log(reader.result);
};

Data URLs 的优点是不需要手动释放内存,缺点是如果 Blob 数据量大,会有性能问题。

将 Canvas 转成 Blob 对象

canvas.toBlob 方法可以将 canvas 转为 Blob,类似给 canvas 截了一个屏, 并将截屏信息保存到 Blob 对象。

<html>
    <head>
        <meta charset="utf-8" />
    </head>
    <body>
        <canvas></canvas>
        <button>Canvas Shot</button>
        <script>
            var canvas = document.querySelector('canvas');
            var context = canvas.getContext('2d');
            var img = new Image();

            img.src = '/ass/img/binary-data-in-javascript.png';
            img.crossOrigin = 'anonymous';
            img.onload = function () {
                context.drawImage(img, 0, 0);
                context.font = '48px serif';
                context.fillText('zhangkai.pro', 0, 40);
            };
            document.querySelector('button').addEventListener(
                'click',
                function (event) {
                    canvas.toBlob((blob) => {
                        var url = URL.createObjectURL(blob);
                        var a = document.createElement('a');

                        a.download = '';
                        a.href = url;
                        a.click();
                    }, 'image/png');
                },
                false
            );
        </script>
    </body>
</html>

将 Blob 对象转成 ArrayBuffer

可以将 Blob 转成 ArrayBuffer 后进行更底层的数据操作。 转换需要使用 FileReader 类。

var blob = new Blob([]);
var reader = new FileReader();

reader.readAsArrayBuffer(blob);
reader.onload = function () {
    // the ArrayBuffer
    console.log(reader.result);
};

File 类

File 类是 Blob 类的扩展。除了继承 Blob 所有属性方法外还有 2 个自己独有的属性: namelastModified。 File 对象除了直接实例化 File 类获得外,还能通过用户选取本地文件获得。

document.querySelector('input[type=file]').addEventListener(
    'change',
    (event) => {
        var input = event.target;
        var theFile = input.files[0];

        console.log(theFile.name); // 松岛枫.avi
        console.log(theFile.lastModified); // 1599813257435
    },
    false
);

FileReader 类

FileReader 类用于读取 Blob(File) 的二进制内容, 上面我们已经使用过 readAsArrayBufferreadAsDataURL 方法, 这里介绍 FileReader 的更多方法。

var reader = new FileReader();

// 读取内容作为 ArrayBuffer 对象
reader.readAsArrayBuffer(blob);
// 读取内容作为字符串
reader.readAsText(blob, encoding);
// 读取内容作为 Data URL
reader.readAsDataURL();

reader.onload = function () {
    console.log(reader.result);
};

TextEncoder/TextDecoder Class

TextDecoder 将 BufferSource 转成字符串; TextEncoder 将字符串转为 utf-8 编码的 BufferSource; TextDecoder 与 FileReader.readAsText 作用类似。

var utf8Decoder = new TextDecoder();
// 一个汉字「张」在 utf-8 编码中需要占用 3 个字节。
var utf8EncodedBytes = new Uint8Array([229, 188, 160]);

console.log(utf8Decoder.decode(utf8EncodedBytes)); // 张

var utf8Encoder = new TextEncoder();

utf8Encoder.encode('🌞'); // Uint8Array(4) [240, 159, 140, 158]

var blob = new Blob([utf8EncodedBytes], {
    type: 'text/plain'
});
var reader = new FileReader();

reader.readAsText(blob);
reader.addEventListener('load', (event) => {
    console.log(event.target.result); // 张
});
// 或者
reader.onload = function () {
    console.log(reader.result); // 张
};

Binary String 概念

Binary String 与 Javascript 中普通的字符串无异, 都是使用 UTF-16 编码,每个字符至少占用 2 个字节。 唯一区别是 Binary String 中的字符码位(code point)不能超过 255 。 类似 ASCII,可以把 Binary String 理解为一个字符集,ASCII 码位不能超过 127, 而 Binary String 不能超过 255。

Binary String 并不是用来展示文字的,而是用来表示原始的二进制数据。 在 TypedArray 出现之前,人们使用 Binary String 作为二进制流,然后通过 charCodeAt() 将每个字符转成一个字节的值。除此以外,btoa 也会用到这个概念:

btoa and atob

btoa 将 Binary String 转化为 Base64 ASCII 字符串。 btoa 可以接受任何字符串作为参数,但是字符串的范围不能超过 Binary String :

console.log('H'.charCodeAt(0)); // 72 , 小于 255,范围内。
console.log(''.charCodeAt(0)); // 24352,大于 255, 范围外。
btoa('H'); // SA==
btoa(''); // Error!

如果需要编码 Binary String 范围以外的字符,需要先将一个字符拆分成多个 Binary String 字符:

function toBinaryString(str) {
    var utf8Encoder = new TextEncoder();
    var buff = utf8Encoder.encode(str);
    var result = [];

    for (var i = 0 ; i < buff.byteLength; i++) {
        result.push(String.fromCharCode(buff[i]));
    }

    return result.join('');
}

function toNoramlString(bs) {
    var utf8Decoder = new TextDecoder();
    var buff = new Uint8Array(bs.split('').map((s) => s.charCodeAt(0)));

    return utf8Decoder.decode(buff);
}

btoa(toBinaryString('')); // 5byg
toNoramlString(atob('5byg')); // 张

atobbtoa 的反向操作。