字符集
一组字符的集合,每个字符都有一个唯一的编号,称为 码点 。字符集的作用是将字符和码点一一对应起来,形成一个对照表。
ASCII
早期计算机起源于美国,ASCII字符集 是为英语设计的。共收录了 128 个字符,其中包括:可见字符(字母、数字、标点符号)和 控制字符(Tab、回车)。
Latin1
Latin1(正式名称是 ISO-8859-1)是一个单字节字符集,也就是说它用 1 个字节(8 bit) 来表示一个字符,因此最多只能表示 256 个字符(0–255) 。能够兼容 ASCII字符集 ,并在其基础上扩展了额外的字符,以支持西欧语言中的特殊字符(如法语、德语、西班牙语等)。
| 范围(十六进制) | 内容说明 |
|---|---|
0x00 – 0x7F | 与 ASCII 完全一致(英文字母、数字、常见标点、控制字符) |
0x80 – 0x9F | 控制字符(很少使用) |
0xA0 – 0xFF | 西欧语言常用字母和符号,比如:é、ñ、ü、©、§、¿ 等 |
GBK
ASCII 字符集 无法兼容中文字符,为解决中文编码问题,设计出了 GBK字符集 ,也被称为国标码,兼容 ASCII 字符集 。GBK字符集 共收录了 21886 个字符,包括汉字(部首、构件)和图形符号。
Unicode
为实现各国语言字符的互通,国际上设计出了 Unicode字符集 ,也被称为万国码,兼容 ASCII字符集 。它包含了3万多个字符。
Base64
由于 ASCII字符集 中包含控制字符,导致并非所有字符都是可见的。为了将二进制数据编码为可见的文本数据,Base64字符集 被推出。它包含 64 个字符,由大小写字母(A-Z、a-z)、数字(0-9)以及两个特殊字符(通常是“+”和“/”)组成,再加上一个用于填充的“=”字符。
编码
编码是指将原始数据(如文本、图片)按照不同的 字符集 转换为二进制数据。反过来,将二进制数据转换回原始数据的过程被称为解码。
UTF-8
最常见的编码方式,使用了 Unicode字符集 进行转换字符,编码输出的二进制长度是1字节~4字节,也就是动态长度。
兼容 ASCII字符集 ,英文占用1个字节。中文字符占用3个字节。
留个悬念:中文在 Unicode字符集 的码点在19968~40959,看起来 2**16 完全够了,而 UTF-8编码 还是规定需要三个字节储存???
| 字符 | 码点 | 二进制 |
|---|---|---|
| 我 | 25105 | 01100010 00010001 |
| i | 105 | 01101001 |
| 你 | 20320 | 01001111 01100000 |
如果我们按照上面的二进制储存到文件中:
我i你 ==> 0110001000010001011010010100111101100000
这就遇到一个问题,如何去解析文件中的二进制数据喃?因为UTF-8编码 是动态字节长度的。
当前字符需要解析多少个字节是未知的,比如:
前8位解析Unicode 字符是 b
前16位解析Unicode 字符是 我
前24位解析Unicode 字符是 错误!!
动态字节长度 是 UTF-8编码的特点,也是他必须要解决的问题,所以 UTF-8编码 设置了一套二进制储存规范。
| UTF-8编码「二进制」 | |
|---|---|
| 0xxxxxxxx | 1字节 |
| 110xxxxx 10xxxxxx | 2字节 |
| 1110xxxx 10xxxxxx 10xxxxxx | 3字节 |
| 11110xxx 10xxxxxx 10xxxxxx | 4字节 |
| 字符 | 码点 | 二进制 |
|---|---|---|
| 我 | 25105 | 11100110 10001001 10000001 |
| i | 105 | 01101001 |
| 你 | 20320 | 11100100 10111101 10100000 |
我i你 ==> 11100110100010011000000101101001111001001011110110100000
// 方案1
new TextEncoder().encode("我i你") // Uint8Array(7) [230, 136, 145, 105, 228, 189, 160]
// 方案2
const render = new FileReader()
render.readAsArrayBuffer(new Blob(["我i你"]))
render.onload = function () {
console.log(new Uint8Array(this.result)) // Uint8Array(7) [230, 136, 145, 105, 228, 189, 160]
}
虽然比原始的二进制数据长,但是可以去按照 开头的标识 去解析二进制数据了。
但是也带来一个问题:不是所有的二进制数据都有所对应的 Unicode字符 。但是当二进制数据找不到所对应的 Unicode字符 时,就会发生我们熟知的乱码问题。
比如:10000000
UTF-32
前面聊到 UTF-8编码 的时候,不得不提到 UTF-32编码。不过,UTF-32编码 出现得比 UTF-8编码 早,但后来却逐渐被淘汰了。
UTF-32编码 是一种固定字节的编码方式,不论字符在 Unicode字符集 中处于什么码点,都用 4 字节 来记录。这样做的好处是字符表示简单直接,能涵盖所有 Unicode字符,还避免了像 UTF-8编码 那样需要识别字节前缀的复杂性。
但它的缺点也很明显,那就是太占空间了。每个字符都用 4 字节,即使是简单的 ASCII 字符也不例外,这在存储和传输大量文本时,会带来很大的资源浪费。
所以,尽管 UTF-32编码 在某些特定场景下仍有其价值,但整体上,它还是因为效率和资源占用的问题,逐渐被 UTF-32编码 等方式取代了。
Latin1
Latin1编码 使用的是 Latin1字符集,因为Latin1字符集 只能表示256 个字符(0–255),因此
是 Latin1编码 1字符=1字节。
Base64
Base64编码 使用的是 Base64字符集 ,是一种将数据转换为可打印字符的编码方式,常用于文件、字符串等数据的可视化传递。
-
使用 64个字符(包括大小写字母、数字和特殊符号)作为字符集,确保所有字符都是可视的。
-
Base64编码的每个字符需要 6位 二进制数据。如果在编码过程中,二进制数据的长度不是 6 的倍数,会在二进制数据末尾添加 0 以补足至 6 的倍数。- 示例: A 二进制数据是 01000001,长度是 8,不是 6 的倍数,会在末尾添加 0,变成 010000010000,使其长度成为 6 的倍数。
-
Base64编码的规则要求编码后的字符串长度必须是 4 的倍数。如果编码后的字符串长度不是 4 的倍数,会在末尾添加=作为填充字符,以补足至 4 的倍数。- 示例:
A经过 Base64 编码后得到的字符串是 QQ,其长度为 2,不是 4 的倍数。为了符合 Base64编码的规则,会在末尾添加=,使其变为 QQ==,长度变为 4,是 4 的倍数。
- 示例:
代码实现
Base64 编码
- 在编码时,以 6位 二进制为一组进行分割。如果最后的二进制数量不足 6位,则进行补 0。
- 在编码成功时,判断
Base64长度是否能被 4整除,如果不足 4 个字符,就进行补 =。
// 定义Base64字符集
const base64CharSet = {};
// 初始化Base64字符集,将索引与字符对应
`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`.split("").forEach((char, index) => base64CharSet[index] = char);
/**
* 将字符串转换为Base64编码
* @param {string} inputStr - 需要转换的字符串
* @returns {string} Base64编码后的字符串
*/
function base64Encode(inputStr) {
// 用于存储最终的Base64编码结果
let base64String = "";
// 将输入字符串的每个字符转换为对应的ASCII码的二进制表示,并拼接成一个长的二进制字符串
// 注意:每个字符的二进制表示需要补足8位
let binaryString = inputStr.split("").map(char => {
return char.charCodeAt().toString(2).padStart(8, '0');
}).join("");
// 如果二进制字符串的长度不是6的倍数,则在末尾补0,使其长度成为6的倍数
if (binaryString.length % 6 !== 0) {
binaryString += "0".repeat(6 - binaryString.length % 6);
}
// 遍历二进制字符串,每6位一组进行处理
for (let index = 0; index < binaryString.length; index += 6) {
// 提取每6位二进制字符
let sixBitSegment = binaryString.substring(index, index + 6);
// 将6位二进制字符转换为十进制索引
let indexInBase64Set = parseInt(sixBitSegment, 2);
// 根据索引从Base64字符集中获取对应的字符,并添加到结果中
base64String += base64CharSet[indexInBase64Set];
}
// 如果Base64编码结果的长度不是4的倍数,则在末尾补“=”字符
if (base64String.length % 4 !== 0) {
base64String += "=".repeat(4 - base64String.length % 4);
}
// 返回最终的Base64编码字符串
return base64String;
}
console.log(base64Encode("hello world!"));
Base64 解码
/**
* 将Base64编码的字符串转换为原始字符串
* @param {string} base64Str - 需要转换的Base64编码字符串
* @returns {string} 解码后的字符串
*/
function base64Decode(base64Str) {
let decodedString = ''; // 用于存储解码后的字符串
// 去除Base64字符串末尾的填充字符("=")
base64Str = base64Str.replace(/=+$/, "");
// 将Base64字符串转换为二进制字符串
let binaryString = base64Str
// 将字符串拆分为字符数组
.split("")
.map(char => {
// 找到字符在Base64字符集中的索引
let index = Object.entries(base64CharSet).find(entry => entry[1] === char)[0];
// 将索引转换为6位二进制字符串
return Number(index).toString(2).padStart(6, '0');
})
// 将所有二进制字符串拼接起来
.join("");
// 去除二进制字符串末尾多余的0(由于去除填充字符可能导致的多余部分)
binaryString = binaryString.slice(0, binaryString.length - binaryString.length % 8);
// 按每8位一组进行处理
for (let i = 0; i < binaryString.length; i += 8) {
// 提取8位二进制字符串
let byteSegment = binaryString.substring(i, i + 8);
// 将8位二进制字符串转换为字符代码
const charCode = parseInt(byteSegment, 2);
// 将字符代码转换为字符,并追加到解码后的字符串中
decodedString += String.fromCharCode(charCode);
}
// 返回解码后的字符串
return decodedString;
}
console.log(base64Decode("aGVsbG8gd29ybGQh"));
疑问
Base64编码 长度不是 4 的倍数时的处理
-
解码时的处理:
=不属于 Base64 字符集, 在解码过程中,解码器会自动识别并去除末尾的=填充字符。例如:
A的Base64码是 QQ==,在解析时,会去除末尾的 = ,变成 QQ。结论:
=不属于 Base64 字符集,仅用于补足长度。在解码时会被去除,因此不会影响解码结果。
Base64编码 转二进制数据长度不是 6 的倍数时,会进行的补位处理,会不会影响到 Base64解码?
-
解码时的处理: 在解码时,解码器会根据实际的
Base64编码长度来确定二进制数据的有效长度,并去除末尾多余的0。例如:
A在 Base64 解析时, 二进制数据为 010000010000,得到的二进制数长度为 12 位,而实际需要的长度为 8 位,那么就会去除末尾的 4 位0。结论:在 Base64 编码转二进制数据时,末尾添加的
0仅用于补足长度。在解码时,这些补位的0会被去除,因此不会影响解码结果。