手撸一个 base64 低配(字符串)版编码解码库

@CoderMing 2018-11-22 04:09:05发表于 CoderMing/blog JS基础

最近负责写了一个 base64 编码解码库,第一版是抄的网上的解决方案,但后面还是改成了自己写一道。于是借这个机会,我去学习了下 base64 的编码解码方案。

实现代码

话不多说,上代码:

const charMap = (() => {
    const mapStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    let encodingMap = {},
        decodingMap = {}
    for (let i = 0; i < mapStr.length; i++) {
        const el = mapStr[i]
        encodingMap[i] = el
        decodingMap[el] = i
    }
    return { encodingMap, decodingMap }
})()

const padZeroStart = function(str, tarNum) {
    let strLen = str.length
    for (let i = 0; i < tarNum - strLen; i++) {
        str = '0' + str
    }
    return str
}

export const encode = str => {
    if (typeof str === 'object' || typeof str === 'function') throw new Error('Invalid argument')

    str = String(str)
    let strBin = '',
        equalFix = ''
    for (let i = 0; i < str.length; i++) {
        strBin += padZeroStart(str[i].charCodeAt().toString(2), 8)
    }
    // 计算末尾余零
    if (strBin.length % 24 !== 0) {
        if ((strBin.length % 24) % 16 === 0) {
            strBin += '00'
            equalFix = '='
        } else if ((strBin.length % 24) % 8 === 0) {
            strBin += '0000'
            equalFix = '=='
        } else throw new SyntaxError('Binary parse error')
    }

    return (
        strBin
            .split(/(?=^(?:[01]{24})+)/g)
            .map(el => {
                const { encodingMap } = charMap
                return el
                    .split(/(?=(?:[01]{6})+$)/)
                    .map(el => encodingMap[Number.parseInt(el, 2)])
                    .join('')
            })
            .join('') + equalFix
    )
}

export const decode = base64Str => {
    if (typeof base64Str === 'object' || typeof base64Str === 'function')
        throw new Error('Invalid argument')
    base64Str = String(base64Str)
    const { decodingMap } = charMap
    let equalNum = 0,
        strBin = ''

    let b64rLength = base64Str.length
    // 判断长度
    if (b64rLength === 0) return ''
    if (b64rLength % 4 !== 0) throw new SyntaxError('Invalid base64 string')
    // 匹配末尾等号
    if (base64Str[b64rLength - 2] === '=') {
        equalNum = 2
        base64Str = base64Str.substring(0, b64rLength - 2)
    } else if (base64Str[b64rLength - 1] === '=') {
        equalNum = 1
        base64Str = base64Str.substring(0, b64rLength - 1)
    }

    // 转二进制串
    for (let i = 0; i < base64Str.length; i++) {
        const el = base64Str[i]
        let prevBin = padZeroStart(decodingMap[el].toString(2), 8)
            .substring(2)
        if (i === base64Str.length - 1) {
            strBin += prevBin.substring(0, 6 - equalNum * 2)
        } else {
            strBin += prevBin
        }
    }

    return String.fromCharCode.apply(
        null,
        strBin.split(/(?=(?:[01]{8})+$)/g).map(el => Number.parseInt(el, 2))
    )
}

export const btoa = encode
export const atob = decode
  • 其中的 padZeroStart 方法可以使用 ES6 的新 api padStart 实现。
  • 中间用到了很多前瞻的正则,主要目的是实现定点切割01 string串。

后续改进

我写的这一版,有一个很明显的可优化点,就是操作01是通过 String 来操作的。这样子的话空间开销将会是 O(8n) (n为传入字符串大小)。日后优化的话,需要使用位运算。
第二个是一个 feature:实现二进制的 base64 转换。这个也需要学习位运算的一部分知识。
上述优化已在日程中,日后有新版本我会持续跟进。