一、编码是什么?为什么需要编码?
计算机中,所有数据的存储与计算都是通过二进制完成的,某个二进制串具体代表什么含义,就需要约定一套 映射关系,这套映射关系就叫做编码。
在通信时,双方必须遵守同一套编码,否则将会出现乱码的情况。
先抛出3个问题:
1、Js用的什么编码方式?为什么?
2、Url用的什么编码方式?为什么?
3、webpack将图片转base64有什么优势?缺陷呢?
二、ASCII 码
ASCII码是最早出现的编码方式,划分为两个集合:标准ASCII码、拓展ASCII码。
标准ASCII码是包含128个字符的7位二进制编码,这里需要注意的是计算机的最小数据存储单位是字节(Byte),而 1 Byte = 8 bit,而标准ASCII码只需要7 bit,多出来的1位就被用作奇偶校验位,奇校验规定:一个字节中1的个数必须是奇数,若非奇数,则在最高位b7添1;偶校验规定:一个字节中1的个数必须是偶数,若非偶数,则在最高位b7添1。
拓展ASCII码抛弃了校验位,是包含256个字符的8位二进制编码。它存在两个问题,一个是额外多出来这128个字符是非标准的,在不同的地区可能是不同的值,比如第130个字符(10000010)在法语中代表é,在希伯来语中却代表ג;第二个问题,256个字符对于亚洲人民来说还是不够用呀,像汉字有近10万个,常用汉字都有3500个,单个字节完全不够用,只能用多字节表示,如常见的GB2312编码标准采用双字节编码,最多可收录256 x 256 = 65536个字符,汉字编码还有BIG5、GBK等,本文不再赘述。
三、Unicode
世界上的编码方式如此之多,根本不统一,每种编码包含的字符还不全,要是有一种编码能将世界上的所有字符都包含进去,不是爽歪歪吗?
Unicode应运而生,也被叫做统一码、万国码、单一码,是统一码联盟(The Unicode Consortium)制定的可以容纳世界上所有文字和符号的编码系统。
Unicode编码系统可以分为编码方式和实现方式两种层次。编码方式是使用0 ~ 0x10FFFF(1到3个字节)来映射这些字符,最多可以容纳1114112个字符,且前128个字符与ascii码相同。如果你拿任意3个字节给计算机,计算机是不知道这3个字节是代表1个字符还是2个字符还是3个字符的,不方便计算机读取,所以为了将 Unicode 转化成真正适合存储的二进制数据,就需要 UCS-2(UCS:Universal Multiple-Octet Coded Character Set)、UCS-4、UTF-8、UTF-16、UTF-32(UTF:UCS Transformation Format)等 Unicode 实现方式。
UCS-2 用固定2个字节编码,0x0000 ~ 0xFFFF,包含65536个编码空间,但为了兼容Unicode,未使用0xD800 ~ 0xDFFF(共2048个)之间的码位,可以为全世界最常用的63000多个字符编码。UCS-2包含的字符还是不够多,所以出现了UTF-16,使用2字节或4字节进行编码,我们可以把UTF-16看作UCS-2的父集。
UCS-4 用固定4个字节编码,0x00000000 ~ 0x7FFFFFFF,包含20多亿个编码空间,但实际使用范围并不超过0x10FFFF,ISO(International Organization for Standardization)也承诺将不会为超出 0x10FFFF 的 UCS-4 编码赋值,在此基础上提出了同样是固定4字节且编码范围不超过 0x10FFFF 的 UTF-32,明面上UTF-32是UCS-4的子集,但目前来说他俩是相等的。[𠮷]
总结一下,UCS-2、UCS-4是旧标准,已经被淘汰了,现在都用它们的升级版 UTF-16 和 UTF-32 。
四、UTF-8
显然,对于使用英文的国家来说,不管是UTF-16还是UTF-32都太浪费存储空间了,所以UTF-8出现了,这是一种变长的编码方式,使用1 ~ 4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8的3个优点:
1、它可以用来表示Unicode标准中的任何字符
2、由于是变长的,不会浪费太多存储空间
3、单字节字符仍与标准ASCII码相容
4.1、编码规则
UTF-8 的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。所以说兼容ascii码。
2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
| Unicode编码 | UTF-8编码(二进制) |
|---|---|
| U+0000 – U+007F | 0xxxxxxx |
| U+0080 – U+07FF | 110xxxxx 10xxxxxx |
| U+0800 – U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 – U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
举个栗子,‘洁’ 这个字的Unicode编码为6D01(二进制为01101101 00000001),对应上表中的第三行,代表这个字在UTF-8编码中需要用3个字节存储,将对应的二进制填入第三行中的 x,多出的位补0,得到11100110 10110100 10000001,转化成十六进制为E6B481,这就是 ‘洁’ 字的UTF-8编码。
为了验证编码是否正确,我们可以用encodeURIComponent方法,不要去用网上的「在线UTF8编码转换」,绝大多数都转成了UTF-16。
4.2、字节序
大家有时候会看到UTF-16BE、UTF-16LE 和 UTF-16,意义如下:
- UTF-16BE Sixteen-bit UCS Transformation Format,
big-endian byte order - UTF-16LE Sixteen-bit UCS Transformation Format,
little-endian byte order - UTF-16 Sixteen-bit UCS Transformation Format,
byte order identified by an optional byte-order mark(BOM)
UTF-16BE 代表大端字节序,即将高位的字节存储在起始地址。
UTF-16LE 代表小端字节序,即将低位的字节存储在起始地址。
UTF-16 无指定后缀,代表字节序由起始的两个字节决定,FE FF(零宽非中断空格)表示大端,FF FE表示小端。
那为什么我们要关注字节序的问题?这是由于如果不同的程序在存储数据的时候采用的方式不同,那么在数据交换的时候就会发生错误。这很容易理解,比如 ‘洁’ 字大端存储为6D01,若以小端方式解析成016D,就会变成 ŭ 这个符号,含义完全就变了。而UTF-8不存在字节序要求,这也是优势之一。
此外,UTF-8还有边界明确、容错率高的优势,在中间丢失了一段编码的时候,影响面最小。缺点也是有的,第一点是UTF-8编码中文需要3个字符,所以对于纯中文文本来说UTF-16或者GB2312等编码明显更优;第二点是在统计字符数量和执行索引操作上的效率都不算高。
4.3、代码实现
/* utf-8.mjs */
// 将 字符串 转化为 十进制数组
export function encodeUtf8(str) {
let bytes = [];
for (const s of str) {
// 获取每个字符的码点
const binaryData = s.codePointAt(0);
if (binaryData <= 127) {
bytes.push(binaryData);
} else if (binaryData <= 2047) {
bytes.push((binaryData >> 6) | 0xc0);
bytes.push((binaryData & 0x3f) | 0x80);
} else if (binaryData <= 65535) {
bytes.push((binaryData >> 12) | 0xe0);
bytes.push(((binaryData >> 6) & 0x3f) | 0x80);
bytes.push((binaryData & 0x3f) | 0x80);
} else if (binaryData <= 1114111) {
bytes.push((binaryData >> 18) | 0xf0);
bytes.push(((binaryData >> 12) & 0x3f) | 0x80);
bytes.push(((binaryData >> 6) & 0x3f) | 0x80);
bytes.push((binaryData & 0x3f) | 0x80);
}
}
return bytes;
}
// 由 十进制数组 转化成 16进制数
export function transferHex(arr) {
return arr.map((item) => item.toString(16)).join("");
}
transferHex(encodeUtf8("你好")); // e4bda0e5a5bd
/* ---------------------------------------------------------------------------- */
// 解码
export function decodeUtf8(str) {
// 将单个16进制数转化为4位的2进制数,并以字节分组
const bytes = [...str].map((item) => (new Array(4).join("0") + parseInt(item, 16).toString(2)).slice(-4)).join("").match(/(\d){8}/g);
const res = [];
let i = 0;
while (i < bytes.length) {
if (bytes[i][0] === '0') {
res.push(parseInt(bytes[i], 2));
i++;
continue;
} else if (bytes[i].slice(0, 3) === '110') {
res.push(parseInt(bytes[i].slice(-5) + bytes[i + 1].slice(-6), 2));
i += 2;
continue;
} else if (bytes[i].slice(0, 4) === '1110') {
res.push(parseInt(bytes[i].slice(-4) + bytes[i + 1].slice(-6) + bytes[i + 2].slice(-6), 2));
i += 3;
continue;
} else if (bytes[i].slice(0, 5) === '11110') {
res.push(parseInt(bytes[i].slice(-3) + bytes[i + 1].slice(-6) + bytes[i + 2].slice(-6) + bytes[i + 3].slice(-6), 2));
i += 4;
continue;
}
}
return res.map(item => String.fromCodePoint(item)).join("");
}
decodeUtf8("e4bda0e5a5bd"); // 你好
五、base64编码
我们在项目里面经常将一些小图片转化为base64,用来减轻服务器压力。它是一种编码的编码,同一字符在不同编码系统下的base64编码可能不同。为什么叫base64?是因为这是一种基于64个可打印字符来表示二进制数据的方法。
5.1、编码规则
每次取出3 byte的数据,放入24 bit的缓冲区中,如果剩余不足3 byte,则在后面补0。然后,每次从缓冲区中取出6 bit数据,往高位补0之后,按照对照表中的字符输出。当原数据长度不是3 byte的整数倍时, 如果最后余下1个字节,就在编码结果后加2个=;如果最后余下2个字节,就在编码结果后加1个=。
举个栗子,人民币符号¥的utf-8编码是:11000010 10100101
1、不足3字节,补足
| 11000010 | 10100101 | 00000000 |
|---|
2、拆分为6 bit一组并补0
00110000 | 00101010 | 00010100 | 00000000 |
|---|---|---|---|
| 48 | 42 | 20 | = |
3、转为字符
wqU=
特点:base64编码后的数据长度会比原数据多约1/3,且由于"+ / ="不适合放在url中传输,一般将"+"替换成"-" ","/"替换成"_",并去掉"="。
5.2、代码实现
/* base64.mjs */
import { encodeUtf8, decodeUtf8 } from "./utf-8.mjs";
// 索引映射
const CHARTS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// 前缀填充:需要填充的字符串、填充到几的倍数、填充符
function padStart(str, len, prefix) {
if (str.length % len === 0) {
return str;
}
return new Array(len - (str.length % len)).fill(prefix).join("") + str;
}
// 后缀填充
function padEnd(str, len, suffix) {
if (str.length % len === 0) {
return str;
}
return str + new Array(len - (str.length % len)).fill(suffix).join("");
}
// 编码
export function encodeBase64(str) {
let binaryData = ""; // 二进制数
let ending = ""; // 需补充的=
for (const s of encodeUtf8(str)) {
binaryData += padStart(s.toString(2), 8, "0");
}
if (binaryData.length % 6 === 2) {
// 余2 表示剩下1个字节 需补充2个=
ending = "==";
} else if (binaryData.length % 6 === 4) {
// 余4 表示剩下2个字节 需补充1个=
ending = "=";
}
binaryData = padEnd(binaryData, 6, "0");
return binaryData.match(/(\d){6}/g).map((item) => CHARTS[parseInt(item, 2)]).join("") + ending;
}
/* ---------------------------------------------------------------------------- */
// 解码
export function decodeBase64(str) {
// 去掉 =,并分割成数组
let [...res] = str.replace(/=/g, '');
// 转化为utf-8编码
res = res.map((item) => {
let value = CHARTS.indexOf(item);
return padStart(value.toString(2), 6, "0");
}).join('').match(/(\d{8})/g).map((item) => parseInt(item, 2).toString(16)).join("");
// 使用utf-8解码
return decodeUtf8(res)
}
六、提供一份html代码方便测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>编码</title>
<style>
.origin {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.origin input {
margin-right: 12px;
}
.utf8 {
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="origin">
<div>字符串:</div>
<input id="input" type="text" />
<button id="encodeBtn">编码</button>
</div>
<div class="utf8">
<div>utf8编码:</div>
<textarea name="" id="utf8" cols="30" rows="10" disabled></textarea>
<br />
<button id="decodeBtn1">解码</button>
<input id="origin1" type="text" disabled />
</div>
<div>
<div>base64编码:</div>
<textarea name="" id="base64" cols="30" rows="10" disabled></textarea>
<br />
<button id="decodeBtn2">解码</button>
<input id="origin2" type="text" disabled />
</div>
<script type="module">
import { encodeUtf8, transferHex, decodeUtf8 } from "./utf-8.mjs";
import { encodeBase64, decodeBase64 } from "./base64.mjs";
encodeBtn.onclick = () => {
if (input.value === "") {
utf8.value = "";
base64.value = "";
origin1.value = "";
origin2.value = "";
return;
}
origin1.value = "";
origin2.value = "";
utf8.value = transferHex(encodeUtf8(input.value));
base64.value = encodeBase64(input.value);
};
decodeBtn1.onclick = () => {
if (utf8.value === "") {
return;
}
origin1.value = decodeUtf8(utf8.value);
};
decodeBtn2.onclick = () => {
if (base64.value === "") {
return;
}
origin2.value = decodeBase64(base64.value);
};
</script>
</body>
</html>
七、拓展:URL编码
encodeURI 和 encodeURIComponent 都是以UTF-8的规则对URL进行编码。
encodeURI 编码所有字符,除了:
A–Z a–z 0–9 - _ . ! ~ * ' ( )
; / ? : @ & = + $ , #
encodeURIComponent 的编码范围更广,上面第二行的字符也会被编码。
const set1 = ";/?:@&=+$,#"; // Reserved Characters
const set2 = "-_.!~*'()"; // Unreserved Marks
const set3 = "ABC abc 123"; // Alphanumeric Characters + Space
console.log(encodeURI(set1)); // ;/?:@&=+$,#
console.log(encodeURI(set2)); // -_.!~*'()
console.log(encodeURI(set3)); // ABC%20abc%20123 (the space gets encoded as %20)
console.log(encodeURIComponent(set1)); // %3B%2C%2F%3F%3A%40%26%3D%2B%24%23
console.log(encodeURIComponent(set2)); // -_.!~*'()
console.log(encodeURIComponent(set3)); // ABC%20abc%20123 (the space gets encoded as %20)