字符编码和Emoji,理解编码,utf-8,utf-16,utf-32

字符编码

1:认识emoji和字符编码

如果您是初学者,了解编码可以让您对软件代码有一个大致的了解。如果你是中级开发者,了解编码有助于提升水平。

平时工作学习中判断字符串长度,'中'.length 输出 1 ,但实际上它占用了个字节。

你是否会感觉到好奇,不妨试下以下代码:

String.fromCharCode(55357,56425,55356,57339,8205,55358,56752)

不出意外的话,你会看到一个美女emoji:👩🏻‍🦰

里面发生了什么?

为什么这么一串数字就能组成一个emoji?

下面程序无法正常打印出信息。会出现 � 特殊符号:

let a = '😂123';
for(let i = 0;i<a.length;i++){console.log(a[i])}

会什么会出现特殊符号?

如果你不了解前端相关知识,不过这没关系,重要的是认识编码相关知识:您可以打开一个你喜欢的浏览器,并且打开控制台,复制这串代码 String.fromCharCode(55357,56425,55356,57339,8205,55358,56752) 到浏览器控制台,点击回车就可以看到效果。

2:ASCII编码

在这里插入图片描述

上个世纪,一群聪明而又令人尊敬的前辈研究出了一套ASCII编码,其中包含了26个英文字母及其他的符号。共128个字符。

随着全球化发展和计算机的流行,ASCII编码渐渐的不能满足每个地区的需求,所以相继出现了ANSI、GBK、GB2312、GB18030等。

为了统一编码,Unicode 编码出生。

Unicode 只是字符集,它包含了 UTF-8、UTF-16、UTF-32 编码规则。

Unicode足够大,大到可以容纳世界上所有的字符。

试下下面这串代码:

String.fromCharCode(97,98,99)

你会看到 abc 三个字母。

3:开机手册

在这里插入图片描述
如果你是javascript从业者,你可以使用:

  1. charCodeAt
  2. codePointAt(UTF-16 码元)

来查看字符对应的unicode编码:

如:'a'.charCodeAt(0),你将看到一个数字 97

如果是python,请使用 ord('a'),你将看到一个数字 97

此外,你可以登录部分网站查看:

分享一个可以查询字符对应unicode编码的网站,你可以方便的在上面查询你想知道的字符对应的编码。

链接地址:查询unicode编码(爱)

相应的,你还可以将 unicode 编码转为对应的字符:

  1. String.fromCharCode()
  2. String.fromCodePoint() (UTF-16 码元)

如:String.fromCharCode(97)

4:可疑的长度

在这里插入图片描述
如果你是前端开发者,应该经常使用 String 的 length 属性。

很多人都说中文占 2 个字节,占 3 个字节,但使用 length 属性的时候明明看到的是长度为 1

这是为什么?

如:'中'.length ,js编译器输出了 1

但实际上,它占用了 3 个字节。

字符串的length数据属性包含 UTF-16 代码单元中字符串的长度。

以上内容摘自 MDN。

从文档中得知,它的长度计算是以 UTF-16 编码格式计算的。

如果以 UTF-16 编码格式编码,那两个字节最大可以保存 65536 个字符,而我们常用,接触到的字符大部分小于该数值,所以没有发生过错误,起码是在我们日常使用过程的印象中,是这样的。

它是有风险的。

'𐆙'.length

该字符会输出为2。

是不是有点疑惑,不过不用担心,继续往下。

5:UTF-8

在这里插入图片描述
想一下,utf-8 在你心中是个什么东西?

水果?

蔬菜?

汉堡包?

在规范中介绍,它是一个编码格式,不仅仅只在 unicode 中使用,要理解其中含义。

比如:unicode中包含了大量字符,如果直接使用 unicode 来传输数据,会造成很大的资源浪费。

在 utf-8 中,一个字节可以存储 128 中字符,其中包含了全部英文字母,而中文大部分占 3-4个字节,如果都用这么大的空间传输数据,对使用英文字母的网络将会多消耗 3-4 倍的流量。这是消费者无法接受的。

utf-8 的出现解决了这个问题,它是一种针对Unicode的可变长度字元编码。

通俗来将就是,英文继续使用单字节,中文使用3-4个字节。

既然有些字符占用三个字节,有些占用1个字节,那它们是怎么区分的呢?

或者说,utf-8 长度是可变的,那么它是怎么知道一个字符的长度的呢?

Unicode编码范围(16进制)UTF-8编码方式(二进制)
U+0000 - U+007F0xxxxxxx
U+0080 - U+07FF110xxxxx 10xxxxxx
U+0800 - U+FFFF1110xxxx 10xxxxxx 10xxxxxx
U+10000 - U+1FFFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U+200000 - U+3FFFFFF111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U+4000000 - U+7FFFFFFF1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

对于单字节,需要单独使用一位前缀标志。之前可以使用8位来保存的值,此时只能使用7位。

所以在utf-8中,一个字节并不能保存256个值,单字节至多保存128个值。

根据之前学习的知识,我们可以知道 “家” 的unicode值为 23478。

'家'.charCodeAt()  // 23478

23478 转换为二进制为 0101101110110110

对照上表分开存储:

  1. 单字节只能存储 7 位(不包含前缀),而0101101110110110 为16位
  2. 使用两个字节可以存储 11 位(不包含前缀),依然还是不够。
  3. 使用三个字节可以存储 16位(不包含前缀),刚刚好。

除此之外,你可以使用范围区间计算,U+0800 - U+FFFF 转化为十进制为 2048 - 65535,可以容纳 23478。

1110xxxx	10xxxxxx	10xxxxxx

先看第一位:1110xxxx

可以容纳 4 位,所以从 0101101110110110 前取出 4 位,将 0101 放入 1110xxxx ,此时:

0101101110110110 = 101110110110 (a)

1110xxxx = 11100101 (b)

再看第二位:10xxxxxx

可以容纳 6 位,所以继续从 a 101110110110 前取出 6 位,将 101110 放入 10xxxxxx ,此时:

101110110110 = 110110 ©

10xxxxxx = 10101110 (d)

继续看第三位:10xxxxxx

可以容纳 6 位,所以继续从 © 110110 前取出 6 位,将 110110 放入 10xxxxxx ,此时:

10xxxxxx = 10110110 (e)

最终结果就是:

b + d + e = 11100101 10101110 10110110

到这里我们就成功使用 utf-8 编码后的二进制来表示字符 “家”

在这里插入图片描述

虽然 utf-8 可以节省空间,但它确实也很占用空间。

6:UTF-16

在这里插入图片描述

刚开始,我们看到了一个问题,经常有人说中文占用3个字节或者4个字节,那为什么 '家'.length 会返回长度 1 呢?

这里就需要了解 UTF-16 编码方式。

它也是一个可变长的编码,只不过相较于 UTF-8,每单位最少使用16位长的码元来进行表示。

16进制编码范围UTF-16表示方法(二进制)
U+0000 - U+FFFFxxxx xxxx xxxx xxxx - yyyy yyyy yyyy yyyy
U+10000 - U+10FFFF1101 10yy yyyy yyyy - 1101 11xx xxxx xxxx

这里继续先以字符 为例,它的unicode码值为 23478,小于 U+10000 ,所以我们直接可以使用 xxxx xxxx xxxx xxxx - yyyy yyyy yyyy yyyy

23478 转换为二进制为 101101110110110

那么以 UTF-16 编码方式编码最终显示结果为 0000 0101 1011 1011 0110

如果字符unicode码值大于 U+10000 ,那么就要额外处理一步了。

第一步:首先,需要减去 0x10000

这里以字符 'ഘ' 为例,它的 unicode 码值为 10d18(十六进制)

0x10d18 - 0x10000 得出 0xd18,转换为二进制为:110100011000

第二步:分割,分为前十位,后十位

注意,应当先保证后十位成功截取,不足则补 0,所以将 110100011000 转换为 00000000110100011000

截取后分为前后两部分:00 0000 001101 0001 1000

第三步:前十位与 0xD800 相加,形成高位

0xD800 + 0x3 得出 0xd803,转换为二进制为:1101100000000011

第四步:后十位与 0xDC00 相加,形成低位

0xDC00 + 0x118 得出 0xdd18,转换为二进制为:1101110100011000

第五步:合并结果(在不考虑字节序的情况下)

最终使用 UTF-16 编码过后显示为 1101 1000 0000 0011 1101 1101 0001 1000

UTF-16无法兼容ASCII编码

7:字节序

在这里插入图片描述

使用 UTF-16 过程中,理解字节序很重要。

字节序分为 大端表示(big endian)小端表示(little endian)

UTF-16 每单位使用 16 位长的码元,而 8 位长的码元代表一字节,它包含了两个字节,这两个字节该从前后哪个位置优先读取就显得很重要。

比如:从上一小节,我们知道 的 UTF-16 编码后的值为 0000 0101 1011 1011 0110,转换为十六进制就是 0x5bb6

按照大端存储, 5b 将存储在内存地址1000 , b6 将存储在内存地址1001。

反过来,小端存储,b6 将存储在内存地址1000,5b 将存储在内存地址1001,最终显示为 0xb65b

通俗来讲,大端存储更符合人的阅读习惯,从左往右。

为了判断文件是以 大端存储的还是小端存储的,会在文件前面增加一个 U+FEFF 空白字符(不是空格),如果是返回的是 FE FF 那就代表是以 大端(BE)编译,如果返回的是 FF FE,那就代表是 小端(LE)编译。

同样,以 上节的 'ഘ'字符为例,它的 UTF-16 编码格式值为 1101 1000 0000 0011 1101 1101 0001 1000,转换为 16 进制后为 0xd803 0xdd18

  • 大端表示:d8 03 dd 18
  • 小端表示:03 d8 18 dd

8:UTF-32

在这里插入图片描述

UTF-32 运行很快,但占用的空间也很大,所以不经常使用。

它可以直接使用下标对应 unicode 的码值,所以不需要转换,另外,它的前导第一位需要为 0,所以只有 31 个码位用来存储。

9:回到原来的

在这里插入图片描述

回到最初,我们遇到的 😂 emoji 字符。

还记得 UTF-16 那一小节吗?它之所以显示 length = 2

是因为该 😂 字符unicode 值为 128514 ,它超出了 U+FFFF

所以,需要使用两个 UTF-16 单位(4个字节)去表示,并且 length 是以 UTF-16 代码单元基础来计算的,所以会显示长度为2。

如果想要以最简单的方式遍历包含 emoji 的字符串,可以使用for of 来解决,如:

let a = '😂123';
for(i of a){console.log(i)}

拓展:你也可以尝试使用学到的 UTF-16 编码方式去根据对应的unicode码值进行遍历。

10:最后

如果觉得内容难以理解,可以给我留言。如果您不忙,我会尽力解答您的问题,为您解答。如果内容有误,谢谢您为我指出,谢谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一拖再拖 一拖再拖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值