base64 是前端常见的编码方式,那它就是如何编码的呢?以“掘金”两个字为例,今天就来一探究竟:
编码字符集
字符集中有 64 个字符,分别是:
- 大写字母 A-Z
- 小写字母 a-z
- 阿拉伯数字 0-9
- 字符 + 和 /
let str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str+=str.toLowerCase()
str+='0123456789+/'
console.log(str) // 得到字符集:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
编码方式
首先要知道下面两个基础知识:
- 任意符号在计算机中都是以二进制存储的
- 1 个字节有 8 个比特位,每个比特位只能存放 0 或 1
那就先看下“掘金”两个字的二进制是如何表示的:
const hex = Buffer.from('掘金').toString('hex') // 得到 16 进制表示 e68e98e98791
const decimal = parseInt(hex, 16) // 得到 10 进制表示 253500125185937
const binary = decimal.toString(2) // 得到 2 进制表示 111001101000111010011000111010011000011110010001
得到 2 进制表示之后,从左到右进行分组,每 6 个比特一组,对其转换成 10 进制,可以写个循环来截取:
for (let i = 0; i < binary.length; i += 6) console.log(binary.slice(i, i + 6))
最终分组结果如下:
111001
101000
111010
011000
111010
011000
011110
010001
然后对每个分组,利用 parseInt 方法将其转换成 10 进制:
parseInt('111001', 2) = 57
parseInt('101000', 2) = 40
parseInt('111010', 2) = 58
parseInt('011000', 2) = 24
parseInt('111010', 2) = 58
parseInt('011000', 2) = 24
parseInt('011110', 2) = 30
parseInt('010001', 2) = 17
然后从编码字符集 str 中取对应的字符:
str.charAt(57) = 5
str.charAt(40) = o
str.charAt(58) = 6
str.charAt(24) = Y
str.charAt(58) = 6
str.charAt(24) = Y
str.charAt(30) = e
str.charAt(17) = R
于是“掘金”的 base64 编码就是:5o6Y6YeR
末尾的等号
在 base64 编码的过程中,我们有时候发现末尾会出现等号,例如字符 a 的 base64 编码就是 YQ==,可是等号并不在字符集中,这代表什么呢?
其实等号是占位符,由于 1 个字节有 8 个比特位,但是 base64 编码又按照 6 个比特进行分组,而 8 和 6 的最小公倍数是 24,所以当二进制的长度为 24 的倍数时,就能分成整数组,例如上面“掘金”转换为二进制之后的长度为 48,恰好是 24 的整数倍。
而对于字符 a 来讲,情况就不太一样了,因为 a 占用 1 个字节,也就是 8 个比特位,二进制表示为 01100001,如果按照每 6 个比特进行分组的话,会被分成 2 组:
011000
01
第二组不足 6 位,就会自动补 0,变成:
011000
010000
计算十进制的结果:
parseInt('011000', 2) = 24
parseInt('010000', 2) = 16
然后从编码字符集 str 中取对应的字符:
str.charAt(24) = Y
str.charAt(16) = Q
虽然第二组满了,但是总共也才 12 个比特,不够 24 个比特,相差 12 个比特,也就是少了 2 组,每少一组就会在末尾补个等号 =,所以补两个等号,最终结果是 YQ==
手写编码函数
掌握了 base64 编码的生成规律,其实就可以自己编解码函数了,下面给出我写的示例:
function base64encode(input) {
// 编码字符集
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
// 转成2进制
const binary = Buffer.from(input).toString('hex')
.split('')
.map((it) =>
Number('0x' + it)
.toString(2)
.padStart(4, 0)
)
.join('')
let ret = '', len = binary.length
// 每6个一组,转成10进制,然后取出对应字符
for (let i = 0; i < len; i += 6) {
const group = binary.slice(i, i + 6).padEnd(6, '0') // 每6个一组
const idx = parseInt(group, 2) // 转成10进制
const char = charset.charAt(idx) // 取出对应字符
ret += char
}
// 计算末尾有多少等号
const suffix = Array.from({ length: (24 - (len % 24)) / 8 }, () => '=').join('')
return ret + suffix
}