JavaScript開(kāi)發(fā)中需要搞懂的字符編碼總結(jié)
字符集和字符編碼
字符集就是字符的集合,如常見(jiàn)的 ASCII字符集,GB2312字符集,Unicode字符集等。這些不同字符集之間最大的區(qū)別是所包含的字符數(shù)量的不同。
字符編碼則代表字符集的實(shí)際編碼規(guī)則,是用于計(jì)算機(jī)解析字符的,如 GB2312,GBK,UTF-8 等。字符編碼的本質(zhì)就是如何使用二進(jìn)制字節(jié)來(lái)表示字符的問(wèn)題。
字符集和編碼是一對(duì)多的關(guān)系,同一字符集可能有多種字符編碼,如Unicode字符集就有 UTF-8,UTF-16 等。
在前端開(kāi)發(fā)中,Javascript程序是使用Unicode字符集,Javascript源碼文本通常是基于UTF-8編碼。
但JS代碼中的字符串類型是UTF-16編碼的,這也是為什么會(huì)碰到api接口返回字符串在前端出現(xiàn)亂碼,因?yàn)槎鄶?shù)后臺(tái)服務(wù)都使用utf-8編碼,前后編碼方式不一致。
說(shuō)起字符集的發(fā)展歷程,可以總結(jié)為一句話:幾乎都是對(duì)ASCII字符集的擴(kuò)展。
ASCII
我們知道,計(jì)算機(jī)是使用二進(jìn)制來(lái)處理信息的。
其中,每一個(gè)二進(jìn)制位(bit)有 0和1 兩種狀態(tài)。一個(gè)字節(jié)(byte)則有8個(gè)二進(jìn)制位,可以有256種狀態(tài)。
而ASCII就是基于拉丁字母、主要用于顯示英文的一種單字節(jié)字符集,它的編碼和字符是一一對(duì)應(yīng)的,因?yàn)樗褪鞘褂靡粋€(gè)字節(jié)8個(gè)二進(jìn)制位來(lái)表示,不會(huì)超過(guò)256個(gè)字符。
標(biāo)準(zhǔn)的ASCII字符總計(jì)有128個(gè)字符(2^7),其中前面32個(gè)控制字符,后面96個(gè)是可打印字符,包括常用的大小寫字母數(shù)字標(biāo)點(diǎn)符號(hào)等。因?yàn)橹徽加昧艘粋€(gè)字節(jié)的后7位,那字節(jié)的最高位一般設(shè)置為0。
'a'.charCodeAt() // 97 'A'.charCodeAt() // 65 '9'.charCodeAt() // 57 '.'.charCodeAt() // 46
如上,每個(gè)字符會(huì)對(duì)應(yīng)一個(gè)編碼(使用數(shù)字標(biāo)識(shí)),總共會(huì)從0-128。完整的ASCII碼表,網(wǎng)上很容易找到。
通過(guò)ASCII碼表,我們發(fā)現(xiàn),小寫字母并沒(méi)有和大寫字母挨著排序?
這是為了方便大小寫之間的轉(zhuǎn)換, A 排在 65(64 + 1) 位,而 a 排在 97(64 + 32 + 1) 位。
65 ^ 32 = 97 // A ^ 32 = a
字符集的發(fā)展歷史
ASCII是幾乎所有字符集的基礎(chǔ)。
標(biāo)準(zhǔn)的ASCII碼最多只能標(biāo)識(shí)128個(gè)字符,歐美國(guó)家可以很好的使用,但其他國(guó)家的字符變多,自然就不夠用了。
這個(gè)時(shí)候,最高位就開(kāi)始被惦記上,通過(guò)擴(kuò)展ASCII碼的最高位,又能滿足用于特殊符號(hào)的一些國(guó)家的需求,這種就是擴(kuò)展ASCII碼。
但是亞非拉更多非拉丁語(yǔ)系的國(guó)家,字符成千上萬(wàn),只能使用新的方式。
如中文,就又進(jìn)行了擴(kuò)展,小于127的字符的意義與標(biāo)準(zhǔn)ASCII碼相同,當(dāng)需要標(biāo)識(shí)漢字時(shí),使用2個(gè)字節(jié),每個(gè)字節(jié)都大于127。這種多字節(jié)字符集即GB2312,后續(xù)因?yàn)椴粩嗟臄U(kuò)展,如繁體字和各種符號(hào),甚至少數(shù)民族的語(yǔ)言符號(hào)等等,又使用了包括GBK等不同字符集。
因此,很多國(guó)家都制定了自己的編碼字符集,基本都是在ASCII的基礎(chǔ)上進(jìn)行的。
各字符集雖然都能夠兼容標(biāo)準(zhǔn)ASCII碼,但在使用交流上的不便是顯而易見(jiàn)的,亂碼也是隨處可見(jiàn)。為了解決這種各自為戰(zhàn)的問(wèn)題,Unicode字符集就誕生了。
Unicode
Unicode
是國(guó)際組織制定的,用于收納世界上所有文字和符號(hào)的字符集方案。
前128個(gè)字符同ASCII一樣,進(jìn)行擴(kuò)充后,使用數(shù)字0-0x10FFFF來(lái)映射這些字符,最多可以有1114112個(gè)字符。目前仍然只使用了其中的一小部分。
Unicode一般使用兩個(gè)字節(jié)來(lái)表示一個(gè)字符。
碼點(diǎn)
- Unicode 規(guī)定了每個(gè)字符的數(shù)字編號(hào),這個(gè)編號(hào)被稱為
碼點(diǎn)(code point)
。 - 碼點(diǎn)以 U+hex 的形式表示,U+是代表Unicode的前綴,而 hex 是一個(gè)16進(jìn)制數(shù)。取值范圍是從 U+0000 到 U+10FFFF。
- 每個(gè)碼點(diǎn)對(duì)應(yīng)一個(gè)字符,絕大部分的常見(jiàn)字符在最前面的 65536 個(gè)字符,范圍是 U+0000到U+FFFF。
- 一般漢字的碼點(diǎn)區(qū)間為 U+2E80 - U+9FFF。
字符平面
- 目前的Unicode分成了17個(gè)編組,也稱平面,每個(gè)平面有65536個(gè)碼點(diǎn)。
- 第一個(gè)平面是基本多語(yǔ)言平面,范圍:U+0000 - U+FFFF,多數(shù)常見(jiàn)字符都在該區(qū)間。
- 其他平面則為輔助平面,范圍:U+10000 到 U+10FFFF,如我們?cè)诰W(wǎng)上常見(jiàn) Emoji 表情。
碼元
- 碼元(Code Unit)可以理解為對(duì)碼點(diǎn)進(jìn)行編碼時(shí)的最小基本單元,碼元是一個(gè)整體。而字符編碼的作用就是將Unicode碼點(diǎn)轉(zhuǎn)換成碼元序列。
- Unicode常用的編碼方式有 UTF-8 、UTF-16 和 UTF-32,UTF是Unicode TransferFormat的縮寫。
- UTF-8是8位的單字節(jié)碼元,UTF-16是16位的雙字節(jié)碼元,UTF-32是32位的四字節(jié)碼元。
編碼方式 | 碼元 | 編碼后字節(jié)數(shù) |
---|---|---|
UTF-8 | 8位 | 1-4字節(jié) |
UTF-16 | 16位 | 2字節(jié)或者4字節(jié) |
UTF-32 | 32位 | 4字節(jié) |
另外,為什么總看到使用十六進(jìn)制數(shù)據(jù)來(lái)表示如碼點(diǎn)等各種數(shù)據(jù)呢?
因?yàn)椋瑑晌坏氖M(jìn)制正好等于一個(gè)字節(jié)8位,0xff = 0b11111111。
UTF-8
UTF-8是一種可變長(zhǎng)度的字符編碼方式。目前是使用 1 到 4 個(gè)字節(jié)來(lái)編碼字符。
是互聯(lián)網(wǎng)時(shí)代應(yīng)用最廣的一種編碼方式,前端接觸的相對(duì)最多。
需要注意的是:漢字一般占3個(gè)字節(jié),表情符號(hào)一般占4個(gè)字節(jié)。
UTF-8的編碼規(guī)則:
- 1個(gè)字節(jié)的字符,第一位為0,后7位為碼點(diǎn),與ASCII相同。
- n個(gè)字節(jié)的字符,第一個(gè)字節(jié)前面
n
位都是1,n+1位是0,可據(jù)此判斷有幾個(gè)字節(jié)。后面的幾個(gè)字節(jié)都是10
為開(kāi)頭2位。
這里規(guī)定的都是前綴,對(duì)于字符的碼點(diǎn),需要進(jìn)行截取后依次放入除前綴外的其他位,所以UTF-8又被稱為前綴碼。
格式如下表:
字節(jié)數(shù) | 碼點(diǎn)位數(shù) | 碼點(diǎn)范圍 | 編碼方式 |
---|---|---|---|
1 | 7 | U+0000~U+007F | 0××××××× |
2 | 11 | U+0080~U+07FF | 110××××× 10×××××× |
3 | 16 | U+0800~U+FFFF | 1110×××× 10×××××× 10×××××× |
4 | 21 | U+10000~U+10FFFF | 11110××× 10×××××× 10×××××× 10×××××× |
通過(guò)上表的編碼規(guī)則,我們就可以進(jìn)行各種轉(zhuǎn)換了。
下面我們以一個(gè)中文字符的編碼轉(zhuǎn)換為例,如漢字 '好':
'好'的Unicode碼點(diǎn):'好'.codePointAt() \\ 22909
,結(jié)果是22909
22909在UTF-8的3字節(jié)數(shù)的編碼區(qū)間 U+0800 (2048) ~ U+FFFF (65535)
22909的二進(jìn)制值:101100101111101,有15位
而3字節(jié)數(shù)的編碼需要16位,前面補(bǔ)0,根據(jù)表中規(guī)則分成3組:0101 100101 111101
依次填入對(duì)應(yīng)的前綴:11100101 10100101 10111101,得到3個(gè)字節(jié)
將得到的三個(gè)字節(jié)轉(zhuǎn)成十六進(jìn)制數(shù)據(jù):E5 A5 BD,所以漢字 '好' 的UTF-8就是:E5 A5 BD
我們使用 encodeURI
進(jìn)行驗(yàn)證————encodeURI函數(shù)支持將中文進(jìn)行 UTF-8 編碼:
encodeURI('好') // '%E5%A5%BD'
去除百分號(hào),結(jié)果正好一致。
UTF-16
UTF-16的編碼方式:基本平面的字符占用 2 個(gè)字節(jié)(U+0000到U+FFFF),輔助平面的字符占用 4 個(gè)字節(jié)(U+010000到U+10FFFF)。
也就是說(shuō),UTF-16的編碼長(zhǎng)度要么是2個(gè)字節(jié)要么是4個(gè)字節(jié)。當(dāng)為2字節(jié)時(shí),則實(shí)際上是與Unicode相同。
并且還有個(gè)原則,在Unicode基本多語(yǔ)言平面內(nèi),從U+D800到U+DFFF之間的碼點(diǎn)區(qū)間是不對(duì)應(yīng)字符的。而UTF-16需要利用這塊碼位來(lái)對(duì)輔助平面的字符進(jìn)行編碼。
它的具體規(guī)則:
碼點(diǎn)小于U+FFFF,基本字符,不需處理,直接使用,占兩個(gè)字節(jié)。
否則,拆分成兩個(gè)碼元,四個(gè)字節(jié),cp表示碼點(diǎn):
低位——((cp - 65536) / 1024) + 0xD800,值范圍是 0xD800~0xDBFF;
高位——((cp - 65536) % 1024) + 0xDC00,值范圍是 0xDC00~0xDFFF。
看下面的示例:
1.漢字 '好','好'.codePointAt() // 22909
,碼點(diǎn)小于U+FFFF,直接進(jìn)行十六進(jìn)制轉(zhuǎn)換:579D。
2.表情符號(hào) '??','??'.codePointAt() // 128516
,碼點(diǎn)大于U+FFFF,需要拆分:
- 低位:
Math.floor(((128516 - 65536) / 1024)) + 0xD800 // 55357
, 得到 D83D - 高位:
((128516 - 65536) % 1024) + 0xDC00 // 56836
,得到 DE04
使用 String.fromCharCode
方法進(jìn)行驗(yàn)證:
String.fromCharCode(0xD83D, 0xDE04) // '??'
需要明確的一點(diǎn),Javascript中的字符串是基于UTF-16編碼的,大端序字節(jié)。
UTF-32是定長(zhǎng)的編碼,每個(gè)碼位使用四個(gè)字節(jié)進(jìn)行編碼。優(yōu)點(diǎn)是和unicode一一對(duì)應(yīng),缺點(diǎn)是太浪費(fèi)空間。
比較
下面將選取字母、漢字、表情字符,進(jìn)行編碼對(duì)比查看:
// 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是變長(zhǎng)1-4個(gè)字節(jié),碼元為8位;UTF-16是2或4字節(jié),碼元是16位。
這里記住UTF-16的碼元,對(duì)于我們理解下面的問(wèn)題,比較有幫助。
前端開(kāi)發(fā)中的編碼
前面已提到過(guò),javascript中的字符串是基于UTF-16編碼的,所以在計(jì)算字符串長(zhǎng)度時(shí),我們需要先理解UTF-16編碼。
下面看下處理字符串時(shí)可能會(huì)遇到的問(wèn)題。
字符串長(zhǎng)度計(jì)算
字符串的length屬性,實(shí)際上是使用UTF-16的碼元個(gè)數(shù)來(lái)進(jìn)行計(jì)算的:
- ASCII碼和大部分中文,都是一個(gè)碼元
- 而表情字符和其他特殊字符都是兩個(gè)碼元
所以當(dāng)某個(gè)字符中存在2個(gè)碼元時(shí),就算顯示的是一個(gè)字符,length卻等于2。
'a'.length // 1 '好'.length // 1,多數(shù)漢字都是基本字符平面,只有一個(gè)碼元,長(zhǎng)度就為1。 '??'.length // 2
組合字符的長(zhǎng)度
還有一種特殊的,組合字符,一般指一些帶標(biāo)點(diǎn)符號(hào)的字符:e?。
'e?'.length // 2 'e\u0301'.length // 2 // 獲取碼點(diǎn)時(shí),忽略了標(biāo)點(diǎn)符號(hào),顯示的是字母的碼點(diǎn) 'e?'.codePointAt() // 101 'e'.codePointAt() // 101
如要正常操作組合字符,使用normalize()。
'e?'.normalize().length = 1。
多碼元字符操作
對(duì)于多碼元字符使用下標(biāo)取值時(shí),得到的將是它的碼元:
'??'[0] // '\uD83D' '123'[0] // '1'
循環(huán)時(shí),使用 for 會(huì)亂碼,而 for-of 則正常:
let smile = '??' for(let i = 0; i < smile.length; i++) { console.log(smile[i]) } // ? // ? for (let tt of smile) { console.log(tt) } // ??
但,可以使用轉(zhuǎn)換成擴(kuò)展數(shù)組的方式訪問(wèn):
[...'??'][0] // '??' Array.from('??') // ['??']
還可以使用碼點(diǎn)的方式:
String.fromCodePoint('??'.codePointAt()) // '??'
對(duì)于這種特殊字符,使用下面的字符串方法都會(huì)分割碼元:
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']
正則中的 u 修飾符
ES6在正則中添加了u修飾符,用來(lái)正確處理大于\uFFFF的 Unicode 字符。
也就是能夠正確處理四個(gè)字節(jié)的 UTF-16 編碼。
/^\S$/.test('??') // false /^\S$/u.test('??') // true
但對(duì)組合字符,u修飾符不起作用:
/^\S$/u.test('e?') // false /^\S$/u.test('e\u0301') // false
轉(zhuǎn)義字符
我們還需要注意的,是轉(zhuǎn)義字符的計(jì)算,結(jié)果會(huì)以實(shí)際字符為準(zhǔn):
'\x3f'.length // 1 '?'.length // 1
讀取操作時(shí),也能正常處理:
'\x3f'[0] // '?' '\x3f'.split('') // ['?']
常用API
前端在對(duì)Unicode編碼處理時(shí),提供了一些可以使用的API,在實(shí)際工作中,會(huì)方便我們處理這方面的問(wèn)題。
處理碼點(diǎn)和字符
charAt(index):從一個(gè)字符串中返回指定的字符,對(duì)于多碼元字符,卻會(huì)返回碼元字符:
'a'.charAt() // 'a' '??'.charAt() // '\uD83D' '??'.charAt(1) // '\uDE04'
charCodeAt(index):返回0到65535之間的整數(shù)碼點(diǎn)值。對(duì)于多碼元字符如果碼點(diǎn)大于U+FFFF,則返回第一個(gè)碼元值,還可以加索引參數(shù)取后面碼元的值。
codePointAt(pos):返回Unicode碼點(diǎn),多碼元也能返回完整的碼點(diǎn)值。codePointAt可以傳入索引參數(shù),對(duì)多碼元字符取第二個(gè)碼元值。
// 小于 U+FFFF '好'.codePointAt() // 22909 '好'.charCodeAt() // 22909 // 大于 U+FFFF '??'.charCodeAt() // 55357 '??'.charCodeAt(1) // 56836 '??'.codePointAt() // 128516 '??'.codePointAt(1) // 56836
String.fromCharCode(num1[, ...[, numN]]):返回由指定的UTF-16碼點(diǎn)序列創(chuàng)建的字符串。參數(shù)范圍0到65535,大于65535的數(shù)據(jù)將被截?cái)?,結(jié)果不準(zhǔn)確。對(duì)于多碼元字符,則會(huì)將兩個(gè)碼元組合得到該字符。
String.fromCodePoint(num1[, ...[, numN]]):返回使用指定的代碼點(diǎn)序列創(chuàng)建的字符串。可以處理多碼元字符的完整碼點(diǎn)值。
String.fromCharCode(55357, 56836, 123) // '??{' String.fromCodePoint(128516, 123, 8776) // '??{≈'
TextEncoder
TextEncoder,使用 UTF-8 編碼將代碼點(diǎn)流轉(zhuǎn)換成字節(jié)流。
TextDecoder:解碼。
默認(rèn)編碼方式就是UTF-8,可以解決字符轉(zhuǎn)UTF-8編碼的問(wèn)題。
const txtEn = new TextEncoder() const enVal = txtEn.encode('好') // Uint8Array(3) [229, 165, 189] const txtDe = new TextDecoder() txtDe.decode(enVal) // '好'
IE不支持。
String.prototype.normalize()
對(duì)于語(yǔ)調(diào)符號(hào)和重音符號(hào),Unicode提供了兩種方法,一種是直接提供帶符號(hào)的字符,如 é
(碼點(diǎn)233);另一種是組合字符,如上文提到的 e?
(碼點(diǎn)101)。
針對(duì)這種碼點(diǎn)不同,但實(shí)質(zhì)一樣的字符,Javascript識(shí)別不了:
'é' === 'e?' // false
而 normalize() 方法的引入,正是為了解決這一問(wèn)題,它會(huì)按照一定的方式將字符的不同表示方法統(tǒng)一為標(biāo)準(zhǔn)形式:
'é' === 'e?'.normalize() // true
URL的UTF8編解碼
另外,在前端常接觸的網(wǎng)頁(yè)中,URL鏈接編碼也是非常常見(jiàn)的。
諸如:'http://baidu.com%2F%E4%B8%AD%E5%9B%BD'
。這里面涉及到的就是關(guān)于UTF-8的編碼。
JavaScript提供了四個(gè)URL的編碼/解碼方法,可以用于將非ASCII碼的字符,如中文字符、特殊字符、表情字符等,進(jìn)行UTF-8的編解碼操作:
- encodeURI() 和 decodeURI()
- encodeURIComponent() 和 decodeURIComponent()
這里的轉(zhuǎn)換方式:先轉(zhuǎn)為UTF-8的字節(jié)碼,然后在每個(gè)字節(jié)碼前面加個(gè) %
符號(hào)進(jìn)行拼接得到編碼結(jié)果。 它們的短處也很明顯,對(duì)ASCII字符如英文數(shù)字等字符則無(wú)法處理。
encodeURI('好') // '%E5%A5%BD' decodeURI('%E5%A5%BD') // '好' encodeURIComponent('好') // '%E5%A5%BD' decodeURIComponent('%E5%A5%BD') // '好' encodeURIComponent('??') // '%F0%9F%98%84' decodeURIComponent('%F0%9F%98%84') // '??' encodeURI('hello') // 'hello' encodeURIComponent('hello') // 'hello'
encodeURI和encodeURIComponent的區(qū)別
這兩者的不同之處,在于對(duì)部分URL元字符符號(hào)的處理上。
URL元字符:分號(hào)(;),逗號(hào)(’,’),斜杠(/),問(wèn)號(hào)(?),冒號(hào)(:),at(@),&,等號(hào)(=),加號(hào)(+),美元符號(hào)($),井號(hào)(#)。
encodeURIComponent會(huì)對(duì)這些URL元字符進(jìn)行編碼,但是encodeURI則不會(huì):
encodeURIComponent(';,/@&=') // '%3B%2C%2F%40%26%3D' encodeURI(';,/@&=') // ';,/@&='
以上就是JavaScript開(kāi)發(fā)中需要搞懂的字符編碼總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于JavaScript字符編碼的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
頁(yè)面點(diǎn)擊小紅心js實(shí)現(xiàn)代碼
有時(shí)候我們經(jīng)常看到有些blog出現(xiàn)一些點(diǎn)擊頁(yè)面出現(xiàn)小紅心的效果,很是喜歡,這里就為大家分享一下代碼直接引用即可2018-05-05關(guān)于js中removeEventListener取消事件監(jiān)聽(tīng)的坑
許多入前端不久的人都會(huì)遇到removeEventListener無(wú)法清除監(jiān)聽(tīng)的情況,下面這篇文章主要給大家介紹了關(guān)于js中removeEventListener取消事件監(jiān)聽(tīng)的坑,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09uniapp基礎(chǔ)知識(shí)點(diǎn)掌握以及面試題整理
uni-app是一個(gè)使用vue.js開(kāi)發(fā)所有前端應(yīng)用的框架,開(kāi)發(fā)者編寫一套代碼,下面這篇文章主要給大家介紹了關(guān)于uniapp基礎(chǔ)知識(shí)點(diǎn)掌握以及面試題整理的相關(guān)資料,需要的朋友可以參考下2023-02-02JS如何將秒數(shù)轉(zhuǎn)化為時(shí)分秒的形式
在實(shí)際工作中經(jīng)常會(huì)遇見(jiàn)把秒數(shù)轉(zhuǎn)化為時(shí)分秒的問(wèn)題,如何處理呢?下面這篇文章主要給大家介紹了關(guān)于JS如何將秒數(shù)轉(zhuǎn)化為時(shí)分秒形式的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-12-12js時(shí)間戳和c#時(shí)間戳互轉(zhuǎn)方法(推薦)
下面小編就為大家?guī)?lái)一篇js時(shí)間戳和c#時(shí)間戳互轉(zhuǎn)方法(推薦)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02跟我學(xué)習(xí)javascript的函數(shù)調(diào)用和構(gòu)造函數(shù)調(diào)用
跟我學(xué)習(xí)javascript的函數(shù)和構(gòu)造函數(shù)調(diào)用,主要包括三方面內(nèi)容函數(shù)調(diào)用、方法調(diào)用以及構(gòu)造函數(shù)調(diào)用,想要了解這些內(nèi)容的朋友千萬(wàn)不要錯(cuò)過(guò)下面的內(nèi)容。2015-11-11