字符集和字符编码
字符集就是字符的集合,如常见的 ascii字符集,gb2312字符集,unicode字符集等。这些不同字符集之间最大的区别是所包含的字符数量的不同。
字符编码则代表字符集的实际编码规则,是用于计算机解析字符的,如 gb2312,gbk,utf-8 等。字符编码的本质就是如何使用二进制字节来表示字符的问题。
字符集和编码是一对多的关系,同一字符集可能有多种字符编码,如unicode字符集就有 utf-8,utf-16 等。
在前端开发中,javascript程序是使用unicode字符集,javascript源码文本通常是基于utf-8编码。
但js代码中的字符串类型是utf-16编码的,这也是为什么会碰到api接口返回字符串在前端出现乱码,因为多数服务都使用utf-8编码,前后编码方式不一致。
说起字符集的发展历程,可以总结为一句话:几乎都是对ascii字符集的扩展。
ascii
我们知道,计算机是使用二进制来处理信息的。
其中,每一个二进制位(bit)有 0和1 两种状态。一个字节(byte)则有8个二进制位,可以有256种状态。
而ascii就是基于拉丁字母、主要用于显示英文的一种单字节字符集,它的编码和字符是一一对应的,因为它就是使用一个字节8个二进制位来表示,不会超过256个字符。
标准的ascii字符总计有128个字符(2^7),其中前面32个控制字符,后面96个是可打印字符,包括常用的大小写字母数字标点符号等。因为只占用了一个字节的后7位,那字节的最高位一般设置为0。
'a'.charcodeat() // 97'a'.charcodeat() // 65'9'.charcodeat() // 57'.'.charcodeat() // 46 如上,每个字符会对应一个编码(使用数字标识),总共会从0-128。完整的ascii码表,网上很容易找到。 通过ascii码表,我们发现,小写字母并没有和大写字母挨着排序?这是为了方便大小写之间的转换, a 排在 65(64 + 1) 位,而 a 排在 97(64 + 32 + 1) 位。65 ^ 32 = 97// a ^ 32 = a
字符集的发展历史
ascii是几乎所有字符集的基础。
标准的ascii码最多只能标识128个字符,欧美国家可以很好的使用,但其他国家的字符变多,自然就不够用了。
这个时候,最高位就开始被惦记上,通过扩展ascii码的最高位,又能满足用于特殊符号的一些国家的需求,这种就是扩展ascii码。
但是亚非拉更多非拉丁语系的国家,字符成千上万,只能使用新的方式。
如中文,就又进行了扩展,小于127的字符的意义与标准ascii码相同,当需要标识汉字时,使用2个字节,每个字节都大于127。 这种多字节字符集即gb2312,后续因为不断的扩展,如繁体字和各种符号,甚至少数民族的语言符号等等,又使用了包括gbk等不同字符集。
因此,很多国家都制定了自己的编码字符集,基本都是在ascii的基础上进行的。
各字符集虽然都能够兼容标准ascii码,但在使用交流上的不便是显而易见的,乱码也是随处可见。 为了解决这种各自为战的问题,unicode字符集就诞生了。
统一码
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。
一般汉字的码点区间为 u+2e80 - u+9fff。
字符平面
目前的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位的四字节码元。
另外,为什么总看到使用十六进制数据来表示如码点等各种数据呢?
因为,两位的十六进制正好等于一个字节8位,0xff = 0b11111111。
utf-8
utf-8是一种可变长度的字符编码方式。目前是使用 1 到 4 个字节来编码字符。
是互联网时代应用最广的一种编码方式,前端接触的相对最多。
需要注意的是:汉字一般占3个字节,表情符号一般占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。
我们使用 encodeuri 进行验证————encodeuri函数支持将中文进行 utf-8 编码:
encodeuri('好') // '%e5%a5%bd'
去除百分号,结果正好一致。
utf-16
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表示码点:低位——((cp - 65536) / 1024) + 0xd800,值范围是 0xd800~0xdbff;高位——((cp - 65536) % 1024) + 0xdc00,值范围是 0xdc00~0xdfff。
看下面的示例:
汉字 '好','好'.codepointat() // 22909,码点小于u+ffff,直接进行十六进制转换:579d。表情符号 '',''.codepointat() // 128516,码点需要拆分:低位:math.floor(((128516 - 65536) / 1024)) + 0xd800 // 55357, 得到 d83d高位:((128516 - 65536) % 1024) + 0xdc00 // 56836,得到 de04
使用 string.fromcharcode 方法进行验证:
string.fromcharcode(0xd83d, 0xde04) // ''
需要明确的一点,javascript中的字符串是基于utf-16编码的,大端序字节。
utf-32是定长的编码,每个码位使用四个字节进行编码。优点是和unicode一一对应,缺点是太浪费空间。
比较
下面将选取字母、汉字、表情字符,进行编码对比查看:
// utf-8'a': 97 - 0x61'好': 22909 - (0xe5 0xa5 0xbd)'':128516 - (0xf0 0x9f 0x98 0x84)// utf-16'a': 97 - 0x0061'好': 22909 - 0x597d'':128516 - (0xd83d, 0xde04)
可以看到,utf-8是变长1-4个字节,码元为8位;
utf-16是2或4字节,码元是16位。
这里记住utf-16的码元,对于我们理解下面的问题,比较有帮助。
前端开发中的编码
前面已提到过,javascript中的字符串是基于utf-16编码的,所以在计算字符串长度时,我们需要先理解utf-16编码。
下面看下处理字符串时可能会遇到的问题。
1 字符串长度计算
字符串的length属性,实际上是使用utf-16的码元个数来进行计算的:
ascii码和大部分中文,都是一个码元
而表情字符和其他特殊字符都是两个码元
所以当某个字符中存在2个码元时,就算显示的是一个字符,length却等于2。
'a'.length // 1'好'.length // 1,多数汉字都是基本字符平面,只有一个码元,长度就为1。''.length // 2
2 组合字符的长度
还有一种特殊的,组合字符,一般指一些带标点符号的字符:é。'é'.length // 2'eu0301'.length // 2// 获取码点时,忽略了标点符号,显示的是字母的码点'é'.codepointat() // 101'e'.codepointat() // 101
如要正常操作组合字符,使用normalize()。'é'.normalize().length = 1。
3 多码元字符操作
对于多码元字符使用下标取值时,得到的将是它的码元:''[0] // 'ud83d'''[1] // 'ude04''123'[0] // '1' 循环时,使用 for 会乱码,而 for-of 则正常:let smile = ''for(let i = 0; i < smile.length; i++) { console.log(smile[i]) }// �// �for (let tt of smile) { console.log(tt)}// 但,可以使用转换成扩展数组的方式访问:[...''][0] // ''array.from('') // [''] 还可以使用码点的方式:string.fromcodepoint(''.codepointat()) // ''
对于这种特殊字符,使用下面的字符串方法都会分割码元:
split(),slice(),charat(),charcodeat(),substr(),substring()。''.slice(0, 2) // ''''.slice(0, 1) // 'ud83d'''.slice(1, 2) // 'ude04'''.substr(0,1) // 'ud83d'''.substr(0,2) // ''''.split('') // ['ud83d', 'ude04']
4 正则中的 u 修饰符
es6在正则中添加了u修饰符,用来正确处理大于uffff的 unicode 字符。
也就是能够正确处理四个字节的 utf-16 编码。/^s$/.test('') // false/^s$/u.test('') // true 但对组合字符,u修饰符不起作用:/^s$/u.test('é') // false/^s$/u.test('eu0301') // false
5 转义字符
我们还需要注意的,是转义字符的计算,结果会以实际字符为准:'x3f'.length // 1'?'.length // 1 读取操作时,也能正常处理:'x3f'[0] // '?''x3f'.split('') // ['?']
6 常用api
前端在对unicode编码处理时,提供了一些可以使用的api,在实际工作中,会方便我们处理这方面的问题。 7 处理码点和字符
charat(index)
从一个字符串中返回指定的字符,对于多码元字符,仍会返回码元字符:
'a'.charat() // 'a'''.charat() // 'ud83d'''.charat(1) // 'ude04'
charcodeat(index)
返回0到65535之间的整数码点值。对于多码元如果字符的码点大于u+ffff,则返回第一个码元值,还可以加索引参数取后面码元的值。
codepointat(pos)
返回unicode码点,多码元也能返回完整的码点值。codepointat可以传入索引参数,对多码元字符取第二个码元值。
// 小于 u+ffff'好'.codepointat() // 22909'好'.charcodeat() // 22909// 大于 u+ffff''.charcodeat() // 55357''.charcodeat(1) // 56836''.codepointat() // 128516''.codepointat(1) // 56836
string.fromcharcode(num1[, ...[, numn]])
返回由指定的utf-16码点序列创建的字符串。参数范围0到65535,大于65535的数据将被截断,结果不准确。对于多码元字符,则会将两个码元组合得到该字符。
string.fromcodepoint(num1[, ...[, numn]])
返回使用指定的代码点序列创建的字符串。可以处理多码元字符的完整码点值。
string.fromcharcode(55357, 56836, 123) // '{'string.fromcodepoint(128516, 123, 8776) // '{≈'
8 textencoder
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) // '好'
ie不支持。 string.prototype.normalize()
对于语调符号和重音符号,unicode提供了两种方法,一种是直接提供带符号的字符,如 é (码点233);另一种是组合字符,如上文提到的 é (码点101)。
针对这种码点不同,但实质一样的字符,javascript识别不了:
'é' === 'é' // false
而 normalize() 方法的引入,正是为了解决这一问题。
它会按照一定的方式将字符的不同表示方法统一为标准形式:'é' === 'é'.normalize() // true
9 url的utf8编解码
另外,在前端常接触的网页中,url链接编码也是非常常见的。
诸如:'http%3a%2f%2fbaidu.com%2f%e4%b8%ad%e5%9b%bd'。
这里面涉及到的就是关于utf-8的编码。
而javascript提供了四个url的编码/解码方法,可以用于将非ascii码的字符,如中文字符、特殊字符、表情字符等,进行utf-8的编解码操作: encodeuri() 和 encodeuricomponent()
decodeuri() 和 decodeuricomponent()
他们的短处也很明显,对ascii字符如英文数字等字符无法处理。
这里的转换方式:先转为utf-8的字节码,然后前面加个 % 进行拼接得到编码结果。
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'
10 encodeuri和encodeuricomponent的区别
这两者的不同之处,在于对部分url元字符符号的处理上。
url元字符:分号(;),逗号(’,’),斜杠(/),问号(?),冒号(:),at(@),&,等号(=),加号(+),美元符号($),井号(#)。
encodeuricomponent会对这些url元字符进行编码,但是encodeuri则不会:
病理领域的AI研究也有了新的进展
维安达斯防爆产品广泛应用于智慧城市-综合管廊
电瓶修复技术—给现在的充电挑些毛病!
芯片制造商的战场正转向智能汽车
MSP430F5438 RTC操作实验详解
编程中用到的字符编码知识点
广和通携手产业链合作伙伴开拓更广阔的5G物联网市场
谷歌语音系统AI新科技:同真人声音无法区分
GAP8物联网应用处理器的主要使用案例
带有2个DS18B20和采样率控制的Arduino数据记录器的制作教程
多目标跟踪雷达的功能主要包括哪些
泰克TDS6804B示波器提升实验室功能
单相电机怎么接电容
电装开发出汽车图像传感器 体积减小50%
中国移动正在放任2G用户的流失
一招让贴标机的接线和编程更简单
NI与摩尔精英签署合作备忘录
网络分析仪中如何理解矢网的框图
RF MEMS开关的运作、优势
贸泽即日备货TE Connectivity LUMAWISE Endurance S连接器系统