特別說明,為便于查閱察迟,文章轉(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ù)字(例如蚌卤,0
,10
贸宏,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 "
)之后廷痘,lastIndex
是7
,它已經(jīng)是開始下一次匹配"2. bar "
所需的位置了件已,如此類推笋额。
如果你要使用粘性模式y
進(jìn)行反復(fù)匹配,那么你就可能想要像我們剛剛展示的那樣尋找一個(gè)機(jī)會(huì)自動(dòng)地定位lastIndex
篷扩。
粘性對(duì)比全局
一些讀者可能意識(shí)到兄猩,你可以使用全局匹配標(biāo)志位g
和exec(..)
方法來模擬某些像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í)候lastIndex
是4
(前一次匹配的末尾)客扎。為什么?因?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è)范例中組合使用y
和m
,你會(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í)上咖祭,以這種方你可以用從2
到36
的任何進(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-unicode 和 http://fluentconf.com/javascript-html-2015/public/content/2015/02/18-javascript-loves-unicode)。
從0x0000
到0xFFFF
范圍內(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 ); // "?"
然而挤庇,\uXXXX
Unicode轉(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)建s1
和s2
的:
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)該將
new
與Symbol(..)
一起使用。它不是一個(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è)代碼段中的symObj
和sym
是可以互換使用的则吟;兩種形式可以在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_LOGIN
和INSTANCE
不得不存儲(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ā)展演變。