ES6走走看看—字元到底發生了什麼變化

是方旭啊發表於2018-09-07

持續更新的github筆記,連結地址:Front-End-Basics

此篇文章的筆記地址:字元到底發生了什麼變化

ES6走走看看系列,特別鳴謝奇舞讀書會~


看正文之前,先思考一下,為什麼你看的ES6各種權威指南里提到的?會有那麼多問題,它length是2,charAt出來是亂碼……

1、 JavaScript字元編碼的“坑”和“填坑”

計算機內部處理的資訊,都是一個些二進位制值,每一個二進位制位(bit)有0和1兩種狀態。 一個位元組(byte)有八個二進位制位,也就是說,一個位元組一共可以用來表示256種不同的狀態,每一個狀態對應一個符號,就是256個符號,從0000000011111111。轉換成十六進位制,一個位元組就是0x00OxFF

1.1 先來聊聊字元編碼的歷程

先祭出一張圖,建議放大看

ES6走走看看—字元到底發生了什麼變化

(1) ASCII 碼

上個世紀60年代,美國製定了一套字元編碼,對英語字元與二進位制位之間的關係,做了統一規定。這被稱為 ASCII 碼(美國資訊交換標準程式碼),一直沿用至今。

ASCII 碼一共規定了128個字元的編碼,只佔用了一個位元組的後面7位,最前面的一位統一規定為0。

第一部分:0~31(0x00~0x1F)及127(共33個)是控制字元或通訊專用字元,有些可以顯示在螢幕上,有些則不能顯示,但能看到其效果(如換行、退格)如下表:

ES6走走看看—字元到底發生了什麼變化

第二部分:是由20~7E共95個,這95個字元是用來表示阿拉伯數字、英文字母大小寫和下劃線、括號等符號,都可以顯示在螢幕上如下表:

ES6走走看看—字元到底發生了什麼變化

(2) 非ASCII 編碼

英語用128個符號編碼就夠了,但是世界上可不只有英語這一種語言,先不說漢語,就是那些不說英語的歐洲國家,128個符號是不夠的。

一些歐洲國家就決定,利用位元組中閒置的最高位編入新的符號,這些歐洲國家使用的編碼體系,可以表示最多256個符號。大家你加你的,我加我的。因此,哪怕它們都使用256個符號的編碼方式,代表的字母卻不一樣。

1981年IBM PC ROM256個字元的字符集,即IBM擴充套件字符集,這128個擴充字元是由IBM制定的,並非標準的ASCII碼.這些字元是用來表示框線、音標和其它歐洲非英語系的字母。如下圖:

ES6走走看看—字元到底發生了什麼變化

在Windows 1.0(1985年11月發行)中,Microsoft沒有完全放棄IBM擴充套件字符集,但它已退居第二重要位置。因為遵循了ANSI草案和ISO標準,純Windows字符集被稱作「ANSI字符集」。

ES6走走看看—字元到底發生了什麼變化

由此可見擴充套件ASCII不再是國際標準。

而對於亞洲國家的文字,使用的符號就更多了,漢字就多達10萬左右(《中華辭海》共收漢字87019個,日本《今昔文字鏡》收錄漢字超15萬)。一個位元組只能表示256種符號,肯定是不夠的,就必須使用多個位元組表達一個符號。比如,簡體中文常見的編碼方式是 GB2312(中華人民共和國國家標準簡體中文字符集),使用兩個位元組表示一個漢字,所以理論上最多可以表示 256 x 256 = 65536 個符號。其實GB 2312標準共收錄6763個漢字,它所收錄的漢字已經覆蓋中國大陸99.75%的使用頻率。

(3) Unicode

之前的編碼,大家在自己的國家使用都挺好的。世界上存在著多種編碼方式,同一個二進位制數字可以被解釋成不同的符號,所以一旦不同國家進行資料傳輸,結果就只有亂碼了。

如果有一種編碼,將世界上所有的符號都納入其中。每一個符號都給予一個獨一無二的編碼,那麼亂碼問題就會消失。這就是 Unicode,就像它的名字所表示的,這是一種所有符號的編碼。

Unicode,定義很簡單,用一個碼點(code point)對映一個字元。碼點值的範圍是從U+0000到U+10FFFF,可以表示超過110萬個符號。

Unicode 最新版本的是 11.0,總共137,374個字元,這麼看來,還是挺夠用的。

Unicode最前面的65536個字元位,稱為基本平面(BMP-—Basic Multilingual Plane),它的碼點範圍是從U+0000到U+FFFF。最常見的字元都放在這個平面,這是Unicode最先定義和公佈的一個平面。 剩下的字元都放在補充平面(Supplementary Plane),碼點範圍從U+010000一直到U+10FFFF,共16個。

需要注意的是,Unicode 只是一個符號集,它只規定了符號的二進位制程式碼,卻沒有規定這個二進位制程式碼應該如何儲存。

// 例如下面的字元對應的碼點
A的碼點 U+0041
a的碼點 U+0061
©的碼點 U+00A9
☃的碼點 U+2603
?的碼點 U+1F4A9
複製程式碼

正是因為上面說的,沒有規定怎麼儲存,所以出現了Unicode 的多種儲存方式,不同的實現導致了Unicode 在很長一段時間內無法推廣,而且本來英文字母只用一個位元組儲存就夠了,如果 Unicode 統一規定,每個符號用三個或四個位元組表示,那麼每個英文字母前都必然有二到三個位元組是0,這對於儲存來說是極大的浪費,文字檔案的大小會因此大出二三倍,這是無法接受的。

在這個時候往往需要一個強大的外力推動,大家訴諸於利益,共同實現一個目標。所以,真正意義上的網際網路普及了,地球變成了村子,交流越來越多,亂碼是怎麼能行。

(4) UTF-8、UTF-16、UTF-32

UTF(Unicode transformation format)Unicode轉換格式,是服務於Unicode的,用於將一個Unicode碼點轉換為特定的位元組序列。 上面三種都是 Unicode 的實現方式之一。 UTF-16(字元用兩個位元組或四個位元組表示)和 UTF-32(字元用四個位元組表示),不過UTF-8 是在網際網路上使用最廣的一種 Unicode 的實現方式。

UTF-8

1992年開始設計,1993年首次被正式介紹,1996年UTF-8標準還沒有正式落實前,微軟的CAB(MS Cabinet)規格就明確容許在任何地方使用UTF-8編碼系統。但有關的編碼器實際上從來沒有實現這方面的規格。2003年11月UTF-8被RFC 3629重新規範,只能使用原來Unicode定義的區域,U+0000到U+10FFFF,也就是說最多四個位元組(之前可以使用一至六個位元組為每個字元編碼)

UTF-8 是一種變長的編碼方式。它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度。越是常用的字元,位元組越短,最前面的128個字元,只使用1個位元組表示,與ASCII碼完全相同(也就是所說的相容ASCII碼)。在英文下這樣就比UTF-16 和 UTF-32節省空間。

UTF-8 的編碼規則很簡單,只有二條:

1)對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的 Unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。

2)對於n位元組的符號(n > 1),第一個位元組的前n位都設為1,第n + 1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位制位,全部為這個符號的 Unicode 碼。

UTF-16

基本平面的字元佔用2個位元組,輔助平面的字元佔用4個位元組。也就是說,UTF-16的編碼長度要麼是2個位元組(U+0000到U+FFFF),要麼是4個位元組(U+010000到U+10FFFF)。

這裡涉及到一個怎麼判斷兩個位元組是一個字元,還是兩個位元組加兩個位元組組成的四個位元組是一個字元?

解決方法是:在基本平面內,從U+D800到U+DFFF是一個空段,即這些碼點不對應任何字元。因此,這個空段可以用來對映輔助平面的字元。

具體來說,輔助平面的字元位共有220個,也就是說,對應這些字元至少需要20個二進位制位。UTF-16將這20位拆成兩半,前10位對映在U+D800到U+DBFF(空間大小210),稱為高位(H),後10位對映在U+DC00到U+DFFF(空間大小210),稱為低位(L)。這意味著,一個輔助平面的字元,被拆成兩個基本平面的字元表示(代理對的概念)。

所以,當我們遇到兩個位元組,發現它的碼點在U+D800到U+DBFF之間,就可以斷定,緊跟在後面的兩個位元組的碼點,應該在U+DC00到U+DFFF之間,這四個位元組必須放在一起解讀。

UTF-16編碼介於UTF-32與UTF-8之間,同時結合了定長和變長兩種編碼方法的特點。

UTF-32

UTF-32 最直觀的編碼方法,每個碼點使用四個位元組表示,位元組內容一一對應碼點。

UTF-32的優點在於,轉換規則簡單直觀,查詢效率高。缺點在於浪費空間,同樣內容的英語文字,它會比ASCII編碼大三倍。這個缺點很致命,導致實際上沒有人使用這種編碼方法,HTML 5標準就明文規定,網頁不得編碼成UTF-32。

(5) UCS UCS-2

國際標準化組織(ISO)的ISO/IEC JTC1/SC2/WG2工作組是1984年成立的,想要做統一字符集,並與1989年開始著手構建UCS(通用字符集),也叫ISO 10646標準,當然另一個想做統一字符集的是1988年成立的Unicode團隊,等到他們發現了對方的存在,很快就達成一致:世界上不需要兩套統一字符集(幸虧知道的早啊)。

1991年10月,兩個團隊決定合併字符集。也就是說,從今以後只發布一套字符集,就是Unicode標準,並且修訂此前釋出的字符集,UCS的碼點將與Unicode完全一致。(兩個標準同時是存在)

UCS的開發進度快於Unicode,1990年就公佈了第一套編碼方法UCS-2,使用2個位元組表示已經有碼點的字元。(那個時候只有一個平面,就是基本平面,所以2個位元組就夠用了。)UTF-16編碼遲至1996年7月才公佈,明確宣佈是UCS-2的超集,即基本平面字元沿用UCS-2編碼,輔助平面字元定義了4個位元組的表示方法。

兩者的關係簡單說,就是UTF-16取代了UCS-2,或者說UCS-2整合進了UTF-16。所以,現在只有UTF-16,沒有UCS-2。

UCS-2 使用2個位元組表示已經有碼點的字元,第一個位元組在前,就是"大尾方式"(Big endian),第二個位元組在前就是"小尾方式"(Little endian)。

那麼很自然的,就會出現一個問題:計算機怎麼知道某一個檔案到底採用哪一種方式編碼?

Unicode 規範定義,每一個檔案的最前面分別加入一個表示編碼順序的字元,這個字元的名字叫做"零寬度非換行空格"(zero width no-break space),用FEFF表示。這正好是兩個位元組,而且FF比FE大1。

如果一個文字檔案的頭兩個位元組是FE FF,就表示該檔案採用大尾方式;如果頭兩個位元組是FF FE,就表示該檔案採用小尾方式。

1.2 JavaScript 編碼方法存在的問題

最上面給出的圖中字元的發展歷史和JavaScript的誕生時間對比下,可以知道JavaScript如果要想用Unicode字符集,比較恰的選擇是UCS-2編碼方法,UTF-8,UTF-16都來的晚了一些,UCS-4倒是有的,但是英文字元本來一個位元組就可以的,現在也要用4個位元組,還是挺嚴重的事情的。96年那個時候,電腦普遍配置記憶體 8MB-16MB,硬碟850MB—1.2GB。

ECMAScript 6 之前,JavaScript字元編碼方式使用UCS-2,是導致之後JavaScript對位於輔助平面的字元(超過兩個位元組的字元)操作出現異常情況的根本原因。

ECMAScript 6 強制使用UTF-16字串編碼來解決字元超過兩個位元組時出現異常的問題,並按照這種字元編碼來標準化字串操作。

// 存在的問題
const text = '?';

console.log(text.length)  //列印 2 ,其實是一個Emoji表情符
console.log(/^.$/.test(text)) // false , 正則匹配也出了問題,說不是一個字元
console.log(/^..$/.test(text)) // true , 是兩個字元
console.log(text.charAt(0)) // � 前後兩個位元組碼位都是落在U+D800到U+DFFF這個空段,列印不出東西
console.log(text.charAt(1)) // �
console.log(text.charCodeAt(0)) // 55357 轉成十六進位制 0xd83d
console.log(text.charCodeAt(1) //56834 轉成十六進位制 0xde02

// 經過查詢Unicode的字元表,?的碼位是U+1f602
console.log('\u1f602' === '?') //false
console.log('\ud83d\ude02' === '?') // true
複製程式碼

擴充套件:� 的Unicode碼點是 U+FFFD,通常用來表示Unicode轉換時無法識別的字元(也就是亂碼)

1.3 ECMAScript 6 解決字元編碼的問題

(1) 為解決charCodeAt()方法獲取字元碼位錯誤的問題,新增codePointAt()方法

codePointAt()方法完全支援UTF-16,引數接收的是編碼單元的位置而非字元位置,返回與字串中給定位置對應的碼位,即一個整數。

對於BMP字符集中的字元,codePointAt()方法的返回值跟charCodeAt()相同,而對於非BMP字符集來說,返回值不同。

const text = '?';

console.log(text.charCodeAt(0)) // 位置0處的一個編碼單元 55357
console.log(text.charCodeAt(1)) // 位置1處的一個編碼單元 56834

console.log(text.codePointAt(0)) // 位置0處的編碼單元開始的碼位,此例是從這個編碼單位開始的兩個編碼單元組合的字元(四個位元組),所以會列印出所有碼位,即四位元組的碼位 128514 即0x1f602,大於0xffff,也證明了是佔四個位元組的儲存空間。
console.log(text.codePointAt(1)) // 位置1處的編碼單元開始的碼位 56834
複製程式碼

(2) 為解決超過兩個位元組的碼點與字元轉換問題,新增了fromCodePoint()方法

// 列印?
console.log(String.fromCharCode(128514)) // 列印失敗 
console.log(String.fromCharCode(55357,56834)) // 引數可以接收一組序列數字,表示 Unicode 值。列印成功 ?

console.log(String.fromCodePoint(128514)) // 列印成功 ?
console.log(String.fromCodePoint(0x1f602)) // 可以接收不同進位制的引數,列印成功 ?
複製程式碼

(3) 為解決正規表示式無法正確匹配超過兩個位元組的字元問題,ES6定義了一個支援Unicode的 u 修飾符

const text = '?';

console.log(/^.$/.test(text)) // false , 正則匹配出了問題,說不是一個字元
console.log(/^..$/.test(text)) // true , 是兩個字元

console.log(/^.$/u.test(text)) // true, 加入 u 修飾符,匹配正確
複製程式碼

注意:u修飾符是語法層面的變更,在不支援ES6的JavaScript的引擎中使用它會導致語法錯誤,可以使用RegExp建構函式和try……catch來檢測,避免發生語法錯誤

(4) 為解決超過\uffff碼點的字元無法直接用碼點表示的問題,引入了\u{xxxxx}

console.log('\u1f602' === '?') //false
console.log('\ud83d\ude02' === '?') // true

console.log('\u{1f602}' === '?') // true
複製程式碼

(5) 解決字串中有四個位元組的字元的length問題

const text = '笑哭了?';

// 解決一
// 上線UTF-16如果是在輔助平面(佔4個位元組)的話,會有代理對,U+D800-U+DBFF和U+DC00-U+DFFF
var surrogatePair = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; // 匹配UTF-16的代理對

function firstGetRealLength(string) {
	return string
		// 把代理對改為一個BMP的字元,然後獲取長度
		.replace(surrogatePair, '_')
		.length;
}
firstGetRealLength(text); // 4

// 解決二(推薦)
// 字串是可迭代的,可以用Array.from()來轉化成陣列計算length
function secondGetRealLength(string) {
	return Array.from(string).length;
}
secondGetRealLength(text); // 4

// 解決三
// 使用正則新增加的修飾符u
function thirdGetRealLength(string) {
    let result = text.match(/[\s\S]/gu);
	return result?result.length:0;
}
thirdGetRealLength(text); // 4
複製程式碼

(5) 解決字串中有四個位元組的字元的字串反轉問題

const text = '笑哭了?';

function reverse(string) {
    return string.split('').reverse().join('');
}

function reversePlus(string) {
	return Array.from(string).reverse().join('');
}

console.log(reverse(text)) // ��了哭笑 因為?是\ud83d\ude02反轉後是\ude02\ud83d,不是一個合法的代理對(高低位元組範圍不同)
console.log(reversePlus(text)) // ?了哭笑
複製程式碼

2、 ECMAScript 6 模板字面量

模板字面量的填補的ES5的一些特性

  • 多行字串
  • 基本的字串格式化,有將變數的值嵌入字串的能力
  • HTML轉義,向HTML中插入經過安全轉換後的字串的能力

(1)多行字串中反撇號中的所有空白符都屬於字串的一部分

let message = `a
            b`;
console.log(message.length) //15
複製程式碼

(2)標籤模板:模板字串可以緊跟在一個函式名後面,該函式將被呼叫來處理這個模板字串。這被稱為“標籤模板”功能(tagged template)。

標籤模板其實不是模板,而是函式呼叫的一種特殊形式。“標籤”指的就是函式,緊跟在後面的模板字串就是它的引數。

let a = 5;
let b = 10;
function tag(s, v1, v2) {
  console.log(s[0]);
  console.log(s[1]);
  console.log(s[2]);
  console.log(v1);
  console.log(v2);

  return "OK";
}

// 標籤模板呼叫
tag`Hello ${ a + b } world ${ a * b }`;
// 等同於
tag(['Hello ', ' world ', ''], 15, 50);

//列印
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"
複製程式碼

“標籤模板”的一個重要應用,就是過濾 HTML 字串,防止使用者輸入惡意內容。標籤模板的另一個應用,就是多語言轉換(國際化處理)。

參考連結

字元編碼筆記:ASCII,Unicode 和 UTF-8

談談Unicode編碼——其中有“大尾”和“小尾”的來源描述小人國呦

字元編碼趣聞

Javascript有個Unicode的天坑

Unicode與JavaScript詳解

UTF-8, a transformation format of ISO 10646

ASCII

UTF-8

UTF-8 遍地開花

UTF-16

通用字符集

Unicode官網

Javascript誕生記

相關文章