第二章:語法 3

特別說明,為便于查閱察迟,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

粘性標(biāo)志

另一個(gè)加入ES6正則表達(dá)式的模式標(biāo)志是y隐绵,它經(jīng)常被稱為“粘性模式(sticky mode)”愧怜。粘性 實(shí)質(zhì)上意味著正則表達(dá)式在它開始時(shí)有一個(gè)虛擬的錨點(diǎn),這個(gè)錨點(diǎn)使正則表達(dá)式僅以自己的lastIndex屬性所指示的位置為起點(diǎn)進(jìn)行匹配则奥。

為了展示一下考润,讓我們考慮兩個(gè)正則表達(dá)式,第一個(gè)沒有使用粘性模式而第二個(gè)有:

var re1 = /foo/,
    str = "++foo++";

re1.lastIndex;          // 0
re1.test( str );        // true
re1.lastIndex;          // 0 —— 沒有更新

re1.lastIndex = 4;
re1.test( str );        // true —— `lastIndex`被忽略了
re1.lastIndex;          // 4 —— 沒有更新

關(guān)于這個(gè)代碼段可以觀察到三件事:

  • test(..)根本不在意lastIndex的值逞度,而總是從輸入字符串的開始實(shí)施它的匹配爬泥。
  • 因?yàn)槲覀兊哪J經(jīng)]有輸入的起始錨點(diǎn)^泳姐,所以對(duì)"foo"的搜索可以在整個(gè)字符串上自由向前移動(dòng)间狂。
  • lastIndex沒有被test(..)更新惨奕。

現(xiàn)在,讓我們?cè)囈幌抡承阅J降恼齽t表達(dá)式:

var re2 = /foo/y,       // <-- 注意粘性標(biāo)志`y`
    str = "++foo++";

re2.lastIndex;          // 0
re2.test( str );        // false —— 在`0`沒有找到“foo”
re2.lastIndex;          // 0

re2.lastIndex = 2;
re2.test( str );        // true
re2.lastIndex;          // 5 —— 在前一次匹配后更新了

re2.test( str );        // false
re2.lastIndex;          // 0 —— 在前一次匹配失敗后重置

于是關(guān)于粘性模式我們可以觀察到一些新的事實(shí):

  • test(..)str中使用lastIndex作為唯一精確的位置來進(jìn)行匹配馆匿。在尋找匹配時(shí)不會(huì)發(fā)生向前的移動(dòng) —— 匹配要么出現(xiàn)在lastIndex的位置抑胎,要么就不存在。
  • 如果發(fā)生了一個(gè)匹配渐北,test(..)就更新lastIndex使它指向緊隨匹配之后的那個(gè)字符阿逃。如果匹配失敗,test(..)就將lastIndex重置為0

沒有使用^固定在輸入起點(diǎn)的普通非粘性范例可以自由地在字符串中向前移動(dòng)來搜索匹配恃锉。但是粘性模式制約這個(gè)范例僅在lastIndex的位置進(jìn)行匹配搀菩。

正如我在這一節(jié)開始時(shí)提到過的,另一種考慮的方式是破托,y暗示著一個(gè)虛擬的錨點(diǎn)肪跋,它位于正好相對(duì)于(也就是制約著匹配的起始位置)lastIndex位置的范例的開頭。

警告: 在關(guān)于這個(gè)話題的以前的文獻(xiàn)中土砂,這種行為曾經(jīng)被聲稱為y像是在范例中暗示著一個(gè)^(輸入的起始)錨點(diǎn)州既。這是不準(zhǔn)確的。我們將在稍后的“錨定粘性”中講解更多細(xì)節(jié)萝映。

粘性定位

對(duì)反復(fù)匹配使用y可能看起來是一種奇怪的限制吴叶,因?yàn)槠ヅ錄]有向前移動(dòng)的能力,你不得不手動(dòng)保證lastIndex恰好位于正確的位置上序臂。

這是一種可能的場(chǎng)景:如果你知道你關(guān)心的匹配總是會(huì)出現(xiàn)在一個(gè)數(shù)字(例如蚌卤,010贸宏,20造寝,等等)倍數(shù)的位置磕洪。那么你就可以只構(gòu)建一個(gè)受限的范例來匹配你關(guān)心的東西吭练,然后在每次匹配那些固定位置之前手動(dòng)設(shè)置lastIndex

考慮如下代碼:

var re = /f../y,
    str = "foo       far       fad";

str.match( re );        // ["foo"]

re.lastIndex = 10;
str.match( re );        // ["far"]

re.lastIndex = 20;
str.match( re );        // ["fad"]

然而析显,如果你正在解析一個(gè)沒有像這樣被格式化為固定位置的字符串鲫咽,在每次匹配之前搞清楚為lastIndex設(shè)置什么東西的做法可能會(huì)難以維系。

這里有一個(gè)微妙之處要考慮谷异。y要求lastIndex位于發(fā)生匹配的準(zhǔn)確位置分尸。但它不嚴(yán)格要求 來手動(dòng)設(shè)置lastIndex

取而代之的是歹嘹,你可以用這樣的方式構(gòu)建你的正則表達(dá)式:它們?cè)诿看沃髌ヅ渲卸疾东@你所關(guān)心的東西的前后所有內(nèi)容箩绍,直到你想要進(jìn)行下一次匹配的東西為止。

因?yàn)?code>lastIndex將被設(shè)置為一個(gè)匹配末尾之后的下一個(gè)字符尺上,所以如果你已經(jīng)匹配了到那個(gè)位置的所有東西材蛛,lastIndex將總是位于下次y范例開始的正確位置。

警告: 如果你不能像這樣足夠范例化地預(yù)知輸入字符串的結(jié)構(gòu)怎抛,這種技術(shù)可能不合適卑吭,而且你可能不應(yīng)使用y

擁有結(jié)構(gòu)化的字符串輸入马绝,可能是y能夠在一個(gè)字符串上由始至終地進(jìn)行反復(fù)匹配的最實(shí)際場(chǎng)景豆赏。考慮如下代碼:

var re = /\d+\.\s(.*?)(?:\s|$)/y
    str = "1. foo 2. bar 3. baz";

str.match( re );        // [ "1. foo ", "foo" ]

re.lastIndex;           // 7 —— 正確位置!
str.match( re );        // [ "2. bar ", "bar" ]

re.lastIndex;           // 14 —— 正確位置掷邦!
str.match( re );        // ["3. baz", "baz"]

這能夠工作是因?yàn)槲沂孪戎垒斎胱址慕Y(jié)構(gòu):總是有一個(gè)像"1. "這樣的數(shù)字的前綴出現(xiàn)在期望的匹配("foo"白胀,等等)之前,而且它后面要么是一個(gè)空格抚岗,要么就是字符串的末尾($錨點(diǎn))纹笼。所以我構(gòu)建的正則表達(dá)式在每次主匹配中捕獲了所有這一切,然后我使用一個(gè)匹配分組( )使我真正關(guān)心的東西被方便地分離出來苟跪。

在第一次匹配("1. foo ")之后廷痘,lastIndex7,它已經(jīng)是開始下一次匹配"2. bar "所需的位置了件已,如此類推笋额。

如果你要使用粘性模式y進(jìn)行反復(fù)匹配,那么你就可能想要像我們剛剛展示的那樣尋找一個(gè)機(jī)會(huì)自動(dòng)地定位lastIndex篷扩。

粘性對(duì)比全局

一些讀者可能意識(shí)到兄猩,你可以使用全局匹配標(biāo)志位gexec(..)方法來模擬某些像lastIndex相對(duì)匹配的東西,就像這樣:

var re = /o+./g,        // <-- 看鉴未,`g`枢冤!
    str = "foot book more";

re.exec( str );         // ["oot"]
re.lastIndex;           // 4

re.exec( str );         // ["ook"]
re.lastIndex;           // 9

re.exec( str );         // ["or"]
re.lastIndex;           // 13

re.exec( str );         // null —— 沒有更多的匹配了!
re.lastIndex;           // 0 —— 現(xiàn)在重新開始铜秆!

雖然使用exec(..)g范例確實(shí)從lastIndex的當(dāng)前值開始它們的匹配淹真,而且也在每次匹配(或失敗)之后更新lastIndex连茧,但這與y的行為不是相同的東西核蘸。

注意前面代碼段中被第二個(gè)exec(..)調(diào)用匹配并找到的"ook",被定位在位置6啸驯,即便在這個(gè)時(shí)候lastIndex4(前一次匹配的末尾)客扎。為什么?因?yàn)檎缥覀兦懊嬷v過的罚斗,非粘性匹配可以在它們的匹配過程中自由地向前移動(dòng)徙鱼。一個(gè)粘性模式表達(dá)式在這里將會(huì)失敗,因?yàn)樗辉试S向前移動(dòng)针姿。

除了也許不被期望的向前移動(dòng)的匹配行為以外袱吆,使用g代替y的另一個(gè)缺點(diǎn)是,g改變了一些匹配方法的行為搓幌,比如str.match(re)杆故。

考慮如下代碼:

var re = /o+./g,        // <-- 看,`g`溉愁!
    str = "foot book more";

str.match( re );        // ["oot","ook","or"]

看到所有的匹配是如何一次性地被返回的嗎处铛?有時(shí)這沒問題饲趋,但有時(shí)這不是你想要的。

test(..)match(..)這樣的工具一起使用撤蟆,粘性標(biāo)志位y將給你一次一個(gè)的推進(jìn)式的匹配奕塑。只要保證每次匹配時(shí)lastIndex總是在正確的位置上就行!

錨定粘性

正如我們?cè)缦缺痪孢^的家肯,將粘性模式認(rèn)為是暗含著一個(gè)以^開頭的范例是不準(zhǔn)確的龄砰。在正則表達(dá)式中錨點(diǎn)^擁有獨(dú)特的含義,它 沒有 被粘性模式改變讨衣。^總是 一個(gè)指向輸入起點(diǎn)的錨點(diǎn)换棚,而且 以任何方式相對(duì)于lastIndex

在這個(gè)問題上反镇,除了糟糕/不準(zhǔn)確的文檔固蚤,一個(gè)在Firefox中進(jìn)行的老舊的前ES6粘性模式實(shí)驗(yàn)不幸地加深了這種困惑,它確實(shí) 曾經(jīng) 使^相對(duì)于lastIndex歹茶,所以這種行為曾經(jīng)存在了許多年夕玩。

ES6選擇不這么做。^在一個(gè)范例中絕對(duì)且唯一地意味著輸入的起點(diǎn)惊豺。

這樣的后果是燎孟,一個(gè)像/^foo/y這樣的范例將總是僅在一個(gè)字符串的開頭找到"foo"匹配,如果它被允許在那里匹配的話尸昧。如果lastIndex不是0揩页,匹配就會(huì)失敗〕勾牛考慮如下代碼:

var re = /^foo/y,
    str = "foo";

re.test( str );         // true
re.test( str );         // false
re.lastIndex;           // 0 —— 失敗之后被重置

re.lastIndex = 1;
re.test( str );         // false —— 由于定位而失敗
re.lastIndex;           // 0 —— 失敗之后被重置

底線:y^lastIndex > 0是一種不兼容的組合碍沐,它將總是導(dǎo)致失敗的匹配。

注意: 雖然y不會(huì)以任何方式改變^的含義衷蜓,但是多行模式m會(huì),這樣^就意味著輸入的起點(diǎn) 或者 一個(gè)換行之后的文本的起點(diǎn)尘喝。所以磁浇,如果你在一個(gè)范例中組合使用ym,你會(huì)在一個(gè)字符串中發(fā)現(xiàn)多個(gè)開始于^的匹配朽褪。但是要記字孟拧:因?yàn)樗恼承?code>y,將不得不在后續(xù)的每次匹配時(shí)確保lastIndex被置于正確的換行的位置(可能是通過匹配到行的末尾)缔赠,否者后續(xù)的匹配將不會(huì)執(zhí)行衍锚。

正則表達(dá)式flags

在ES6之前,如果你想要檢查一個(gè)正則表達(dá)式來看看它被施用了什么標(biāo)志位嗤堰,你需要將它們 —— 諷刺的是戴质,可能是使用另一個(gè)正則表達(dá)式 —— 從source屬性的內(nèi)容中解析出來,就像這樣:

var re = /foo/ig;

re.toString();          // "/foo/ig"

var flags = re.toString().match( /\/([gim]*)$/ )[1];

flags;                  // "ig"

在ES6中,你現(xiàn)在可以直接得到這些值告匠,使用新的flags屬性:

var re = /foo/ig;

re.flags;               // "gi"

雖然是個(gè)細(xì)小的地方戈抄,但是ES6規(guī)范要求表達(dá)式的標(biāo)志位以"gimuy"的順序羅列,無論原本的范例中是以什么順序指定的后专。這就是出現(xiàn)/ig"gi"的區(qū)別的原因划鸽。

是的,標(biāo)志位被指定和羅列的順序無所謂戚哎。

ES6的另一個(gè)調(diào)整是裸诽,如果你向構(gòu)造器RegExp(..)傳遞一個(gè)既存的正則表達(dá)式,它現(xiàn)在是flags敏感的:

var re1 = /foo*/y;
re1.source;                         // "foo*"
re1.flags;                          // "y"

var re2 = new RegExp( re1 );
re2.source;                         // "foo*"
re2.flags;                          // "y"

var re3 = new RegExp( re1, "ig" );
re3.source;                         // "foo*"
re3.flags;                          // "gi"

在ES6之前型凳,構(gòu)造re3將拋出一個(gè)錯(cuò)誤崭捍,但是在ES6中你可以在復(fù)制時(shí)覆蓋標(biāo)志位。

數(shù)字字面量擴(kuò)展

在ES5之前啰脚,數(shù)字字面量看起來就像下面的東西 —— 八進(jìn)制形式?jīng)]有被官方指定殷蛇,唯一被允許的是各種瀏覽器已經(jīng)實(shí)質(zhì)上達(dá)成一致的一種擴(kuò)展:

var dec = 42,
    oct = 052,
    hex = 0x2a;

注意: 雖然你用不同的進(jìn)制來指定一個(gè)數(shù)字,但是數(shù)字的數(shù)學(xué)值才是被存儲(chǔ)的東西橄浓,而且默認(rèn)的輸出解釋方式總是10進(jìn)制的粒梦。前面代碼段中的三個(gè)變量都在它們當(dāng)中存儲(chǔ)了值42

為了進(jìn)一步說明052是一種非標(biāo)準(zhǔn)形式擴(kuò)展荸实,考慮如下代碼:

Number( "42" );             // 42
Number( "052" );            // 52
Number( "0x2a" );           // 42

ES5繼續(xù)允許這種瀏覽器擴(kuò)展的八進(jìn)制形式(包括這樣的不一致性)匀们,除了在strict模式下,八進(jìn)制字面量(052)是不允許的准给。做出這種限制的主要原因是泄朴,許多開發(fā)者似乎習(xí)慣于下意識(shí)地為了將代碼對(duì)齊而在十進(jìn)制的數(shù)字前面前綴0,然后遭遇他們完全改變了數(shù)字的值的意外露氮!

ES6延續(xù)了除十進(jìn)制數(shù)字之外的數(shù)字字面量可以被表示的遺留的改變/種類∽婊遥現(xiàn)在有了一種官方的八進(jìn)制形式,一種改進(jìn)了的十六進(jìn)制形式畔规,和一種全新的二進(jìn)制形式局扶。由于Web兼容性的原因,在非strict模式下老式的八進(jìn)制形式052將繼續(xù)是合法的叁扫,但其實(shí)應(yīng)當(dāng)永遠(yuǎn)不再被使用了三妈。

這些是新的ES6數(shù)字字面形式:

var dec = 42,
    oct = 0o52,         // or `0O52` :(
    hex = 0x2a,         // or `0X2a` :/
    bin = 0b101010;     // or `0B101010` :/

唯一允許的小數(shù)形式是十進(jìn)制的。八進(jìn)制莫绣,十六進(jìn)制畴蒲,和二進(jìn)制都是整數(shù)形式。

而且所有這些形式的字符串表達(dá)形式都是可以被強(qiáng)制轉(zhuǎn)換/變換為它們的數(shù)字等價(jià)物的:

Number( "42" );         // 42
Number( "0o52" );       // 42
Number( "0x2a" );       // 42
Number( "0b101010" );   // 42

雖然嚴(yán)格來說不是ES6新增的对室,但一個(gè)鮮為人知的事實(shí)是你其實(shí)可以做反方向的轉(zhuǎn)換(好吧模燥,某種意義上的):

var a = 42;

a.toString();           // "42" —— 也可使用`a.toString( 10 )`
a.toString( 8 );        // "52"
a.toString( 16 );       // "2a"
a.toString( 2 );        // "101010"

事實(shí)上咖祭,以這種方你可以用從236的任何進(jìn)制表達(dá)一個(gè)數(shù)字,雖然你會(huì)使用標(biāo)準(zhǔn)進(jìn)制 —— 2涧窒,8心肪,10,和16 ——之外的情況非常少見纠吴。

Unicode

我只能說這一節(jié)不是一個(gè)窮盡了“關(guān)于Unicode你想知道的一切”的資料硬鞍。我想講解的是,你需要知道在ES6中對(duì)Unicode改變了什么戴已,但是我們不會(huì)比這深入太多固该。Mathias Bynens (http://twitter.com/mathias) 大量且出色地撰寫/講解了關(guān)于JS和Unicode (參見 https://mathiasbynens.be/notes/javascript-unicodehttp://fluentconf.com/javascript-html-2015/public/content/2015/02/18-javascript-loves-unicode)。

0x00000xFFFF范圍內(nèi)的Unicode字符包含了所有的標(biāo)準(zhǔn)印刷字符(以各種語言)糖儡,它們都是你可能看到過和互動(dòng)過的伐坏。這組字符被稱為 基本多文種平面(Basic Multilingual Plane (BMP))。BMP甚至包含像這個(gè)酷雪人一樣的有趣字符: ? (U+2603)握联。

在這個(gè)BMP集合之外還有許多擴(kuò)展的Unicode字符桦沉,它們的范圍一直到0x10FFFF。這些符號(hào)經(jīng)常被稱為 星形(astral) 符號(hào)金闽,這正是BMP之外的字符的16組 平面 (也就是纯露,分層/分組)的名稱。星形符號(hào)的例子包括?? (U+1D11E)和?? (U+1F4A9)代芜。

在ES6之前埠褪,JavaScript字符串可以使用Unicode轉(zhuǎn)義來指定Unicode字符,例如:

var snowman = "\u2603";
console.log( snowman );         // "?"

然而挤庇,\uXXXXUnicode轉(zhuǎn)義僅支持四個(gè)十六進(jìn)制字符钞速,所以用這種方式表示你只能表示BMP集合中的字符。要在ES6以前使用Unicode轉(zhuǎn)義表示一個(gè)星形字符嫡秕,你需要使用一個(gè) 代理對(duì)(surrogate pair) —— 基本上是兩個(gè)經(jīng)特殊計(jì)算的Unicode轉(zhuǎn)義字符放在一起渴语,被JS解釋為一個(gè)單獨(dú)星形字符:

var gclef = "\uD834\uDD1E";
console.log( gclef );           // "??"

在ES6中,我們現(xiàn)在有了一種Unicode轉(zhuǎn)義的新形式(在字符串和正則表達(dá)式中)淘菩,稱為Unicode 代碼點(diǎn)轉(zhuǎn)義

var gclef = "\u{1D11E}";
console.log( gclef );           // "??"

如你所見遵班,它的區(qū)別是出現(xiàn)在轉(zhuǎn)義序列中的{ },它允許轉(zhuǎn)義序列中包含任意數(shù)量的十六進(jìn)制字符潮改。因?yàn)槟阒恍枰鶄€(gè)就可以表示在Unicode中可能的最高代碼點(diǎn)(也就是,0x10FFFF)腹暖,所以這是足夠的汇在。

Unicode敏感的字符串操作

在默認(rèn)情況下,JavaScript字符串操作和方法對(duì)字符串值中的星形符號(hào)是不敏感的脏答。所以糕殉,它們獨(dú)立地處理每個(gè)BMP字符亩鬼,即便是可以組成一個(gè)單獨(dú)字符的兩半代理“⒌考慮如下代碼:

var snowman = "?";
snowman.length;                 // 1

var gclef = "??";
gclef.length;                   // 2

那么雳锋,我們?nèi)绾尾拍苷_地計(jì)算這樣的字符串的長(zhǎng)度呢?在這種場(chǎng)景下羡洁,下面的技巧可以工作:

var gclef = "??";

[...gclef].length;              // 1
Array.from( gclef ).length;     // 1

回想一下本章早先的“for..of循環(huán)”一節(jié)玷过,ES6字符串擁有內(nèi)建的迭代器。這個(gè)迭代器恰好是Unicode敏感的筑煮,這意味著它將自動(dòng)地把一個(gè)星形符號(hào)作為一個(gè)單獨(dú)的值輸出辛蚊。我們?cè)谝粋€(gè)數(shù)組字面量上使用擴(kuò)散操作符...,利用它創(chuàng)建了一個(gè)字符串符號(hào)的數(shù)組真仲。然后我們只需檢查這個(gè)結(jié)果數(shù)組的長(zhǎng)度袋马。ES6的Array.from(..)基本上與[...XYZ]做的事情相同,不過我們將在第六章中講解這個(gè)工具的細(xì)節(jié)秸应。

警告: 應(yīng)當(dāng)注意的是虑凛,相對(duì)地講,與理論上經(jīng)過優(yōu)化的原生工具/屬性將做的事情比起來软啼,僅僅為了得到一個(gè)字符串的長(zhǎng)度就構(gòu)建并耗盡一個(gè)迭代器在性能上的代價(jià)是高昂的桑谍。

不幸的是,完整的答案并不簡(jiǎn)單或直接焰宣。除了代理對(duì)(字符串迭代器可以搞定的)霉囚,一些特殊的Unicode代碼點(diǎn)有其他特殊的行為,解釋起來非常困難匕积。例如盈罐,有一組代碼點(diǎn)可以修改前一個(gè)相鄰的字符,稱為 組合變音符號(hào)(Combining Diacritical Marks)

考慮這兩個(gè)數(shù)組的輸出:

console.log( s1 );              // "é"
console.log( s2 );              // "é"

它們看起來一樣闪唆,但它們不是盅粪!這是我們?nèi)绾蝿?chuàng)建s1s2的:

var s1 = "\xE9",
    s2 = "e\u0301";

你可能猜到了,我們前面的length技巧對(duì)s2不管用:

[...s1].length;                 // 1
[...s2].length;                 // 2

那么我們能做什么悄蕾?在這種情況下票顾,我們可以使用ES6的String#normalize(..)工具,在查詢這個(gè)值的長(zhǎng)度前對(duì)它實(shí)施一個(gè) Unicode正規(guī)化操作

var s1 = "\xE9",
    s2 = "e\u0301";

s1.normalize().length;          // 1
s2.normalize().length;          // 1

s1 === s2;                      // false
s1 === s2.normalize();          // true

實(shí)質(zhì)上帆调,normalize(..)接受一個(gè)"e\u0301"這樣的序列奠骄,并把它正規(guī)化為\xE9。正規(guī)化甚至可以組合多個(gè)相鄰的組合符號(hào)番刊,如果存在適合他們組合的Unicode字符的話:

var s1 = "o\u0302\u0300",
    s2 = s1.normalize(),
    s3 = "?";

s1.length;                      // 3
s2.length;                      // 1
s3.length;                      // 1

s2 === s3;                      // true

不幸的是含鳞,這里的正規(guī)化也不完美。如果你有多個(gè)組合符號(hào)在修改一個(gè)字符芹务,你可能不會(huì)得到你所期望的長(zhǎng)度計(jì)數(shù)蝉绷,因?yàn)橐粋€(gè)被獨(dú)立定義的鸭廷,可以表示所有這些符號(hào)組合的正規(guī)化字符可能不存在。例如:

var s1 = "e\u0301\u0330";

console.log( s1 );              // "??"

s1.normalize().length;          // 2

你越深入這個(gè)兔子洞熔吗,你就越能理解要得到一個(gè)“長(zhǎng)度”的精確定義是很困難的辆床。我們?cè)谝曈X上看到的作為一個(gè)單獨(dú)字符繪制的東西 —— 更精確地說,它稱為一個(gè) 字形 —— 在程序處理的意義上不總是嚴(yán)格地關(guān)聯(lián)到一個(gè)單獨(dú)的“字符”上桅狠。

提示: 如果你就是想看看這個(gè)兔子洞有多深讼载,看看“字形群集邊界(Grapheme Cluster Boundaries)”算法(http://www.Unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)。

字符定位

與長(zhǎng)度的復(fù)雜性相似垂攘,“在位置2上的字符是什么维雇?”,這么問的意思究竟是什么晒他?前ES6的原生答案來自charAt(..)吱型,它不會(huì)遵守一個(gè)星形字符的原子性,也不會(huì)考慮組合符號(hào)陨仅。

考慮如下代碼:

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

console.log( s1 );              // "ab?d"
console.log( s2 );              // "ab?d"
console.log( s3 );              // "ab??d"

s1.charAt( 2 );                 // "c"
s2.charAt( 2 );                 // "?"
s3.charAt( 2 );                 // "" <-- 不可打印的代理字符
s3.charAt( 3 );                 // "" <-- 不可打印的代理字符

那么津滞,ES6會(huì)給我們Unicode敏感版本的charAt(..)嗎?不幸的是灼伤,不触徐。在本書寫作時(shí),在后ES6的考慮之中有一個(gè)這樣的工具的提案狐赡。

但是使用我們?cè)谇耙还?jié)探索的東西(當(dāng)然也帶著它的限制W拆摹),我們可以黑一個(gè)ES6的答案:

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

[...s1.normalize()][2];         // "?"
[...s2.normalize()][2];         // "?"
[...s3.normalize()][2];         // "??"

警告: 提醒一個(gè)早先的警告:在每次你想得到一個(gè)單獨(dú)的字符時(shí)構(gòu)建并耗盡一個(gè)迭代器……在性能上不是很理想颖侄。對(duì)此鸟雏,希望我們很快能在后ES6時(shí)代得到一個(gè)內(nèi)建的,優(yōu)化過的工具览祖。

那么charCodeAt(..)工具的Unicode敏感版本呢孝鹊?ES6給了我們codePointAt(..)

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

s1.normalize().codePointAt( 2 ).toString( 16 );
// "107"

s2.normalize().codePointAt( 2 ).toString( 16 );
// "107"

s3.normalize().codePointAt( 2 ).toString( 16 );
// "1d49e"

那么從另一個(gè)方向呢?String.fromCharCode(..)的Unicode敏感版本是ES6的String.fromCodePoint(..)

String.fromCodePoint( 0x107 );      // "?"

String.fromCodePoint( 0x1d49e );    // "??"

那么等一下展蒂,我們能組合String.fromCodePoint(..)codePointAt(..)來得到一個(gè)剛才的Unicode敏感charAt(..)的更好版本嗎又活?是的!

var s1 = "abc\u0301d",
    s2 = "ab\u0107d",
    s3 = "ab\u{1d49e}d";

String.fromCodePoint( s1.normalize().codePointAt( 2 ) );
// "?"

String.fromCodePoint( s2.normalize().codePointAt( 2 ) );
// "?"

String.fromCodePoint( s3.normalize().codePointAt( 2 ) );
// "??"

還有好幾個(gè)字符串方法我們沒有在這里講解锰悼,包括toUpperCase()柳骄,toLowerCase()substring(..)箕般,indexOf(..)夹界,slice(..),以及其他十幾個(gè)隘世。它們中沒有任何一個(gè)為了完全支持Unicode而被改變或增強(qiáng)過可柿,所以在處理含有星形符號(hào)的字符串是,你應(yīng)當(dāng)非常小心 —— 可能干脆回避它們丙者!

還有幾個(gè)字符串方法為了它們的行為而使用正則表達(dá)式复斥,比如replace(..)match(..)。值得慶幸的是械媒,ES6為正則表達(dá)式帶來了Unicode支持目锭,正如我們?cè)诒菊略缜暗摹癠nicode標(biāo)志”中講解過的那樣。

好了纷捞,就是這些痢虹!有了我們剛剛講過的各種附加功能,JavaScript的Unicode字符串支持要比前ES6時(shí)代好太多了(雖然還不完美)主儡。

Unicode標(biāo)識(shí)符名稱

Unicode還可以被用于標(biāo)識(shí)符名稱(變量奖唯,屬性,等等)糜值。在ES6之前丰捷,你可以通過Unicode轉(zhuǎn)義這么做,比如:

var \u03A9 = 42;

// 等同于:var Ω = 42;

在ES6中寂汇,你還可以使用前面講過的代碼點(diǎn)轉(zhuǎn)義語法:

var \u{2B400} = 42;

// 等同于:var ?? = 42;

關(guān)于究竟哪些Unicode字符被允許使用病往,有一組復(fù)雜的規(guī)則。另外骄瓣,有些字符只要不是標(biāo)識(shí)符名稱的第一個(gè)字符就允許使用停巷。

注意: 關(guān)于所有這些細(xì)節(jié),Mathias Bynens寫了一篇了不起的文章 (https://mathiasbynens.be/notes/javascript-identifiers-es6)榕栏。

很少有理由畔勤,或者是為了學(xué)術(shù)上的目的,才會(huì)在標(biāo)識(shí)符名稱中使用這樣不尋常的字符臼膏。你通常不會(huì)因?yàn)橐揽窟@些深?yuàn)W的功能編寫代碼而感到舒服硼被。

Symbol

在ES6中,長(zhǎng)久以來首次渗磅,有一個(gè)新的基本類型被加入到了JavaScript:symbol嚷硫。但是,與其他的基本類型不同始鱼,symbol沒有字面形式仔掸。

這是你如何創(chuàng)建一個(gè)symbol:

var sym = Symbol( "some optional description" );

typeof sym;     // "symbol"

一些要注意的事情是:

  • 你不能也不應(yīng)該將newSymbol(..)一起使用。它不是一個(gè)構(gòu)造器医清,你也不是在產(chǎn)生一個(gè)對(duì)象起暮。
  • 被傳入Symbol(..)的參數(shù)是可選的。如果傳入的話会烙,它應(yīng)當(dāng)是一個(gè)字符串负懦,為symbol的目的給出一個(gè)友好的描述筒捺。
  • typeof的輸出是一個(gè)新的值("symbol"),這是識(shí)別一個(gè)symbol的主要方法纸厉。

如果描述被提供的話系吭,它僅僅用于symbol的字符串化表示:

sym.toString();     // "Symbol(some optional description)"

與基本字符串值如何不是String的實(shí)例的原理很相似,symbol也不是Symbol的實(shí)例颗品。如果肯尺,由于某些原因,你想要為一個(gè)symbol值構(gòu)建一個(gè)封箱的包裝器對(duì)像躯枢,你可以做如下的事情:

sym instanceof Symbol;      // false

var symObj = Object( sym );
symObj instanceof Symbol;   // true

symObj.valueOf() === sym;   // true

注意: 在這個(gè)代碼段中的symObjsym是可以互換使用的则吟;兩種形式可以在symbol被用到的地方使用。沒有太多的理由要使用封箱的包裝對(duì)象形式(symObj)锄蹂,而不用基本類型形式(sym)氓仲。和其他基本類型的建議相似,使用sym而非symObj可能是最好的败匹。

一個(gè)symbol本身的內(nèi)部值 —— 稱為它的name —— 被隱藏在代碼之外而不能取得寨昙。你可以認(rèn)為這個(gè)symbol的值是一個(gè)自動(dòng)生成的,(在你的應(yīng)用程序中)獨(dú)一無二的字符串值掀亩。

但如果這個(gè)值是隱藏且不可取得的舔哪,那么擁有一個(gè)symbol還有什么意義?

一個(gè)symbol的主要意義是創(chuàng)建一個(gè)不會(huì)和其他任何值沖突的類字符串值槽棍。所以捉蚤,舉例來說,可以考慮將一個(gè)symbol用做表示一個(gè)事件的名稱的值:

const EVT_LOGIN = Symbol( "event.login" );

然后你可以在一個(gè)使用像"event.login"這樣的一般字符串字面量的地方使用EVT_LOGIN

evthub.listen( EVT_LOGIN, function(data){
    // ..
} );

其中的好處是炼七,EVT_LOGIN持有一個(gè)不能被其他任何值所(有意或無意地)重復(fù)的值缆巧,所以在哪個(gè)事件被分發(fā)或處理的問題上不可能存在任何含糊。

注意: 在前面的代碼段的幕后豌拙,幾乎可以肯定地認(rèn)為evthub工具使用了EVT_LOGIN參數(shù)值的symbol值作為某個(gè)跟蹤事件處理器的內(nèi)部對(duì)象的屬性/鍵陕悬。如果evthub需要將symbol值作為一個(gè)真實(shí)的字符串使用,那么它將需要使用String(..)或者toString(..)進(jìn)行明確強(qiáng)制轉(zhuǎn)換按傅,因?yàn)閟ymbol的隱含字符串強(qiáng)制轉(zhuǎn)換是不允許的捉超。

你可能會(huì)將一個(gè)symbol直接用做一個(gè)對(duì)象中的屬性名/鍵,如此作為一個(gè)你想將之用于隱藏或元屬性的特殊屬性唯绍。重要的是拼岳,要知道雖然你試圖這樣對(duì)待它,但是它 實(shí)際上 并不是隱藏或不可接觸的屬性况芒。

考慮這個(gè)實(shí)現(xiàn)了 單例 模式行為的模塊 —— 也就是惜纸,它僅允許自己被創(chuàng)建一次:

const INSTANCE = Symbol( "instance" );

function HappyFace() {
    if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];

    function smile() { .. }

    return HappyFace[INSTANCE] = {
        smile: smile
    };
}

var me = HappyFace(),
    you = HappyFace();

me === you;         // true

這里的symbol值INSTANCE是一個(gè)被靜態(tài)地存儲(chǔ)在HappyFace()函數(shù)對(duì)象上的特殊的,幾乎是隱藏的,類元屬性耐版。

替代性地祠够,它本可以是一個(gè)像__instance這樣的普通屬性,而且其行為將會(huì)是一模一樣的椭更。symbol的使用僅僅增強(qiáng)了程序元編程的風(fēng)格哪审,將這個(gè)INSTANCE屬性與其他普通的屬性間保持隔離。

Symbol注冊(cè)表

在前面幾個(gè)例子中使用symbol的一個(gè)微小的缺點(diǎn)是虑瀑,變量EVT_LOGININSTANCE不得不存儲(chǔ)在外部作用域中(甚至也許是全局作用域),或者用某種方法存儲(chǔ)在一個(gè)可用的公共位置滴须,這樣代碼所有需要使用這些symbol的部分都可以訪問它們舌狗。

為了輔助組織訪問這些symbol的代碼,你可以使用 全局symbol注冊(cè)表 來創(chuàng)建symbol扔水。例如:

const EVT_LOGIN = Symbol.for( "event.login" );

console.log( EVT_LOGIN );       // Symbol(event.login)

和:

function HappyFace() {
    const INSTANCE = Symbol.for( "instance" );

    if (HappyFace[INSTANCE]) return HappyFace[INSTANCE];

    // ..

    return HappyFace[INSTANCE] = { .. };
}

Symbol.for(..)查詢?nèi)謘ymbol注冊(cè)表來查看一個(gè)symbol是否已經(jīng)使用被提供的說明文本存儲(chǔ)過了痛侍,如果有就返回它。如果沒有魔市,就創(chuàng)建一個(gè)并返回主届。換句話說,全局symbol注冊(cè)表通過描述文本將symbol值看作它們本身的單例待德。

但這也意味著只要使用匹配的描述名君丁,你的應(yīng)用程序的任何部分都可以使用Symbol.for(..)從注冊(cè)表中取得symbol。

諷刺的是将宪,基本上symbol的本意是在你的應(yīng)用程序中取代 魔法字符串 的使用(被賦予了特殊意義的隨意的字符串值)绘闷。但是你正是在全局symbol注冊(cè)表中使用 魔法 描述字符串值來唯一識(shí)別/定位它們的!

為了避免意外的沖突较坛,你可能想使你的symbol描述十分獨(dú)特印蔗。這么做的一個(gè)簡(jiǎn)單的方法是在它們之中包含前綴/環(huán)境/名稱空間的信息。

例如丑勤,考慮一個(gè)像下面這樣的工具:

function extractValues(str) {
    var key = Symbol.for( "extractValues.parse" ),
        re = extractValues[key] ||
            /[^=&]+?=([^&]+?)(?=&|$)/g,
        values = [], match;

    while (match = re.exec( str )) {
        values.push( match[1] );
    }

    return values;
}

我們使用魔法字符串值"extractValues.parse"华嘹,因?yàn)樵谧?cè)表中的其他任何symbol都不太可能與這個(gè)描述相沖突。

如果這個(gè)工具的一個(gè)用戶想要覆蓋這個(gè)解析用的正則表達(dá)式法竞,他們也可以使用symbol注冊(cè)表:

extractValues[Symbol.for( "extractValues.parse" )] =
    /..some pattern../g;

extractValues( "..some string.." );

除了symbol注冊(cè)表在全局地存儲(chǔ)這些值上提供的協(xié)助以外耙厚,我們?cè)谶@里看到的一切其實(shí)都可以通過將魔法字符串"extractValues.parse"作為一個(gè)鍵,而不是一個(gè)symbol爪喘,來做到颜曾。這其中在元編程的層次上的改進(jìn)要多于在函數(shù)層次上的改進(jìn)。

你可能偶然會(huì)使用一個(gè)已經(jīng)被存儲(chǔ)在注冊(cè)表中的symbol值來查詢它底層存儲(chǔ)了什么描述文本(鍵)秉剑。例如泛豪,因?yàn)槟銦o法傳遞symbol值本身,你可能需要通知你的應(yīng)用程序的另一個(gè)部分如何在注冊(cè)表中定位一個(gè)symbol。

你可以使用Symbol.keyFor(..)取得一個(gè)被注冊(cè)的symbol描述文本(鍵):

var s = Symbol.for( "something cool" );

var desc = Symbol.keyFor( s );
console.log( desc );            // "something cool"

// 再次從注冊(cè)表取得symbol
var s2 = Symbol.for( desc );

s2 === s;                       // true

Symbols作為對(duì)象屬性

如果一個(gè)symbol被用作一個(gè)對(duì)象的屬性/鍵诡曙,它會(huì)被以一種特殊的方式存儲(chǔ)臀叙,以至這個(gè)屬性不會(huì)出現(xiàn)在這個(gè)對(duì)象屬性的普通枚舉中:

var o = {
    foo: 42,
    [ Symbol( "bar" ) ]: "hello world",
    baz: true
};

Object.getOwnPropertyNames( o );    // [ "foo","baz" ]

要取得對(duì)象的symbol屬性:

Object.getOwnPropertySymbols( o );  // [ Symbol(bar) ]

這表明一個(gè)屬性symbol實(shí)際上不是隱藏的或不可訪問的,因?yàn)槟憧偸强梢栽?code>Object.getOwnPropertySymbols(..)的列表中看到它价卤。

內(nèi)建Symbols

ES6帶來了好幾種預(yù)定義的內(nèi)建symbol劝萤,它們暴露了在JavaScript對(duì)象值上的各種元行為。然而慎璧,正如人們所預(yù)料的那樣床嫌,這些symbol 沒有 沒被注冊(cè)到全局symbol注冊(cè)表中。

取而代之的是胸私,它們作為屬性被存儲(chǔ)到了Symbol函數(shù)對(duì)象中厌处。例如,在本章早先的“for..of”一節(jié)中岁疼,我們介紹了值Symbol.iterator

var a = [1,2,3];

a[Symbol.iterator];         // native function

語言規(guī)范使用@@前綴注釋指代內(nèi)建的symbol阔涉,最常見的幾個(gè)是:@@iterator@@toStringTag捷绒,@@toPrimitive瑰排。還定義了幾個(gè)其他的symbol,雖然他們可能不那么頻繁地被使用暖侨。

注意: 關(guān)于這些內(nèi)建symbol如何被用于元編程的詳細(xì)信息椭住,參見第七章的“通用Symbol”。

復(fù)習(xí)

ES6給JavaScript增加了一堆新的語法形式它碎,有好多東西要學(xué)函荣!

這些東西中的大多數(shù)都是為了緩解常見編程慣用法中的痛點(diǎn)而設(shè)計(jì)的,比如為函數(shù)參數(shù)設(shè)置默認(rèn)值和將“剩余”的參數(shù)收集到一個(gè)數(shù)組中扳肛。解構(gòu)是一個(gè)強(qiáng)大的工具傻挂,用來更簡(jiǎn)約地表達(dá)從數(shù)組或嵌套對(duì)象的賦值。

雖然像箭頭函數(shù)=>這樣的特性看起來也都是關(guān)于更簡(jiǎn)短更好看的語法挖息,但是它們實(shí)際上擁有非常特殊的行為金拒,你應(yīng)當(dāng)在恰當(dāng)?shù)那闆r下有意地使用它們。

擴(kuò)展的Unicode支持套腹,新的正則表達(dá)式技巧绪抛,和新的symbol基本類型充實(shí)了ES6語法的發(fā)展演變。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末电禀,一起剝皮案震驚了整個(gè)濱河市幢码,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尖飞,老刑警劉巖症副,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件店雅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡贞铣,警方通過查閱死者的電腦和手機(jī)闹啦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辕坝,“玉大人窍奋,你說我怎么就攤上這事〗闯” “怎么了琳袄?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)圣贸。 經(jīng)常有香客問我挚歧,道長(zhǎng),這世上最難降的妖魔是什么吁峻? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮在张,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘帮匾。我一直安慰自己,他們只是感情好瘟斜,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著虽惭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芽唇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天匆笤,我揣著相機(jī)與錄音谱邪,去河邊找鬼。 笑死惦银,一個(gè)胖子當(dāng)著我的面吹牛末誓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播傀蚌,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼基显,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了善炫?” 一聲冷哼從身側(cè)響起撩幽,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎箩艺,沒想到半個(gè)月后窜醉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡艺谆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年榨惰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片静汤。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡琅催,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出虫给,到底是詐尸還是另有隱情藤抡,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布抹估,位于F島的核電站缠黍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏药蜻。R本人自食惡果不足惜瓷式,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望语泽。 院中可真熱鬧贸典,春花似錦、人聲如沸湿弦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔬充。三九已至饥漫,卻和暖如春罗标,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背竿拆。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工踊东, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喳钟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像笙各,于是被迫代替她去往敵國(guó)和親酪惭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容

  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持虏缸,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券刽辙,享受所有官網(wǎng)優(yōu)惠宰缤,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 2,902評(píng)論 0 16
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持慨灭,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券氧骤,享受所有官網(wǎng)優(yōu)惠筹陵,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 1,402評(píng)論 0 2
  • 特別說明并思,為便于查閱语稠,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 418評(píng)論 0 0
  • 情緒是信息宙暇,你的每一個(gè)感受都會(huì)告訴你發(fā)生了什么;情緒是信息型奥,情緒也是能量厢汹,很多的情緒幫助我們采取了很多行為谐宙,是我們...
    孫蘋閱讀 306評(píng)論 1 2
  • 我很想你忠荞,很想很想很想。 一直很想很想見你堂油,因?yàn)橄胍娔愠剖詨?mèng)中一直都有你的身影寓免。從一開始的隔個(gè)三兩周夢(mèng)中才會(huì)出現(xiàn)...
    垃圾人閱讀 268評(píng)論 0 0