字符集、字符编码、unicode、uft-8

发布时间 2024-01-08 14:01:11作者: zk10152003

字符集就是字符的集合,如常见的 ASCII字符集,GB2312字符集,Unicode字符集等。
字符编码则代表字符集的实际编码规则,是用于计算机解析字符的,如 GB2312,GBK,UTF-8 等。字符编码的本质就是如何使用二进制字节来表示字符的问题。

Unicode是国际组织制定的,用于收纳世界上所有文字和符号的字符集方案。
前128个字符同ASCII一样,进行扩充后,使用数字0-0x10FFFF来映射这些字符,最多可以有1114112个字符。目前仍然只使用了其中的一小部分。
Unicode一般使用两个字节来表示一个字符。
码点
Unicode 规定了每个字符的数字编号,这个编号被称为 码点(code point)。
码点以 U+hex 的形式表示,U+是代表Unicode的前缀,而 hex 是一个16进制数。取值范围是从 U+0000 到 U+10FFFF。
每个码点对应一个字符,绝大部分的常见字符在最前面的 65536 个字符,范围是 U+0000到U+FFFF。
字符平面
目前的Unicode分成了17个编组,也称平面,每个平面有65536个码点。
第一个平面是基本多语言平面,范围:U+0000 - U+FFFF,多数常见字符都在该区间。
其他平面则为辅助平面,范围:U+10000 到 U+10FFFF,如我们在网上常见 Emoji 表情。
码元
码元(Code Unit)可以理解为对码点进行编码时的最小基本单元,码元是一个整体。而字符编码的作用就是将Unicode码点转换成码元序列。
Unicode常用的编码方式有 UTF-8 、UTF-16 和 UTF-32,UTF是Unicode TransferFormat的缩写。
UTF-8是8位的单字节码元,UTF-16是16位的双字节码元,UTF-32是32位的四字节码元。

UTF-8是一种可变长度的字符编码方式。目前是使用 1 到 4 个字节来编码字符。
UTF-8的编码规则:
1个字节的字符,第一位为0,后7位为码点,与ASCII相同。
n个字节的字符,第一个字节前面 n 位都是1,n+1位是0,可据此判断有几个字节。后面的几个字节都是 10 为开头2位。
这里规定的都是前缀,对于字符的码点,需要进行截取后依次放入除前缀外的其他位,所以UTF-8又被称为前缀码。

下面我们以一个中文字符的编码转换为例,如汉字 '好':
'好'的Unicode码点:'好'.codePointAt() \ 22909,结果是22909;
22909在UTF-8的3字节数的编码区间 U+0800 (2048) ~ U+FFFF (65535);
22909的二进制值:101100101111101,有15位;
而3字节数的编码需要16位,前面补0,根据表中规则分成3组:0101 100101 111101;
依次填入对应的前缀:11100101 10100101 10111101,得到3个字节;
将得到的三个字节转成十六进制数据:E5 A5 BD,所以汉字 '好' 的UTF-8就是:E5 A5 BD。

UTF-16的编码方式:基本平面的字符占用 2 个字节(U+0000到U+FFFF),辅助平面的字符占用 4 个字节(U+010000到U+10FFFF)。
也就是说,UTF-16的编码长度要么是2个字节要么是4个字节。当为2字节时,则实际上与Unicode相同。
并且还有个原则,在Unicode基本多语言平面内,从U+D800到U+DFFF之间的码点区间是不对应字符的。而UTF-16需要利用这块码位来对辅助平面的字符进行编码。
码点小于U+FFFF,基本字符,不需处理,直接使用,占两个字节。
否则,拆分成两个码元,四个字节,cp表示码点:
1、低位——((cp - 65536) / 1024) + 0xD800,值范围是 0xD800~0xDBFF;
2、高位——((cp - 65536) % 1024) + 0xDC00,值范围是 0xDC00~0xDFFF。

汉字 '好','好'.codePointAt() // 22909,码点小于U+FFFF,直接进行十六进制转换:579D。
表情符号 '?','?'.codePointAt() // 128516,码点需要拆分:
1、低位:Math.floor(((128516 - 65536) / 1024)) + 0xD800 // 55357, 得到 D83D
2、高位:((128516 - 65536) % 1024) + 0xDC00 // 56836,得到 DE04

使用 String.fromCharCode 方法进行验证:String.fromCharCode(0xD83D, 0xDE04) // '?'
需要明确的一点,Javascript中的字符串是基于UTF-16编码的,大端序字节。
// UTF-8
'a': 97 - 0x61
'好': 22909 - (0xE5 0xA5 0xBD)
'?': 128516 - (0xF0 0x9F 0x98 0x84)

// UTF-16
'a': 97 - 0x0061
'好': 22909 - 0x597d
'?': 128516 - (0xD83D, 0xDE04)

前端在对Unicode编码处理时,提供了一些可以使用的API,在实际工作中,会方便我们处理这方面的问题。
1、charAt(index)
从一个字符串中返回指定的字符,对于多码元字符,仍会返回码元字符:
'a'.charAt() // 'a'
'?'.charAt() // '\uD83D'
'?'.charAt(1) // '\uDE04'
2、charCodeAt(index)
返回0到65535之间的整数码点值。对于多码元如果字符的码点大于U+FFFF,则返回第一个码元值,还可以加索引参数取后面码元的值。
3、codePointAt(pos)
返回Unicode码点,多码元也能返回完整的码点值。codePointAt可以传入索引参数,对多码元字符取第二个码元值。
// 小于 U+FFFF
'好'.codePointAt() // 22909
'好'.charCodeAt() // 22909

// 大于 U+FFFF
'?'.charCodeAt() // 55357
'?'.charCodeAt(1) // 56836
'?'.codePointAt() // 128516
'?'.codePointAt(1) // 56836
4、String.fromCharCode(num1[, ...[, numN]])
返回由指定的UTF-16码点序列创建的字符串。参数范围0到65535,大于65535的数据将被截断,结果不准确。
对于多码元字符,则会将两个码元组合得到该字符。
5、String.fromCodePoint(num1[, ...[, numN]])
返回使用指定的代码点序列创建的字符串。可以处理多码元字符的完整码点值。
String.fromCharCode(55357, 56836, 123) // '?{'
String.fromCodePoint(128516, 123, 8776) // '?{≈'

TextEncoder,使用 UTF-8 编码将代码点流转换成字节流。
TextDecoder:解码。
默认编码方式就是UTF-8,可以解决字符转UTF-8编码的问题。
const txtEn = new TextEncoder()
const enVal = txtEn.encode('好')
// Uint8Array(3) [229, 165, 189]
const txtDe = new TextDecoder()
txtDe.decode(enVal) // '好'

URL的UTF8编解码
encodeURI() 和 encodeURIComponent()
decodeURI() 和 decodeURIComponent()

encodeURI('好') // '%E5%A5%BD'
decodeURI('%E5%A5%BD') // '好'
encodeURIComponent('好') // '%E5%A5%BD'
decodeURIComponent('%E5%A5%BD') // '好'
encodeURI('hello') // 'hello'
encodeURIComponent('hello') // 'hello'
encodeURIComponent('?') // '%F0%9F%98%84'

encodeURI和encodeURIComponent的区别
这两者的不同之处,在于对部分URL元字符符号的处理上。URL元字符:分号(;),逗号(’,’),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#)。
encodeURIComponent会对这些URL元字符进行编码,但是encodeURI则不会:
encodeURIComponent(';,/@&=') // '%3B%2C%2F%40%26%3D'
encodeURI(';,/@&=') // ';,/@&='