base64编码

325 阅读4分钟

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
}