这几天不务正业, 在看 HTTP 以及 express 的服务端应用. 关于认证就有很多技巧. 什么基本认证, 记号认证, Passport 认证. 虽然其它认证都是通过 md5 或者 UUID 来实现一种不可逆且唯一的识别码. 换句话说, 我也不会实现这些东西. 不过, Base64 虽然作为最基本的操作, 安全性基本没有(因为是对称的),所以可以作为一个很好的练练手的代码.
Base64
Base64 的基本思路就是, 3/4. 每三个字节为一组, 将 24 bits 分为 4 份, 每份 6bits. 然后在一个大小为 64 的索引表中找到对应的字符.
针对不能整除 3 的情况, 就将补 0. 之后这些 0 使用 = 来填充. 但是在实现中, 索引表中的第 0 个是 A, 所以如果我们要提前填充的话, 就要去掉多余的字符. 但是在 JS 中, 如果数组不存在某个下标, 会返回 undefined
, 而对 undefined
进行操作会得到 0. 所以, 我们就不需要提前填充.
实现
作为一个合格的程序员, 当然不能傻逼到自己去一个个写索引表, 其实复制粘贴很快! 所以我就用 map 函数来生成这些. 代码很简单
let temp = Array(64).fill(0)
let index = String.fromCharCode(
...temp.map((_, idx) => {
if (0 <= idx && idx < 26) {
return 0x41 + idx
} else if (idx >= 26 && idx < 52) {
return 0x61 + idx - 26
} else if (idx >= 52 && idx < 62) {
return 0x30 + idx - 52
} else {
return idx === 62 ? 43 : 47
}
})
)
接下来就是关于 3 / 4 的具体操作咯. 其实很好理解, 每 6 个一份, 通过位操作就能将每个部分分离出来.
当然, 因为 JS 中的数字都是 4 个字节起步的, 所以有点浪费内存, 应该使用 Uint8Array
来减少内存的消耗.
let first = data[0] >> 2
let two = ((data[0] & 0x3) << 4) | (data[1] >> 4)
let three = ((data[1] & 0xf) << 2) | (data[2] >> 6)
let four = data[2] & 0x3f
return `${index[first]}${index[two]}${index[three]}${index[four]}`
代码很简单, 就像上面一样. 不过需要注意的就是优先级问题. 位移动操作在大多数语言中, 优先级都是高于其它的位操作, 低于 + - 运算. 除了 Swift. 所以这里的 () 主要还是避免歧义.
好了, 轮子都找好了, 剩下的就是修车咯!
function encode(data) {
// 将 data 转化成 Uint8Array 转成能接受的值
let transfer = new Uint8Array([...data].map(c => c.charCodeAt(0)))
let base64 = []
let startPoint
for (startPoint = 0; startPoint < data.length; startPoint += 3) {
base64.push(...threeToFour(transfer.slice(startPoint, startPoint + 3)))
}
// 因为 threeFour 会假设传入的就是 3 个字节, 但是如果不是 3 个的话, 就会出现问题.
// 比如 data[2] 就会是 undefined. 对 undefined 进行位操作会被当成 0, 所以多出来的字母肯定会有一个 0.
// 再根据 Base64 的规则, 加上相应的等号
if (startPoint - data.length === 1) {
// 说明 % 3 余 2, 简单的算一下就知道咯.
base64.pop()
base64.push('=')
} else if (startPoint - data.length === 2) {
base64.pop()
base64.pop()
base64.push('==')
}
return base64.join('')
}
需要注意的是那个循环. 所以我们知道要分辨能不能整除 3. 但是, 仔细想一下, 我用 3, 4, 5 作为 🌰 的话, 就能看出来.
3 的时候, 从 0 开始, 最后跳出循环的 startPoint 就等于 3. 所以没有任何需要另外处理的情况.
4 的时候, 就会在 6 结束, 这个时候 6 - 4 等于 2, 所以需要补两个字节. 但是因为我的 threeToFour
函数没有专门处理这种情况, 最后总会返回 4 个字符, 所以我们需要将多出来的字节替换成 =. 当然, 你也可以使用 splice 做到一次完成这种操作.
5 的时候和上面相似.
最后这就是整个代码的实现咯. 不得不说, 对于那种需要频繁增加减少的操作, 我还是喜欢用数组, 然后 join
起来. 不过, 浏览器对于 += 运算符应该是做了大量的优化的, 所以到底哪个性能高, 我还真的不确定……
**Update: ** 现代浏览器针对 +=
有客观的优化,虽然还有一些细节上的区分,不过基本上不超过几百万字节的话,+= 是更快的。但是因为这里还需要修改最后的字符串,需要再复制一下,这个可能会带来不必要的性能损耗。
总结
这种题目整个做出来, 对于自己的思维还是很有锻炼的. 虽然, 不可能一次就能编对, 特别是那个位操作的一段很容易出现问题, 这个时候一定要保持冷静才行.