午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

每個JavaScript開發(fā)者都該懂的Unicode

 dxw555 2017-09-04

關(guān)鍵時刻,第一時間送達!


(譯者注:本文含有Unicode輔助平面的特殊字符,部分瀏覽器可能無法正確顯示,但并不影響理解文章內(nèi)容。)


在動筆寫這篇文章之前,我得先懺悔一下:在很長一段時間里我對Unicode充滿了恐懼。


每次遇到需要Unicode知識的編程問題時,我總是找一個hack方案來解決,但解決方案的原理我也不懂。


直到遇見一個需要深入了解Unicode知識才能解決的問題,我才停止了這種逃避。因為這個問題沒辦法應(yīng)用特定情境的解決方案。


在努力讀了一大堆文章之后,我驚訝地發(fā)現(xiàn)Unicode并不難懂。好吧,確實是有些文章起碼得看3遍才能看懂。


但我發(fā)現(xiàn)Unicode標(biāo)準(zhǔn)不僅世界通用,而且十分優(yōu)雅簡潔,只不過要理解其中一些抽象概念有點困難。


如果你覺得理解Unicode很難,那么是時候來面對它了!其實它沒你想的那么難。去沏一杯香濃的茶或咖啡吧?,讓我們進入抽象概念、字符、星光平面(輔助平面)和代理對的世界。


本文首先會解釋Unicode中的基本概念,這是必需的背景知識。


然后會說明JavaScript如何解析Unicode,以及你可能踩到哪些坑。


你還會學(xué)到如何利用ECMAScript 2015的新特性來解決部分難題。


準(zhǔn)備好了?那就燥起來吧!


1. Unicode背后的思想


首先問一個最基礎(chǔ)的問題:你是怎樣閱讀并理解這篇文章的?答案很簡單,因為你明白這些字以及由字組成的單詞的含義。


那你又是如何明白這些字的含義的呢?答案也很簡單,因為你(讀者)和我(作者)對于這些(呈現(xiàn)在屏幕上的)圖形與漢字(即含義)之間的聯(lián)系有著相同的認(rèn)知。


對計算機來說這個原理也差不多,只有一點不同:計算機不懂這些字(字母)的含義,只是將其理解為特定的比特序列。


讓我們設(shè)想一個情景:計算機User1向計算機User2發(fā)送一條消息'hello'。


計算機并不知道這些字母的含義。所以計算機User1將消息'hello'轉(zhuǎn)換為一串?dāng)?shù)字序列0x68 0x65 0x6C 0x6C 0x6F,每個字母對應(yīng)一個數(shù)字:h對應(yīng)0x68, e對應(yīng)0x65,等等。


接著將這些數(shù)字發(fā)送給計算機User2。


計算機User2收到數(shù)字序列0x68 0x65 0x6C 0x6C 0x6F后,使用同一套字母與數(shù)字的對應(yīng)關(guān)系重建消息內(nèi)容,'hello'就能正確地顯示出來了。


不同計算機之間對字母與數(shù)字之間對應(yīng)關(guān)系的協(xié)議就是Unicode進行標(biāo)準(zhǔn)化的結(jié)果。


根據(jù)Unicode,h是一個名為LATIN SMALL LETTER H的抽象字符。這個抽象字符對應(yīng)數(shù)字0x68,也就是一個標(biāo)記為U 0068的代碼點。這些概念將在下一章中說明。


Unicode的作用就是提供一個抽象字符列表(字符集),并給每一個字符分配一個獨一無二的標(biāo)識符代碼點(編碼字符集)。


2. Unicode基本概念


www.unicode.org網(wǎng)站提到:


Unicode為每一個字符分配一個專有的數(shù)字


不分平臺


不分程序


不分語言


Unicode是一個世界通用的字符集,它定義了全世界大部分書寫體系的字符集,并為每一個字符分配了一個獨一無二的數(shù)字(代碼點)。



Unicode囊括了大部分現(xiàn)代語言、標(biāo)點符號、附加符號(變音符)、數(shù)學(xué)符號、技術(shù)符號、箭頭和表情符號等。


Unicode第一版1.0于1991年10月發(fā)布,包含7161個字符。最新版9.0(2016年6月發(fā)布)則提供了128172個字符的編碼。


Unicode的通用性與開放性解決了過去一直存在的一個問題:供應(yīng)商們各自實現(xiàn)不同的字符集和編碼規(guī)則,很難處理。


創(chuàng)建一個支持所有字符集和編碼規(guī)則的應(yīng)用是十分復(fù)雜的。更不用說你選用的編碼可能不支持所有你需要的語言。


如果你覺得Unicode很難,那就想想如果沒有它編程會更難。


我還記得從前隨機選擇所需的字符集和編碼規(guī)則去讀取文件內(nèi)容的時候。全靠人品?。?/p>


2.1 字符與代碼點


抽象字符(即文本字符)是用來組織、管理或表現(xiàn)文本數(shù)據(jù)的信息單位。


Unicode中的字符是一個抽象概念。每一個抽象字符都有一個對應(yīng)的名稱,例如LATIN SMALL LETTER A。該抽象字符的圖像表現(xiàn)形式(glyph)是a。(譯者注:glyph即圖像字符)


代碼點是指被分配給某個抽象字符的數(shù)字


代碼點以U <hex>的形式表示,U 是代表Unicode的前綴,而<hex>是一個16進制數(shù)。例如U 0041和U 2603都是代碼點。


代碼點的取值范圍是從U 0000到U 10FFFF。


記住代碼點就是一個簡單的數(shù)字。思考有關(guān)Unicode的問題時要記得這一點。


代碼點就好像數(shù)組元素的下標(biāo)。


Unicode的神奇之處就在于將代碼點與抽象字符關(guān)聯(lián)起來。例如U 0041對應(yīng)的抽象字符名為LATIN CAPITAL LETTER A (表現(xiàn)為A),而U 2603對應(yīng)的抽象字符名為SNOWMAN(表現(xiàn)為?)


注意,并非所有的代碼點都有對應(yīng)的抽象字符??捎玫拇a點有1114112個,但分配了抽象字符的只有128237個。


2.2 Unicode平面


平面是指從U n0000到U nFFFF的區(qū)間,也就是65536(1000016)個連續(xù)的Unicode代碼點,n的取值范圍是從016到1016。


這些平面將Unicode代碼點分為17個大小相等的集合:


  • 平面0包含從U 0000到U FFFF的代碼點

  • 平面1包含從U **1**0000到U **1**FFFF的代碼點

  • 平面16包含從U **10**0000到U **10**FFFF的代碼點


基本多文種平面


平面0比較特殊,被稱為基本多文種平面或簡稱BMP。它包含了大多數(shù)現(xiàn)代語言的字符 (基本拉丁字母, 西里爾字母, 希臘字母等)和大量的符號。


如上文所述,基本多文種平面的代碼點取值范圍是從U 0000到U FFFF,最多可以有4位16進制數(shù)字。


大多數(shù)時候開發(fā)者處理的都是BMP中的字符。它包含了大多數(shù)情況下的必需字符。


BMP中的一些字符:


  • e對應(yīng)代碼點U 0065 抽象字符名: LATIN SMALL LETTER E

  • |對應(yīng)代碼點U 007C 抽象字符名: VERTICAL BAR

  • ■對應(yīng)代碼點U 25A0 抽象字符名: BLACK SQUARE

  • ?對應(yīng)代碼點U 2602 抽象字符名: UMBRELLA


星光平面


BMP之后的16個平面(平面1,平面2,…,平面16)被稱為星光平面或輔助平面。


星光平面的代碼點被稱為星光代碼點。這些代碼點的取值范圍是從U 10000到U 10FFFF。


星光代碼點可能會有5位或6位16進制數(shù)字:U ddddd或U dddddd。


來看幾個星光平面里的字符:


  • 對應(yīng)U 1D11E抽象字符名:MUSICAL SYMBOL G CLEF

  • 對應(yīng)U 1D401抽象字符名:MATHEMATICAL BOLD CAPITAL B

  • 對應(yīng)U 1F035抽象字符名:DOMINO TITLE HORIZONTAL-00-04

  • 對應(yīng)U 1F600抽象字符名:GRINNING FACE


2.3 碼元


計算機在存儲時當(dāng)然不會使用代碼點或抽象字符,它們是存在于開發(fā)者大腦中的概念。


所以自然要有一種在物理層面表示Unicode代碼點的方式:碼元。


碼元是指使用某種給定的編碼規(guī)則給抽象字符編碼后得到的比特序列。


字符編碼將抽象層面的代碼點轉(zhuǎn)換為物理層面的比特序列:碼元。


換句話說,字符編碼的作用就是將Unicode代碼點翻譯成獨一無二的碼元序列。


常用的字符編碼有UTF-8, UTF-16 和 UTF-32.


大多數(shù)JavaScript引擎使用UTF-16編碼字符。它會影響JavaScript處理Unicode的方式。所以從這里開始讓我們集中精力于UTF-16吧。


UTF-16(全稱:16位統(tǒng)一碼轉(zhuǎn)換格式)是一種變長編碼:


  • BMP中的代碼點編碼為單個16位的碼元

  • 星光平面的代碼點編碼為兩個16位的碼元


來看幾個例子


假設(shè)我們想把LATIN SMALL LETTER A,也就是抽象字符a存入硬盤。Unicode告訴我們抽象字符LATIN SMALL LETTER A對應(yīng)代碼點U 0061。


現(xiàn)在我們來看看UTF-16如何轉(zhuǎn)換U 0061。編碼規(guī)范上說,對于BMP中的代碼點只需將它的16進制數(shù)字U 0061存入一個16位的碼元就行了。


顯然,BMP中的代碼點剛好能存進一個16位的碼元。編碼BMP可謂小菜一碟。


2.4 代理對


現(xiàn)在讓我們來研究一個復(fù)雜些的例子。假設(shè)我們想存儲一個星光代碼點(屬于星光平面):GRINNING FACE character 。該字符對應(yīng)的代碼點是 U 1F600。


由于星光代碼點需要21個比特來存儲字符信息,UTF-16需要兩個碼元來編碼,每個16比特。代碼點 U 1F600 被拆分為所謂的代理對:0xD83D(高位代理碼元)與0xDE00(低位代理碼元)。


代理對用來表示那些對應(yīng)2個16位碼元序列的抽象字符,其中第一個碼元是高位代理碼元而第二個是低位代理碼元。


編碼一個星光代碼點需要兩個碼元:即一個代理對。比如前面那個例子,使用UTF-16編碼U 1F600 ()就使用了一個代理對:0xD83D 0xDE00。


`console.log('\uD83D\uDE00'); // => ''`


高位代理碼元的取值范圍是從0xD800到0xDBFF。 低位代理碼元的取值范圍是從0xDC00到0xDFFF。


代理對與代碼點之間互相轉(zhuǎn)換的算法如下所示:


function getSurrogatePair(astralCodePoint) {  

  let highSurrogate =

     Math.floor((astralCodePoint - 0x10000) / 0x400)   0xD800;

  let lowSurrogate = (astralCodePoint - 0x10000) % 0x400   0xDC00;

  return [highSurrogate, lowSurrogate];

}

getSurrogatePair(0x1F600); // => [0xDC00, 0xDFFF]

 

function getAstralCodePoint(highSurrogate, lowSurrogate) {  

  return (highSurrogate - 0xD800) * 0x400

        lowSurrogate - 0xDC00   0x10000;

}

getAstralCodePoint(0xD83D, 0xDE00); // => 0x1F600


代理對并不是一個令人愉快的東西。在JavaScript中處理字符串時我們必須將它們視為特殊情況來處理,具體內(nèi)容我們在下章細(xì)說。


但UTF-16的存儲效率很高。因為99%需要處理的字符都屬于BMP,只需要1個碼元。


2.5 組合用字符


在一個書寫系統(tǒng)的上下文中,一個字素或者符號是最小的可區(qū)分單元。


字素就是用戶所認(rèn)為的一個字符。屏幕上所展示的一個有形的字素稱為圖像字符(glyph)。


在大多數(shù)情況下,一個Unicode字符就代表一個字素。例如 U 0066 LATIN SMALL LETTER F就是一個英文字母f。


但有時候一個字素會包含一系列字符。


例如?在丹麥語書寫系統(tǒng)中是一個不可再分的字素。但它是用U 0061 LATIN SMALL LETTER A (渲染為a) 結(jié)合一個特殊字符U 030A COMBINING RING ABOVE(渲染為??)來顯示的。


U 030A用來修飾前一個字符,這種字符稱為組合用字符。


console.log('\u0061\u030A'); // => 'a?'  

console.log('\u0061');       // => 'a'


組合用字符是應(yīng)用在前一個基礎(chǔ)字符上以形成完整字素的字符。


組合用字符包括以下字符:重音符號、變音符、希伯來語點、阿拉伯語元音符號和印度語節(jié)拍符。


組合用字符通常不會離開基礎(chǔ)字符單獨使用。我們應(yīng)該避免單獨顯示它們。


與代理對一樣,在JavaScript中處理組合用字符也很棘手。


在用戶看來一個組合字符序列(基礎(chǔ)字符 組合用字符)是【一】個符號(例如'\u0061\u030A'就是'a?')。但開發(fā)者必須清楚實際上要用到兩個代碼點U 0061和U 030A來生成a?。



3. JavaScript中的Unicode


ES2015規(guī)范提到源代碼文本使用Unicode(5.1及以上版本)表示。源碼文本是一串取值范圍從U 0000到U 10FFFF的代碼點序列。盡管ECMAScript規(guī)范沒有指明源碼儲存和交換的方式,但通常都以UTF-8編碼(在web中推薦使用的編碼)。


我建議將源代碼文本控制在Basic Latin Unicode block(或者說ASCII)中。超出ASCII的字符應(yīng)該避免使用。這能保證源碼文本在編碼時少出些問題。


ECMAScript 2015在語言層面上給出了JavaScript中String(字符串)的明確定義:


String類型是由16比特?zé)o符號整型數(shù)值(“元素”)組成的集合,最少包含0個元素,最多包含253-1個元素。String類型通常用來在運行ECMAScript的程序中表示文本信息,因此String中的每個元素都被當(dāng)作一個UTF-16碼元值。


字符串中的每一個元素都會被引擎解釋為一個碼元。而字符串的渲染結(jié)果并不能明確地反映它包含的碼元(及其所代表的代碼點)??聪旅孢@個例子:


console.log('cafe\u0301'); // => 'cafe?'  

console.log('café');       // => 'café'


雖然字面量'cafe\u0301'和'café'有輕微的差別,但兩者都被渲染為同樣的字符序列cafe?。


字符串的長度是指其中包含的元素(即16位數(shù)值)的個數(shù)。ECMAScript在解釋String類型時,字符串的每一個元素都被解釋為一個UTF-16碼元。


從上一章關(guān)于代理對和組合用字符的內(nèi)容可知,某些字符需要2個以上的碼元來表示。所以在計算字符長度或通過字符串索引訪問字符時要格外小心。


let smile = '\uD83D\uDE00';  

console.log(smile);        // => ''  

console.log(smile.length); // => 2

 

let letter = 'e\u0301';  

console.log(letter);        // => 'e?'  

console.log(letter.length); // => 2


字符串smile包含兩個碼元:\uD83D (高位代理)和\uDE00(低位代理)。由于字符串是碼元的序列,因此盡管 smile 的渲染結(jié)果只有一個字符'',smile.length的值卻為2。


對于字符串letter也一樣。組合用字符U 0301應(yīng)用于前一個字符,渲染結(jié)果是一個字符'e?'。然而letter包含2個碼元,因此letter.length值為2。


我的建議是:始終將JavaScript中的字符串視為一串碼元序列。字符串渲染的結(jié)果并不能清晰地表明它包含了怎樣的碼元。


星光符號和組合字符序列需要2個以上的碼元來編碼,卻被視為一個語素。


如果字符串中含有代理對或組合用字符,而開發(fā)者又不清楚這一點,那么在計算字符串長度或通過索引訪問字符時就可能會感到困惑。


大多數(shù)JavaScript字符串方法都不能識別Unicode。如果字符串含有混合的Unicode字符,在調(diào)用myString.slice()、myString.substring()等方法時就要小心了。


3.1 轉(zhuǎn)義序列


JavaScript字符串中的轉(zhuǎn)義序列通常都是基于代碼點數(shù)字的。JavaScript有3種轉(zhuǎn)義模式,在ECMAScript 2015中有相關(guān)介紹。


來詳細(xì)看看這幾種模式吧。


16進制轉(zhuǎn)義序列


最簡短的形式稱為16進制轉(zhuǎn)義序列:\x<hex>. \x為前綴,后面跟一個2位的16進制數(shù)。


比如'\x30'(字符 '0')和'\x5B'(字符 '[')。


在字符串中使用16進制轉(zhuǎn)義序列如下所示:


var str = '\x4A\x61vaScript';  

console.log(str);                    // => 'JavaScript'  

var reg = /\x4A\x61va.*/;  

console.log(reg.test('JavaScript')); // => true


16進制轉(zhuǎn)義序列只能編碼從U 00到U FF的有限數(shù)量的代碼點,因為它只能有2位數(shù)字。但16進制轉(zhuǎn)義序列的好處是它很短。


Unicode轉(zhuǎn)義序列


如果你想轉(zhuǎn)義整個BMP中的代碼點,那就用Unicode轉(zhuǎn)義序列。轉(zhuǎn)義形式是\u<hex>,\u為前綴,后面跟一個4位的16進制數(shù)。


比如 '\u0051' (字符 'Q')和'\u222B' (積分符號 '∫').


像下面這樣使用Unicode轉(zhuǎn)義序列:


var str = 'I\u0020learn \u0055nicode';  

console.log(str);                 // => 'I learn Unicode'  

var reg = /\u0055ni.*/;  

console.log(reg.test('Unicode')); // => true


Unicode轉(zhuǎn)義序列可以編碼從U 0000到U FFFF的有限數(shù)量的代碼點(BMP中全部代碼點),因為它可以有4位數(shù)字。大多數(shù)時候這已經(jīng)足夠用來表示常用字符了。


想要在JavaScript文本中表示星光字符,可以用兩個連續(xù)的Unicode轉(zhuǎn)義序列(高位代理與低位代理),生成代理對:


var str = 'My face \uD83D\uDE00';  

console.log(str); // => 'My face '


代碼點轉(zhuǎn)義序列


ECMAScript 2015提供了能夠表示整個Unicode空間:從U 0000到U 10FFFF,也就是BMP與星光平面的轉(zhuǎn)義序列。


這種新格式被稱為代碼點轉(zhuǎn)義序列:\u{<hex>},<hex>是一個長度為1至6位的16進制數(shù)。 比如'\u{7A}'(字符'z')和'\u{1F639}'(Funny cat符號)。


來看看它應(yīng)該如何應(yīng)用:


var str = 'Funny cat \u{1F639}';  

console.log(str);                      // => 'Funny cat '  

var reg = /\u{1F639}/u;  

console.log(reg.test('Funny cat ')); // => true


注意正則表達式/\u{1F639}/u有一個特殊flagu,它支持額外的Unicode特性(詳情見3.5正則匹配)。


我喜歡代碼點轉(zhuǎn)義不需要使用代理對來表示星光符號這一點。讓我們來轉(zhuǎn)義代碼點U 1F607 SMILING FACE WITH HALO吧:


var niceEmoticon = '\u{1F607}';  

console.log(niceEmoticon);   // => ''  

var spNiceEmoticon = '\uD83D\uDE07'  

console.log(spNiceEmoticon); // => ''  

console.log(niceEmoticon === spNiceEmoticon); // => true


被賦給變量niceEmoticon的字符串字面量包含一個代碼點轉(zhuǎn)義序列'\u{1F607}',它表示一個星光代碼點U 1F607。


然而在這種表象之下代碼點轉(zhuǎn)義序列依舊生成了一個代理對(2個碼元)。我們可以看到變量spNiceEmoticon被賦值為使用代理對創(chuàng)建的Unicode轉(zhuǎn)義序列'\uD83D\uDE07',而它與變量niceEmoticon是相等的。



如果正則表達式是用構(gòu)造函數(shù)RegExp創(chuàng)建的,那么在字符串字面量中必須將每一個 \替換為\\來表示Unicode轉(zhuǎn)義序列。


以下正則表達式對象是相等的:


var reg1 = /\x4A \u0020 \u{1F639}/;  

var reg2 = new RegExp('\\x4A \\u0020 \\u{1F639}');  

console.log(reg1.source === reg2.source); // => true


3.2 字符串比較


JavaScript中的字符串是碼元的序列。因此字符串的比較可以看作是碼元的計算與匹配。


這種方法快速而有效,對于“簡單”的字符串不失為一種好方法。


var firstStr = 'hello';  

var secondStr = '\u0068ell\u006F';  

console.log(firstStr === secondStr); // => true


字符串firstStr與secondStr包含相同的碼元序列,故它們相等。


假設(shè)你想比較兩個渲染結(jié)果相同,但包含不同碼元序列的字符串。


那么你可能會得到意外的結(jié)果,因為看上去相同的字符串經(jīng)過比較卻不相等:


var str1 = '?a va bien';  

var str2 = 'c\u0327a va bien';  

console.log(str1);          // => '?a va bien'  

console.log(str2);          // => 'c?a va bien'  

console.log(str1 === str2); // => false


str1和str2渲染結(jié)果看起來相同,但包含不同的碼元。


因為字素?可以通過兩種方法生成:


  • 使用U 00E7 LATIN SMALL LETTER C WITH CEDILLA

  • 或者用組合字符序列:U 0063 LATIN SMALL LETTER C 加上組合用字符U 0327COMBINING CEDILLA.


那么該如何處理這種情況,正確地比較字符串?答案是字符串標(biāo)準(zhǔn)化。


標(biāo)準(zhǔn)化


標(biāo)準(zhǔn)化是指將字符串轉(zhuǎn)換為統(tǒng)一的表示形式,以保證具有標(biāo)準(zhǔn)等價性(或兼容等價性)的字符串只有一種表示形式。


換句話說,當(dāng)字符串包含組合用字符序列或其他混合結(jié)構(gòu)等復(fù)雜的結(jié)構(gòu)時,我們可以將它統(tǒng)一成標(biāo)準(zhǔn)的形式。標(biāo)準(zhǔn)化的字符串在進行比較或文本查找等操作時就很輕松了。


Unicode Standard Annex #15對標(biāo)準(zhǔn)化方法有詳細(xì)地描述。


在JavaScript中對字符串進行標(biāo)準(zhǔn)化需要調(diào)用ES2015提供的myString.normalize([normForm])方法。normForm是一個可選參數(shù)(默認(rèn)為'NFC'),取值為以下標(biāo)準(zhǔn)化模式之一:


  • 'NFC' as Normalization Form Canonical Composition(標(biāo)準(zhǔn)化形式-標(biāo)準(zhǔn)性合成)

  • 'NFD' as Normalization Form Canonical Decomposition(標(biāo)準(zhǔn)化形式-標(biāo)準(zhǔn)性分解)

  • 'NFKC' as Normalization Form Compatibility Composition(標(biāo)準(zhǔn)化形式-兼容性合成)

  • 'NFKD' as Normalization Form Compatibility Decomposition(標(biāo)準(zhǔn)化形式-兼容性分解)


讓我們利用字符串標(biāo)準(zhǔn)化來改進上面的例子吧,這次可以正確地比較字符串了:


var str1 = '?a va bien';  

var str2 = 'c\u0327a va bien';  

console.log(str1 === str2.normalize()); // => true  

console.log(str1 === str2);     


'?'與'c\u0327'具有標(biāo)準(zhǔn)等價性。


調(diào)用str2.normalize(),會返回一個str2 的標(biāo)準(zhǔn)形式副本('c\u0327'替換為'?')。因此比較語句str1 === str2.normalize()會如預(yù)期一般返回true.


str1不受標(biāo)準(zhǔn)化影響,因為它已經(jīng)是標(biāo)準(zhǔn)形式了。


但為了使操作符兩端都取得標(biāo)準(zhǔn)化字符串,將待比較的2個字符串都標(biāo)準(zhǔn)化也是合理的。


3.3 字符串長度


想要知道一個字符串的長度通常我們會訪問myString.length這個屬性。該屬性表明了字符串中包含的碼元個數(shù)。


對于只包含BMP代碼點的字符串來說獲取字符串長度通常都能符合預(yù)期:


var color = 'Green';  

console.log(color.length); // => 5


color中的每個碼元都對應(yīng)著一個字素。預(yù)期的字符串長度為5.


長度與代理對


當(dāng)字符串中包含用來表示星光代碼點的代理對時,事情就變得不對勁了。因為每個代理對包含2個碼元(一個高位代理和一個低位代理),length屬性值會比預(yù)期值要大。


比如這個例子:


var str = 'cat\u{1F639}';  

console.log(str);        // => 'cat'  

console.log(str.length); // => 5


字符串str的渲染結(jié)果是4個字符cat。


然而smile.length等于5,因為U 1F639是一個星光代碼點,它被編碼成了2個碼元(一個代理對)。


不幸的是目前還沒有一種高性能的原生方法能解決這個問題。


但至少ECMAScript 2015引入了一種能夠識別星光字符的算法。星光字符即使被編譯為2個碼元,也會被計算為一個字符。


這個能夠識別Unicode的利器就是字符迭代器String.prototype[@@iterator]()。你可以給字符串加上擴展操作符[...str]或Array.from(str)函數(shù)(兩者都會調(diào)用字符串迭代器)。然后再計算返回數(shù)組中的字符個數(shù)。


需要注意的是這個解決方案如果大量使用可能會造成輕微的性能損失。


讓我們用這個擴展操作符來改進上面的例子吧:


var str = 'cat\u{1F639}';  

console.log(str);             // => 'cat'  

console.log([...str]);        // => ['c', 'a', 't', '']  

console.log([...str].length); // => 4


[...str]創(chuàng)建了一個包含4個字符的數(shù)組。編碼U 1F639 CAT FACE WITH TEARS OF JOY 的代理對原封不動地保留了下來,因為字符串迭代器能夠識別Unicode。


長度與組合用字符


那么組合字符序列呢?由于每個組合用字符都是一個碼元,因此你會遇到同樣的困難。


這個問題對于標(biāo)準(zhǔn)化的字符串可以不用擔(dān)心。如果運氣好,組合字符序列會被標(biāo)準(zhǔn)化為單個字符。我們來試試看:


var drink = 'cafe\u0301';  

console.log(drink);                    // => 'cafe?'  

console.log(drink.length);             // => 5  

console.log(drink.normalize())         // => 'café'  

console.log(drink.normalize().length); // => 4


字符串drink包含5個碼元(因此drink.length等于5),盡管它只顯示4個字符。


在標(biāo)準(zhǔn)化drink時,我們幸運地發(fā)現(xiàn)組合字符序列'e\u0301'有標(biāo)準(zhǔn)形式'é'。因此drink.normalize().length返回了預(yù)期的4。


不幸的是標(biāo)準(zhǔn)化并不能解決所有問題。那些比較長的組合字符序列并不都有對應(yīng)的單個字符標(biāo)準(zhǔn)形式。比如這個例子:


var drink = 'cafe\u0327\u0301';  

console.log(drink);                    // => 'cafe??'  

console.log(drink.length);             // => 6  

console.log(drink.normalize());        // => 'caf??'  

console.log(drink.normalize().length); // => 5


drink包含6個碼元所以drink.length值為6。然而drink只包含4個字符。


標(biāo)準(zhǔn)化函數(shù)drink.normalize()將組合序列'e\u0327\u0301'轉(zhuǎn)換為含有2個字符的標(biāo)準(zhǔn)形式'?\u0301'(只去掉了一個組合用字符)。于是我們很難過地發(fā)現(xiàn)drink.normalize().length的值為5,仍然不能正確地計算字符的個數(shù)。


3.4 字符定位


由于字符串是碼元的序列,通過字符串索引來訪問字符同樣會有困難。


如果字符串只包含BMP字符(除了從U D800到U DBFF的高位代理和從U DC00到U DFFF的低位代理),字符定位可以得到正確的結(jié)果。


var str = 'hello';  

console.log(str[0]); // => 'h'  

console.log(str[4]); // => 'o'


上例中每個字符被編碼為一個碼元,因此通過索引訪問字符可以得到正確的結(jié)果。


字符定位與代理對


當(dāng)字符串中包含星光字符時情況就不一樣了。


星光字符被編碼為2個碼元(一個代理對)。因此通過索引來訪問字符可能會返回一個單獨的高位代理或低位代理,而單獨的高位/低位代理是無效字符。


下面這個例子演示了訪問星光字符的情形:


var omega = '\u{1D6C0} is omega';  

console.log(omega);        // => ' is omega'  

console.log(omega[0]);     // => '' (unprintable symbol)  

console.log(omega[1]);     // => '' (unprintable symbol)


由于U 1D6C0 MATHEMATICAL BOLD CAPITAL OMEGA是一個星光字符,它的編碼使用了一個代理對,即2個碼元。


omega[0]訪問的是高位代理碼元而omega[1]訪問的是低位代理碼元,代理對被分成了兩半。


想要正確地訪問字符串中星光字符,有2種方法:


  • 使用能夠識別Unicode的字符串迭代器生成一個字符數(shù)組[...str][index]

  • 用number = myString.codePointAt(index)獲取代碼點,然后用String.fromCodePoint(number)將代碼點轉(zhuǎn)換為字符(推薦方法)


讓我們來嘗試一下這兩種方法:


var omega = '\u{1D6C0} is omega';  

console.log(omega);                        // => ' is omega'  

// Option 1

console.log([...omega][0]);                // => ''  

// Option 2

var number = omega.codePointAt(0);  

console.log(number.toString(16));          // => '1d6c0'  

console.log(String.fromCodePoint(number)); // => ''


[...smile]返回一個包含字符串omega中字符的數(shù)組。代理對被正確識別,因此訪問第一個字符返回了符合預(yù)期的結(jié)果:[...smile][0]返回''.


函數(shù)omega.codePointAt(0)能夠識別Unicode,因此它返回了字符串omega第一個字符的星光代碼點數(shù)字0x1D6C0。函數(shù)String.fromCodePoint(number)則返回了這個代碼點對應(yīng)的字符:''。


字符定位與組合用字符


字符定位在遇到組合用字符時會出現(xiàn)和上面一樣的問題。


通過索引訪問字符實際上是訪問碼元。然而組合字符序列應(yīng)該被整體訪問,而不是被分成單個的碼元。


下面這個例子演示了這個問題:


var drink = 'cafe\u0301';  

console.log(drink);        // => 'cafe?'  

console.log(drink.length); // => 5  

console.log(drink[3]);     // => 'e'  

console.log(drink[4]);     // => ??


drink[3]只訪問到了基礎(chǔ)字符e,沒有包括組合用字符U 0301 COMBINING ACUTE ACCENT(渲染為?? )。


drink[4]訪問的是獨立的組合用字符?? 。


這種情況需要使用字符串標(biāo)準(zhǔn)化。組合字符序列U 0065 LATIN SMALL LETTER E U 0301 COMBINING ACUTE ACCENT有對應(yīng)的標(biāo)準(zhǔn)形式U 00E9 LATIN SMALL LETTER E WITH ACUTE é


我們來改進一下前面的例子:


var drink = 'cafe\u0301';  

console.log(drink.normalize());        // => 'cafe?'  

console.log(drink.normalize().length); // => 4  

console.log(drink.normalize()[3]);     // => 'é'


需要注意的是并非所有組合字符序列都有對應(yīng)的單個標(biāo)準(zhǔn)字符。因此標(biāo)準(zhǔn)化并不能解決所有問題。


好在對于歐洲/北美語言來說它可以解決大部分問題。


3.5 正則匹配


正則表達式與字符串一樣,是基于碼元工作的。因此與上文描述的情形相似,使用正則表達式在處理代理對和組合字符序列時也會遇到困難。


BMP字符的匹配是符合預(yù)期的,因為一個碼元對應(yīng)一個字符:


var greetings = 'Hi!';  

var regex = /.{3}/;  

console.log(regex.test(greetings)); // => true


greetings有3個字符,編碼為3個碼元。正則表達式/.{3}/期望的是3個碼元,因此與greetings匹配成功。


在匹配星光字符(被編碼為2個碼元的代理對)時,你可能會遇到困難:


var smile = '';  

var regex = /^.$/;  

console.log(regex.test(smile)); // => false


smile包含星光字符U 1F600 GRINNING FACE。U 1F600被編碼為一個代理對0xD83D 0xDE00。


然而正則表達式/^.$/期望的是1個碼元,于是正則匹配regexp.test(smile)失敗了。


在定義字符區(qū)間的時候情況會更糟。JavaScript直接報錯了:


var regex = /[-]/;  

// => SyntaxError: Invalid regular expression: /[-]/:

// Range out of order in character class


星光代碼點會被編碼為代理對,因此JavaScript會用碼元/[\uD83D\uDE00-\uD83D\uDE0E]/來表示這個正則表達式。而在pattern中每個碼元被視為一個單獨的元素,所以正則表達式會忽略代理對這個概念。


又由于\uDE00比\uD83D大,\uDE00-\uD83D這個字符區(qū)間是無效的,所以就報錯了。


正則表達式 u 標(biāo)志


好在ECMAScript 2015引入了u標(biāo)志,使得正則表達式能夠識別Unicode。這個標(biāo)志讓我們能夠正確處理星光字符。


在正則表達式中可以使用Unicode轉(zhuǎn)義序列/u{1F600}/u。這樣比寫高位代理和低位代理/\uD83D\uDE00/要短。


讓我們來嘗試應(yīng)用一下u標(biāo)志,看看.操作符(包括量詞?、 、*和{3}、{3,}, {2,3})能否匹配星光字符:


var smile = '';  

var regex = /^.$/u;  

console.log(regex.test(smile)); // => true


正則表達式/^.$/u由于加上了u標(biāo)志而能夠識別Unicode,因此正確地匹配了星光字符。


u標(biāo)志還能使星光字符區(qū)間被正確處理:


var smile = '';  

var regex = /[-]/u;  

var regexEscape = /[\u{1F600}-\u{1F60E}]/u;  

var regexSpEscape = /[\uD83D\uDE00-\uD83D\uDE0E]/u;  

console.log(regex.test(smile));         // => true  

console.log(regexEscape.test(smile));   // => true  

console.log(regexSpEscape.test(smile)); // => true


現(xiàn)在[-]被視為一個星光字符的區(qū)間了。/[-]/u成功匹配了''。


正則表達式與組合用字符


不幸的是不論有沒有u標(biāo)志,正則表達式都會把組合用標(biāo)記視為獨立的碼元來處理。


要匹配組合字符序列,只能分別匹配基礎(chǔ)字符與組合用字符。


看下面的例子:


var drink = 'cafe\u0301';  

var regex1 = /^.{4}$/;  

var regex2 = /^.{5}$/;  

console.log(drink);              // => 'cafe?'  

console.log(regex1.test(drink)); // => false  

console.log(regex2.test(drink)); // => true


字符串渲染為4個字符cafe?。


然而成功匹配'cafe\u0301'的正則表達式是匹配5個元素的/^.{5}$/。


4. 結(jié)語


也許在JavaScript中有關(guān)Unicode的最重要的概念就是將字符串視為碼元序列,事實也確實如此。


如果開發(fā)者認(rèn)為字符串是由字素(或字符)組成,忽略碼元序列這個概念,就會感到困惑。


在處理包含代理對或組合字符序列的字符串時這種想法會造成誤解。


  • 獲取字符串長度

  • 字符定位

  • 正則匹配


注意JavaScript中大多數(shù)字符串方法都不能識別Unicode:比如myString.indexOf()、myString.slice()等。


ECMAScript 2015在字符串和正則表達式中增加了一些很棒的特性,例如代碼點轉(zhuǎn)義序列\(zhòng)u{1F600}。


新的正則表達式標(biāo)志u使字符串匹配能夠識別Unicode,這樣一來匹配星光字符就簡單多了。


字符串迭代器String.prototype[@@iterator]()能夠識別Unicode。使用擴展操作符[...str]或Array.from(str)可以創(chuàng)建一個字符數(shù)組,通過這個數(shù)組的下標(biāo)來計算字符串長度或訪問字符就不會把代理對拆開了。但要注意這種方法會影響性能。


如果你需要更好的辦法來處理Unicode字符,你可以使用punycode庫或者生成特殊的正則表達式。


但愿此文能幫助你掌握Unicode!


    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多