字符发展历史

字节

  • 计算机存储时候一种计量单位; 计算机内部, 所有信息最终都是一个二进制值
  • 字节是通过网络传输信息的单位, 一个字节最大值十进制数是 255
  • 每一个二进制位(bit) 有 0 和 1 两种状态, 因此八个二进制位就可以组合出 256 种状态, 被称为一个字节(byte)

单位

  • 8bit -> 1byte
  • 1024byte -> 1K
  • 1024K -> 1M
  • 1024M -> 1G
  • 1024G -> 1T

JavaScript 中的进制

  1. 进制表示
    1
    2
    3
    4
    5
    6
    7
    let a = 0b10100; // 二进制
    let b = 0o24; // 八进制
    let c = 20; // 十进制
    let d = 0x14; // 十六进制
    console.log(a == b);
    console.log(b == c);
    console.log(c == d);
  2. 进制转换
    1
    2
    3
    4
    5
    6
    // 将任意进制转为十进制
    parseInt("任意进制字符串", 原始进制);
    parseInt('0b10100', 2);
    // 将十进制转为任意进制
    (十进制数字).toString(目标进制);
    (20).toString(2);

ASCII

最早计算机只在美国用, 八位的字节可以组合出 256 种不同的状态; 0 - 32 种状态规定了特殊用途, 一旦中断, 打印机遇上约定好的这些字节被传过来时, 就要做约定好的动作; 又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示, 一直编到了第 127 号, 这样计算机就可以用不同字节来存储英语的文字了

这 128 个符号(包括 32 个不能打印出来的控制符号), 只占用了一个字节的后面 7 位, 最前面的一位统一规定为 0, 这个方案就叫 ASCII 编码: American Standard Code for information Interchange 美国信息互换标准代码

GB2312

后来西欧一些国家用的不是英文, 他们的字母在 ASCII 里没有, 为了保证他们的文字, 他们使用 127 号后面的空位来保存新的字母, 一直编到了最后一位(255), 当然不同国家同一位表示的符号也不一样

中国为了表示汉字, 把 127 号之后的符号取消了, 规定

  • 一个小于 127 的字符的意义和原来相同, 但两个大于 127 的字符连在一起, 就表示一个汉字
  • 前面的一个字节(高字节)从 0xA1 用到 0xF7, 后面一个字节(低字节)从 0xA1 到 0xFE
  • 这样就可以大概组合出 7000 多个 (247-161)*(254-161) 简体汉字了
  • 还有把数学符号、日文假名和 ASCII 中原有的数字、标点、字母都重新变成两个字节的编码; 这就是全角字符, 127 以下就叫半角字符
  • 把这种汉字方案叫做 GB2312; GB2312 是对 ASCII 的中文扩展

GBK

后来还是不够用, 于是不再要求低字节一定是 127 号之后的内码, 只要第一个字节是大于 127 就固定表示这是一个汉字的开始, 又增加了将近 20000 个新的汉字(包括繁体字)和符号

GB18030 / DBCS

又加了几千个新的少数民族汉字, GBK 扩展成了 GB18030, 统称它们叫 DBCS: Double Byte Character Set 双字节字符集

在 DBCS 系列标准中, 最大的特点就是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案中

各个国家都像中国这样搞出一套自己的编码标准, 结果互相之间谁也不懂谁的编码, 谁也不支持谁的编码

Unicode

ISO 的国际组织废了所有的地区性编码方案, 重新搞了一个包括了地球上所有文化、所有字符和符号的编码, Unicode 是一个很大的集合, 现在的规模可以容纳 100 多万个符号

  • International Organization for Standardization 国际标准化组织
  • Universal Multiple-Octet Character Set, 简称 UCS, 俗称 Unicode

ISO 就直接规定必须用两个字节, 也就是 16 位来统一表示所有的字符, 对于 ASCII 里面的那些半角字符, Unicode 保持其源编码不变, 只是将其长度由原来的 8 位扩展为 16 位, 而其他文化和语言的字符则全部重新统一编码

从 Unicode 开始, 无论半角英文字符, 还是全角汉字, 都是统一的一个字符, 同时, 也都是统一的两个字节

字节是一个 8 位的物理存储单元, 而字符则是一个文化相关的符号

UTF-8

Unicode 在很长一段时间无法推广, 直到互联网的出现, 为解决 Unicode 如何在网络上传输的问题, 于是面向传输的众多 UTF(Universal Character Set Transfer Format: UTF 编码) 标准出现了

  • UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式
  • UTF-8 就是每次以 8 个位为单位传输数据, 而 UTF-16 就是每次 16 个位
  • UTF-8 最大的一个特点就是它是一种针对 Unicode 的可变长度字符的编码, 也是一种前缀码
  • Unicode 一个中文字符占 2 个字节, 而 UTF-8 一个中文字符占 3 个字节

编码规则

JavaScript 使用的是 Unicode 字符集编写的, 但如果需要发送一段二进制到服务器的时候, 服务器规定二进制内容必须是 UTF-8, 此时就需要将 Unicode 字符串转成 UTF-8 编码的字符串

String 的 charCodeAt 方法, 会返回 Unicode 码

  1. 对于单字节的符号, 字节的第一位设为 0, 后面 7 位为这个符号的 Unicode 码, 因此对于英语字母, UTF-8 编码和 ASCII 码是相同的
  2. 对于 n 字节的符号(n > 1), 第一个字节的前 n 位都设为 1, 第 n+1 位设为 0, 后面字节的前两位一律设为 10, 剩下的没有提及的二进制位, 全部为这个符号的 Unicode 码

单个字符的大小占用字节数, 单个 Unicode 字符编码之后的最大长度为6个字节

  • 1个字节: Unicode 码为 0 - 127
  • 2个字节: Unicode 码为 128 - 2047
  • 3个字节: Unicode 码为 2048 - 0xFFFF
  • 4个字节: Unicode 码为 65536 - 0x1FFFFF
  • 5个字节: Unicode 码为 0x200000 - 0x3FFFFFF
  • 6个字节: Unicode 码为 0x4000000 - 0x7FFFFFFF

unicode-utf8

Unicode 码转换成 UTF-8 编码, Unicode 都是十六进制, 所有的汉字都是三个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function transferUnicodeToUtf8(str) {
let result = [], arr;
for (let i = 0; i < str.length; i++) {
// 获取 Unicode 值
const charCode = str[i].charCodeAt();
// 获取 charCode 二进制值
const binary = charCode.toString(2);
if (0x00 <= charCode && charCode <= 0x7f) {
arr = ['0'];
arr[0] += binary.padStart(7, 0);
} else if (0x80 <= charCode && charCode <= 0x7ff) {
arr = ['110', '10'];
arr[0] += binary.substring(0, binary.length - 6).padStart(5, 0);
arr[1] += binary.substring(binary.length - 6, binary.length - 9).padStart(6, 0);
} else if ((0x800 <= charCode && charCode <= 0xd7ff) ||
(0xe000 <= charCode && charCode <= 0xffff)) {
arr = ['1110', '10', '10'];
arr[0] += binary.substring(0, binary.length - 12).padStart(4, 0);
arr[1] += binary.substring(binary.length - 12, binary.length - 6).padStart(6, 0);
arr[2] += binary.substring(binary.length - 6).padStart(6, 0);
}
// 转成 UTF8 编码字节: arr.map(item => parseInt(item, 2));
// UTF8 编码字节转为 16 进制: arr.map(item => parseInt(item, 2).toString(16);
let transferred = arr.map(item => '%' + parseInt(item, 2).toString(16)).join('');
result.push(transferred);
}
}
let trans = transferUnicodeToUtf8('1 Ű 中');

// 再转回 Unicode
trans.map(item => decodeURI(item)).join('');

// 便捷方法, 但只支持转中文
encodeURI('中文字符');

文本编码

使用 NodeJS 编写前端工具时, 操作最多的是文本文件, 因此也就是涉及到文件编码的处理问题; 常用的文本编码有 UTF-8和 GBK 两种, 并且 UTF-8 文件还可能带有 BOM; 在读取不同编码的文本文件时, 需要将文件内容转换为 JS 使用的 UTF-8 编码字符串后才能正常处理

BOM 的移除: BOM 用于标记一个文本文件使用 Unicode 编码, 其本身是一个 Unicode 字符(\uFEFF), 位于文本文件头部; 在不同的 Unicode 编码下, BOM 字符对应的二进制字节如下:

1
2
3
4
5
Bytes       Encoding
---------------------
FE FF UTF16BE
FF FE UTF16LE
EF BB BF UTF8

可以根据文本文件头几个字节等于什么来判断文件是否包含 BOM, 以及使用哪种 Unicode 编码; BOM 字符虽然起到了标记文件编码的作用, 其本身却不属于文件内容的一部分, 如果读取文本文件是不去掉 BOM, 在某些使用场景下就会有问题, 如将几个 JS 文件合并成一个文件后, 如果文件中间含有 BOM 字符, 就会导致浏览器 JS 语法错误, 因此, 使用 NodeJS 读取文本文件时, 一般要去掉 BOM

1
2
3
4
5
6
7
8
const fs = require('fs');
function readText(pathname) {
const bin = fs.readFileSunc(pathname);
if (bin[0] === 0xef && bin[1] === 0xbb && bin[2] === 0xbf) {
bin = bin.slice(3);
}
return bin.toString();
}

GBK 转 UTF-8: NodeJS 支持在读取文本文件或在 Buffer 转换为字符串时指定文本编码, 但 GBK 编码不在 NodeJS 自身支持范围内; 因此, 一般借助 iconv-lite 这个第三方包来转换编码

1
2
3
4
5
const iconv = require('iconv-lite');
function readGBKText(pathname) {
const bin = fs.readFileSync(pathname);
return iconv.decode(bin, 'gbk');
}