编码
字符 —> 二进制之间的映射
ASCII
- 采用 ASCII 编码的字符,每个字符固定使用 1 个字节(byte/8bites)。因此,如果一个文本 只包含 ASCII 字符,那么它的字符数就等于字节数。
如
hello字节数就是 5 - 将数字、英文、常见字符映射到 0-127 的数值空间里。
其中最高 bit 位是 0,剩余 7 位用来真正的编码。故
2^7=128如h十进制的码值是 104,二进制码值就是:0110 1000
ASCII 码表
GBK / ANSI
-
双字节的编码,故最多能表示
2^16 = 65535个字符。“GBK 映射为双字节” 只对中文、日文等非英文字符成立,对英文还是 1 字节。
Unicode
- GBK 两个字节编码,只能表示 65535 个字符,全世界的文字字符总和远超这个数(汉语就有 55000 多字符),所以需要 3 个或 4 个甚至更多字节来表示一个字符。
- 于是出现了
Unicode,为每一个有效字符定义的一个唯一的整数,称为码点 。这些码点通常使用十六进制来表示,并以U+作为前缀。可以容纳 100 多万的字符(甚至包括表情包)。严格来讲,Unicode 是抽象映射(十六进制形式存储的字符集),不是一种编码方式。
length 的取值问题
字符:通常指的是 Unicode 的码点。 字素:视觉上认为的一个字。可能由多个码点组成。如:带组合音标的拉丁字母、国旗、emoji 码元:某种具体编码在内存里用的“最小存储块”。
- UTF-16:一个码元是 16 位。JavaScript / Java / C# 等(UTF-16 字符串模型),不同语言标准不同。
- str.length(JS):统计的是 UTF-16 码元个数,不是码点个数,更不是字素个数。
- Unicode 把全部码点分成多个平面,其中 BMP(Basic Multilingual Plane,基本多文种平面) 内的码点通常 1 个 UTF-16 码元;增补平面(很多 emoji)通常需要 2 个 UTF-16 码元。 对于 BMP 字符,UTF-16 就是直接拿码点的二进制,无需拼凑;对于辅助平面,虽然要算代理对,但逻辑也不同于 UTF-8 的位分布。
- 所以 js 中很多 emoji、𠮷 这类可能显示为 2。
UTF-8
- 一个字符就需要 3 个字节表示,对于英文这样一个只需 1 个字节就可以表示的,太浪费了,于是需要对 Unicode 进行“压缩”编码,于是就有了 UTF-8、UTF-16、UTF-32 等编码。
- UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度。有效利用存储空间,避免浪费。
A | Unicode 码点 | UTF-8 | UTF-16 |
|---|---|---|---|
| 十六进制 | U+0041 | 0x41 | 0x0041 |
| 十进制 | 65 | 65 | 65 |
| 十进制计算方式 | 'A'.codePointAt(0) | new TextEncoder().encode("A")[0] | 'A'.charCodeAt(0) |
| 二进制 | 0000 0000 0100 0001 | 0100 0001 | 0000 0000 0100 0001 |
UTF-8的编码规则:
- 对于单字节的符号,字节的最高位设为 0,后面 7 位为这个符号的 Unicode 码点的二进制。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
- 对于 n(n>1)字节的符号,第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,由这个符号的二进制进行填充。
下表总结了编码规则,字母x表示可用编码的位
| Unicode 符号范围 (十六进制) | UTF-8 编码方式(二进制) |
|---|---|
| 0000 0000-0000 007F | 0xxxxxxx(和 ASCII 编码一致) |
| 0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
| 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
| 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- 以汉字
严为例,已知严的Unicode码点是4E25(1001110 00100101),根据上表,4E25 处在第三行的范围内0000 0800-0000 FFFF,对应的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。- 接着从
严的最后一个二进制位开始,依次从后向前填入格式中的 x,空位补 0。- 然后就得到了,
严的 UTF-8 编码是11100100 10111000 10100101,转换成十六进制就是E4 B8 A5。
Base64编码
编码原理:
- 首先需要将字符转换为二进制,因此 Base64 的结果完全依赖于中间选择的字符编码方式。
- 将构成编码对象的二进制进行拼接起来,按 3 字节(24bit) 一组处理。
- 再拆成 4 组 6 bit。若最后字节不足 6 bit 位,用
0补。 - 转换成十进制,然后对应 Base64 码表找到对应的字符。
用 6 个 bit 位来表示一个字节,
2^6=64。包含:大小写字母(52)、数字(10)、+、/ - 若最终 Base64 字符数量不是 4 的倍数,再用
=补齐。
模拟实现
const base64Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function base64Encode(str) {
// 常用 UTF-8 编码,不会造成空间浪费
const bytes = new TextEncoder().encode(str); // [ 228, 184, 150, 74 ] 其中 世 占三个字节,J 占一个字节
let binary = "";
for (const b of bytes) binary += b.toString(2).padStart(8, "0");
let result = "";
for (let i = 0; i < binary.length; i += 6) {
let chunk = binary.slice(i, i + 6);
if (chunk.length < 6) chunk = chunk.padEnd(6, "0");
result += base64Chars[parseInt(chunk, 2)];
}
const pad = (4 - (result.length % 4)) % 4;
return result + "=".repeat(pad);
}
const char = "世J";
console.log(base64Encode(char)); // 5LiWSg==
Base64 的优点和缺点:
- 优点
- Base64 编码可以将任意的二进制数据转换成 ASCII 可表示的字符集(纯文本格式),在数据传输和存储中有重要作用;
可安全跨系统传输、且不容易冲突的可打印字符远没有 256 个;会包含大量控制字符/特殊字节,邮件、URL、JSON、HTTP 头等场景容易出问题。 Base64 选了 64 个安全字符,接受约 33% 膨胀来换兼容性。 8bit 那套更像“原始字节流”而不是“安全文本编码”。
- 减少服务器访问次数
以前(HTTP/1.1)这点较明显; 现在 HTTP/2/HTTP/3 多路复用后,这个优势相对变小; 内联 Base64 仍可用于“小图标、避免额外请求”的场景。
为什么 base64 可以是文本,原始字节的是二进制流?
-
关键不在 “6bit/8bit” 本身,而在你把这些 bit 映射成什么。
-
Base64:把数据映射到一组 “可打印字符”(A-Z a-z 0-9 + / =)。这些字符在 UTF-8/ASCII 里都有明确文本表示,所以看起来就是文本字符串,可放进 JSON、HTML、邮件正文。
-
原始 8bit 字节:每个字节可取 0~255,里面大量值对应控制字节或不可打印内容。不一定能按文本编码正确解释,常会乱码/截断/协议冲突,所以通常按“二进制流”处理。未经文本编码约束的字节集合在工程上更适合作为流。
-
缺点
- Base64 编码后通常约增加 33%(原来3 字节原始数据 → 4 字节 Base64 文本)。
落盘/内存里每个 Base64 字符一般是 8bit。 数据越大,整体越接近 +33%,小数据时因为 = 和分组边界,涨幅可能不止 33%。
- Base64 资源通常跟随 HTML/CSS/JS 一起缓存,不能像独立图片那样细粒度复用缓存。
- Base64 编码后的字符串无法直接阅读,且解码需要额外的计算开销(虽然不大)。
- 不是加密:可直接解码,不提供保密性。
应用场景:
- 小体积资源内联:如 HTML/CSS 中嵌入小图标(data URL:
< img src="data:image/png;base64,...">)。 - 二进制数据需要放进“只适合文本”的通道:如邮件内容、JSON/XML 字段、日志、配置文件。
- 令牌与签名相关文本:如 JWT 的各段(常见是 Base64URL 变体)。
- URL/表单中传输少量二进制片段(更常见用 Base64URL,避免 + /)。