字符集
一组字符的集合,每个字符都有一个唯一的编号,称为 码点 。字符集的作用是将字符和码点一一对应起来,形成一个对照表。
ASCII
早期计算机起源于美国,ASCII字符集
是为英语设计的。共收录了 128 个字符,其中包括:可见字符(字母、数字、标点符号)和 控制字符(Tab、回车)。
Latin1
它是一种单字节编码,能够表示 256 个不同的字符。能够兼容 ASCII字符集
,并在其基础上扩展了额外的字符,以支持西欧语言中的特殊字符(如法语、德语、西班牙语等)。
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编码
等方式取代了。
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 格式
window.btoa
console.log(window.btoa("hello world!")); // aGVsbG8gd29ybGQh
btoa
只支持 Latin1字符集
的字符,对于 非 Latin1字符集
的字符会抛出错误
window.btoa("你好!")
TextEncoder
和 Blob
let str = "你好!"
const blob = new Blob([new TextEncoder("utf-8").encode(str)], {
type: "text/plain"
})
let render = new FileReader()
render.readAsDataURL(blob)
render.onload = function () {
console.log(render.result)
}
Base64 格式转为 字符串编码
window.atob
只能处理属于 Latin-1字符集
的字符。
window.atob("aGVsbG8gd29ybGQh")
代码实现
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
会被去除,因此不会影响解码结果。