Skip to content
On this page

什么是 binaryString

字面意思:二进制的字符串,说了又好像没说……

我们熟知二进制就是1000001,为一个字节,如果用二进制表示一段文本则会很长难以阅读,我们会将二进制转换为八进制、十进制、十六进制等等形式来展现。

相似的,binaryString 也是一种二进制的表示形式,区别是不需要转换,并且可以表示任意的二进制数据。

那么有的人会说了,ascii 字符集共 128 个,每个字符也会对应一个二进制的值,那不也是 binaryString 吗?

是的,ascii 字符集从属于 binaryString

因为 binaryString 可以表示任意二进制数据,而不限定某个字符集。

比如你现在看到的这篇文章的文字,也是可以称为 binaryString。

为什么会用到 binaryString?

思考一下,在 javascript 中,有哪些场景会涉及到二进制数据的处理?

  1. 文件下载,响应头是 application/octet-stream (表示任意的二进制数据文件)时,我们可以通过 xhr 拿到 Blob 的结果。
  2. Base64 转换,提交给接口或者本地转换为 base64 显示文件。
  3. 报文组装,请求的报文一般按固定的字节来存储信息,我们需要将 binaryString 转换为对应的二进制字节存储。
  4. 图像处理,比如 png 的文件头信息

因为文件编码方式、字符集的差异,就不可避免的需要对二进制数据、binaryString及其他字符集的互相转换操作。

在 javascript 中,并不能直接操作二进制数据,只能通过 ArrayBuffer 的视图方法 TypedArray 提供的 Int8Array() UInt8Array() 等来读写缓冲区(buffer)里的数据。

同时,也不能直接读写文件,只能通过 BlobFile 对象提供的方法来读取和构造文件。

编码与字符集

在 javascript 中,常见的编码方式:

  1. base64
  2. urlEncode

常使用的字符集:

  1. ascii
  2. unicode

概念

base64 编码是从 ascii 字符集中选了 64 个字符按照一定规则对 binaryString 进行排列。

unicode 是国际通用的字符集,采用十六进制编码,共17个面(0x0000 - 0x10ffff),在一个基本多文种面内为双字节字符(0x0000 - 0xffff),所以存在字节的顺序——大端序和小端序。

之所以会出现不同的字符集,是因为字符不够用。

  1. ascii 只能表示 128 个字符
  2. ascii 扩展能表示 256 个字符
  3. unicode 能表示 65536 * 17 个字符

atob是指:ascii to binaryString,作用是解码 base64 字符。因为 base64 使用的字符来源于 ascii,所以缩写为 a。

btoa是指:binaryString to ascii,作用是编码为 base64。同理。

但是 binaryString 的位数只有 256 个,也就是说,如果你想要使用 binaryString 表示二进制值在 256 以外的字符,理论上是行不通的,因为压根儿存储不下

好在浏览器提供了标准的转换方法 TextEncoder,方法接受一个字符串作为输入,返回一个对参数中给定的文本的编码后的 Uint8Array

所以也不难理解,为什么在涉及 binaryString 转换的过程中使用的是 Uint8Array (无符号8位)了,因为正好匹配 binaryString 所能表示的长度。

字符与码点

在字符集中,每一个字符都会对应一个二进制值,但是在unicode出现后,对应的值被称为码点,例如 A 的码点是 65。

字符与码点的互相转换

  1. 在基本多文种面中(0x0000 - 0xffff)码点范围

    tsx
    // 字符转码点
    str.charCodeAt()
    // 码点转字符
    String.fromCharCode()
    
  2. 在更通用的情况下

    tsx
    // 字符转码点
    str.codePointAt()
    // 码点转字符
    String.fromCodePoint()
    

文件与乱码

以 png 文件为例,将文件读取为 binaryString 后

365ff9ad06ee4a789c54054914cafa9e48546c14

字段解析:

  • PNG 是三个字节,分别是 807871,十六进制为 50 4E 47

  • \r 表示回车符(CR),十进制值为 13,十六进制值为 0D

  • \n 表示换行符(LF),十进制值为 10,十六进制值为 0A

  • 接着是 \u001a ,可以看到是十六进制编码的,而有些文件是八进制

    tsx
    \211PNG\r\n\032\n\000\000\000\rIHDR\000\000
    
  • 除了十六进制,图中我们还能看到所谓的乱码 œžŸ¶ ,这其实是 ascii 扩展字符集中的字符,因为 256 位刚好能够显示其对应的字符。也可以转为八进制直接显示为 \266\210\222

打开 ascii 码对照表:https://www.asciim.cn/

tsx
0o266 // 的到的十进制就是 188

04878fd55a9f326619a21fc428b6ccdff0603a4d

实例解析

使用 base64 方式上传文件

tsx
fileInput.addEventListener('change', function(event) {
  const file = event.target.files[0];
  const reader = new FileReader();

  reader.onload = function(event) {
    // 读取完成后,result 属性包含文件内容的 ArrayBuffer 表示
    const arrayBuffer = event.target.result;
    
    // 将 ArrayBuffer 转换为 Uint8Array
    const uint8Array = new Uint8Array(arrayBuffer);
    
    // 将 uint8Array 转换为 binaryString
    let binaryString = '';
    for (let i = 0; i < uint8Array.byteLength; i++) {
	      // 因为是单字节字符,所以直接取下标,并使用 fromCharCode 即可
	      // 如果是双字节呢?
        binaryString += String.fromCharCode(uint8Array[i]);
    }
    const base64String = btoa(binaryString)
  }
  // 为什么不直接使用 reader.[readAsBinaryString](https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsBinaryString)()
  // 因为该方法已被废弃 https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsBinaryString
  reader.readAsArrayBuffer()
})      

如果是双字节字符,要怎么处理呢?

Ascii 以外的字符转换

tsx
const text = ''
const encoder = new TextEncoder()
// 得到的是字节数组,每个字节对应的字符是在 ascii 范围内
const uint8Array = encoder.encode(text)
// [230, 136, 145]
// 再将字节的值转为字符
let binaryString = ''
for (let i = 0; i < uint8Array.length;i++) {
	binaryString += String.fromCharCode(uint8Array[i])
}
// æ \x88\x91
// 输出的结果是不是很熟悉,我们经常称之为乱码!!!
// 是时候给它正名了。

那么,如果是上面的例子中是双字节字符怎么处理?

tsx
''.codePointAt().toString(2)
// 01100010 00010001

// 使用 utf8 编码后
// 第一部分:0110 -> 第一个字节是 11100110 (0xE6)
// 第二部分:001000 -> 第二个字节是 10000010 (0x82)
// 第三部分:010001 -> 第三个字节是 10100001 (0xA1)

// 也就是 [230, 136, 145] 3个字节长度

可以看到这个过程本质就是 utf16 转 utf8 的过程,也就是 TextEncoder 所做的事

对于上面例子的疑问:

tsx
let binaryString = '';
for (let i = 0; i < uint8Array.byteLength; i++) {
    binaryString += String.fromCharCode(uint8Array[i]);
    // 如果是字符 我,需要 3 个字节
    // 如果需要解析,则需要下标 i+3
}

但其实不用处理,因为转换后是一一映射对应的字符,直接输出拿到结果即可。