什么是 binaryString
字面意思:二进制的字符串,说了又好像没说……
我们熟知二进制就是1000001,为一个字节,如果用二进制表示一段文本则会很长难以阅读,我们会将二进制转换为八进制、十进制、十六进制等等形式来展现。
相似的,binaryString 也是一种二进制的表示形式,区别是不需要转换,并且可以表示任意的二进制数据。
那么有的人会说了,ascii 字符集共 128 个,每个字符也会对应一个二进制的值,那不也是 binaryString 吗?
是的,ascii 字符集从属于 binaryString。
因为 binaryString 可以表示任意二进制数据,而不限定某个字符集。
比如你现在看到的这篇文章的文字,也是可以称为 binaryString。
为什么会用到 binaryString?
思考一下,在 javascript 中,有哪些场景会涉及到二进制数据的处理?
- 文件下载,响应头是
application/octet-stream
(表示任意的二进制数据文件)时,我们可以通过 xhr 拿到 Blob 的结果。 - Base64 转换,提交给接口或者本地转换为 base64 显示文件。
- 报文组装,请求的报文一般按固定的字节来存储信息,我们需要将 binaryString 转换为对应的二进制字节存储。
- 图像处理,比如 png 的文件头信息
因为文件编码方式、字符集的差异,就不可避免的需要对二进制数据、binaryString及其他字符集的互相转换操作。
在 javascript 中,并不能直接操作二进制数据,只能通过 ArrayBuffer 的视图方法 TypedArray 提供的 Int8Array()
UInt8Array()
等来读写缓冲区(buffer)里的数据。
同时,也不能直接读写文件,只能通过 Blob
或 File
对象提供的方法来读取和构造文件。
编码与字符集
在 javascript 中,常见的编码方式:
- base64
- urlEncode
常使用的字符集:
- ascii
- unicode
概念
base64 编码是从 ascii 字符集中选了 64 个字符按照一定规则对 binaryString 进行排列。
unicode 是国际通用的字符集,采用十六进制编码,共17个面(0x0000 - 0x10ffff),在一个基本多文种面内为双字节字符(0x0000 - 0xffff),所以存在字节的顺序——大端序和小端序。
之所以会出现不同的字符集,是因为字符不够用。
- ascii 只能表示 128 个字符
- ascii 扩展能表示 256 个字符
- 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。
字符与码点的互相转换
在基本多文种面中(0x0000 - 0xffff)码点范围
tsx// 字符转码点 str.charCodeAt() // 码点转字符 String.fromCharCode()
在更通用的情况下
tsx// 字符转码点 str.codePointAt() // 码点转字符 String.fromCodePoint()
文件与乱码
以 png 文件为例,将文件读取为 binaryString 后
字段解析:
PNG
是三个字节,分别是80
、78
和71
,十六进制为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
实例解析
使用 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
}
但其实不用处理,因为转换后是一一映射对应的字符,直接输出拿到结果即可。