你不懂JS:ES6與未來 第二章:語法(中)

官方中文版原文鏈接

感謝社區(qū)中各位的大力支持顷扩,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠羹膳,并抽取幸運(yùn)大獎(jiǎng):點(diǎn)擊這里領(lǐng)取

對(duì)象字面量擴(kuò)展

ES6給不起眼兒的{ .. }對(duì)象字面量增加了幾個(gè)重要的便利擴(kuò)展两嘴。

簡約屬性

你一定很熟悉用這種形式的對(duì)象字面量聲明:

var x = 2, y = 3,
    o = {
        x: x,
        y: y
    };

如果到處說x: x總是讓你感到繁冗,那么有個(gè)好消息箩绍。如果你需要定義一個(gè)名稱和詞法標(biāo)識(shí)符一致的屬性,你可以將它從x: x縮寫為x荞下×嫜。考慮如下代碼:

var x = 2, y = 3,
    o = {
        x,
        y
    };

簡約方法

本著與我們剛剛檢視的簡約屬性相同的精神,添附在對(duì)象字面量屬性上的函數(shù)也有一種便利簡約形式尖昏。

以前的方式:

var o = {
    x: function(){
        // ..
    },
    y: function(){
        // ..
    }
}

而在ES6中:

var o = {
    x() {
        // ..
    },
    y() {
        // ..
    }
}

警告: 雖然x() { .. }看起來只是x: function(){ .. }的縮寫,但是簡約方法有一種特殊行為构资,是它們對(duì)應(yīng)的老方式所不具有的抽诉;確切地說,是允許super(參見本章稍后的“對(duì)象super”)的使用吐绵。

Generator(見第四章)也有一種簡約方法形式:

var o = {
    *foo() { .. }
};

簡約匿名

雖然這種便利縮寫十分誘人迹淌,但是這其中有一個(gè)微妙的坑要小心。為了展示這一點(diǎn)己单,讓我們檢視一下如下的前ES6代碼唉窃,你可能會(huì)試著使用簡約方法來重構(gòu)它:

function runSomething(o) {
    var x = Math.random(),
        y = Math.random();

    return o.something( x, y );
}

runSomething( {
    something: function something(x,y) {
        if (x > y) {
            // 使用相互對(duì)調(diào)的`x`和`y`來遞歸地調(diào)用
            return something( y, x );
        }

        return y - x;
    }
} );

這段蠢代碼只是生成兩個(gè)隨機(jī)數(shù),然后用大的減去小的纹笼。但這里重要的不是它做的是什么纹份,而是它是如何被定義的。讓我把焦點(diǎn)放在對(duì)象字面量和函數(shù)定義上廷痘,就像我們?cè)谶@里看到的:

runSomething( {
    something: function something(x,y) {
        // ..
    }
} );

為什么我們同時(shí)說something:function something蔓涧?這不是冗余嗎?實(shí)際上笋额,不是元暴,它們倆被用于不同的目的。屬性something讓我們能夠調(diào)用o.something(..)兄猩,有點(diǎn)兒像它的公有名稱茉盏。但是第二個(gè)something是一個(gè)詞法名稱鉴未,使這個(gè)函數(shù)可以為了遞歸而從內(nèi)部引用它自己。

你能看出來為什么return something(y,x)這一行需要名稱something來引用這個(gè)函數(shù)嗎鸠姨?因?yàn)檫@里沒有對(duì)象的詞法名稱铜秆,要是有的話我們就可以說return o.something(y,x)或者其他類似的東西。

當(dāng)一個(gè)對(duì)象字面量的確擁有一個(gè)標(biāo)識(shí)符名稱時(shí)享怀,這其實(shí)是一個(gè)很常見的做法羽峰,比如:

var controller = {
    makeRequest: function(..){
        // ..
        controller.makeRequest(..);
    }
};

這是個(gè)好主意嗎?也許是添瓷,也許不是梅屉。你在假設(shè)名稱controller將總是指向目標(biāo)對(duì)象。但它也很可能不是 —— 函數(shù)makeRequest(..)不能控制外部的代碼鳞贷,因此不能強(qiáng)制你的假設(shè)一定成立坯汤。這可能會(huì)回過頭來咬到你。

另一些人喜歡使用this定義這樣的東西:

var controller = {
    makeRequest: function(..){
        // ..
        this.makeRequest(..);
    }
};

這看起來不錯(cuò)搀愧,而且如果你總是用controller.makeRequest(..)來調(diào)用方法的話它就應(yīng)該能工作惰聂。但現(xiàn)在你有一個(gè)this綁定的坑,如果你做這樣的事情的話:

btn.addEventListener( "click", controller.makeRequest, false );

當(dāng)然咱筛,你可以通過傳遞controller.makeRequest.bind(controller)作為綁定到事件上的處理器引用來解決這個(gè)問題搓幌。但是這很討厭 —— 它不是很吸引人。

或者要是你的內(nèi)部this.makeRequest(..)調(diào)用需要從一個(gè)嵌套的函數(shù)內(nèi)發(fā)起呢迅箩?你會(huì)有另一個(gè)this綁定災(zāi)難溉愁,人們經(jīng)常使用var self = this這種用黑科技解決,就像:

var controller = {
    makeRequest: function(..){
        var self = this;

        btn.addEventListener( "click", function(){
            // ..
            self.makeRequest(..);
        }, false );
    }
};

更討厭饲趋。

注意: 更多關(guān)于this綁定規(guī)則和陷阱的信息拐揭,參見本系列的 this與對(duì)象原型 的第一到二章。

好了奕塑,這些與簡約方法有什么關(guān)系堂污?回想一下我們的something(..)方法定義:

runSomething( {
    something: function something(x,y) {
        // ..
    }
} );

在這里的第二個(gè)something提供了一個(gè)超級(jí)便利的詞法標(biāo)識(shí)符,它總是指向函數(shù)自己龄砰,給了我們一個(gè)可用于遞歸盟猖,事件綁定/解除等等的完美引用 —— 不用亂搞this或者使用不可靠的對(duì)象引用。

太好了!

那么寝贡,現(xiàn)在我們?cè)囍鴮⒑瘮?shù)引用重構(gòu)為這種ES6解約方法的形式:

runSomething( {
    something(x,y) {
        if (x > y) {
            return something( y, x );
        }

        return y - x;
    }
} );

第一眼看上去不錯(cuò)扒披,除了這個(gè)代碼將會(huì)壞掉。return something(..)調(diào)用經(jīng)不會(huì)找到something標(biāo)識(shí)符圃泡,所以你會(huì)得到一個(gè)ReferenceError碟案。噢,但為什么颇蜡?

上面的ES6代碼段將會(huì)被翻譯為:

runSomething( {
    something: function(x,y){
        if (x > y) {
            return something( y, x );
        }

        return y - x;
    }
} );

仔細(xì)看价说。你看出問題了嗎辆亏?簡約方法定義暗指something: function(x,y)”钅浚看到我們依靠的第二個(gè)something是如何被省略的了嗎扮叨?換句話說,簡約方法暗指匿名函數(shù)表達(dá)式领迈。

對(duì)彻磁,討厭。

注意: 你可能認(rèn)為在這里=>箭頭函數(shù)是一個(gè)好的解決方案狸捅。但是它們也同樣不夠衷蜓,因?yàn)樗鼈円彩悄涿瘮?shù)表達(dá)式。我們將在本章稍后的“箭頭函數(shù)”中講解它們尘喝。

一個(gè)部分地補(bǔ)償了這一點(diǎn)的消息是磁浇,我們的簡約函數(shù)something(x,y)將不會(huì)是完全匿名的。參見第七章的“函數(shù)名”來了解ES6函數(shù)名稱的推斷規(guī)則朽褪。這不會(huì)在遞歸中幫到我們置吓,但是它至少在調(diào)試時(shí)有用處。

那么我們?cè)鯓涌偨Y(jié)簡約方法缔赠?它們簡短又甜蜜衍锚,而且很方便。但是你應(yīng)當(dāng)僅在你永遠(yuǎn)不需要將它們用于遞歸或事件綁定/解除時(shí)使用它們嗤堰。否則构拳,就堅(jiān)持使用你的老式something: function something(..)方法定義。

你的很多方法都將可能從簡約方法定義中受益梁棠,這是個(gè)非常好的消息!只要小心幾處未命名的災(zāi)難就好斗埂。

ES5 Getter/Setter

技術(shù)上講符糊,ES5定義了getter/setter字面形式,但是看起來它們沒有被太多地使用呛凶,這主要是由于缺乏轉(zhuǎn)譯器來處理這種新的語法(其實(shí)男娄,它是ES5中加入的唯一的主要新語法)。所以雖然它不是一個(gè)ES6的新特性漾稀,我們也將簡單地復(fù)習(xí)一下這種形式模闲,因?yàn)樗赡軙?huì)隨著ES6的向前發(fā)展而變得有用得多。

考慮如下代碼:

var o = {
    __id: 10,
    get id() { return this.__id++; },
    set id(v) { this.__id = v; }
}

o.id;           // 10
o.id;           // 11
o.id = 20;
o.id;           // 20

// 而:
o.__id;         // 21
o.__id;         // 還是 —— 21崭捍!

這些getter和setter字面形式也可以出現(xiàn)在類中尸折;參見第三章。

警告: 可能不太明顯殷蛇,但是setter字面量必須恰好有一個(gè)被聲明的參數(shù)实夹;省略它或羅列其他的參數(shù)都是不合法的語法橄浓。這個(gè)單獨(dú)的必須參數(shù) 可以 使用解構(gòu)和默認(rèn)值(例如,set id({ id: v = 0 }) { .. })亮航,但是收集/剩余...是不允許的(set id(...v) { .. })荸实。

計(jì)算型屬性名

你可能曾經(jīng)遇到過像下面的代碼段那樣的情況,你的一個(gè)或多個(gè)屬性名來自于某種表達(dá)式缴淋,因此你不能將它們放在對(duì)象字面量中:

var prefix = "user_";

var o = {
    baz: function(..){ .. }
};

o[ prefix + "foo" ] = function(..){ .. };
o[ prefix + "bar" ] = function(..){ .. };
..

ES6為對(duì)象字面定義增加了一種語法准给,它允許你指定一個(gè)應(yīng)當(dāng)被計(jì)算的表達(dá)式,其結(jié)果就是被賦值屬性名重抖÷兜考慮如下代碼:

var prefix = "user_";

var o = {
    baz: function(..){ .. },
    [ prefix + "foo" ]: function(..){ .. },
    [ prefix + "bar" ]: function(..){ .. }
    ..
};

任何合法的表達(dá)式都可以出現(xiàn)在位于對(duì)象字面定義的屬性名位置的[ .. ]內(nèi)部。

很有可能仇哆,計(jì)算型屬性名最經(jīng)常與Symbol(我們將在本章稍后的“Symbol”中講解)一起使用沦辙,比如:

var o = {
    [Symbol.toStringTag]: "really cool thing",
    ..
};

Symbol.toStringTag是一個(gè)特殊的內(nèi)建值,我們使用[ .. ]語法求值得到讹剔,所以我們可以將值"really cool thing"賦值給這個(gè)特殊的屬性名油讯。

計(jì)算型屬性名還可以作為簡約方法或簡約generator的名稱出現(xiàn):

var o = {
    ["f" + "oo"]() { .. }   // 計(jì)算型簡約方法
    *["b" + "ar"]() { .. }  // 計(jì)算型簡約generator
};

設(shè)置[[Prototype]]

我們不會(huì)在這里講解原型的細(xì)節(jié),所以關(guān)于它的更多信息延欠,參見本系列的 this與對(duì)象原型陌兑。

有時(shí)候在你聲明對(duì)象字面量的同時(shí)給它的[[Prototype]]賦值很有用。下面的代碼在一段時(shí)期內(nèi)曾經(jīng)是許多JS引擎的一種非標(biāo)準(zhǔn)擴(kuò)展由捎,但是在ES6中得到了標(biāo)準(zhǔn)化:

var o1 = {
    // ..
};

var o2 = {
    __proto__: o1,
    // ..
};

o2是用一個(gè)對(duì)象字面量聲明的兔综,但它也被[[Prototype]]鏈接到了o1。這里的__proto__屬性名還可以是一個(gè)字符串"__proto__"狞玛,但是要注意它 不能 是一個(gè)計(jì)算型屬性名的結(jié)果(參見前一節(jié))软驰。

客氣點(diǎn)兒說贺氓,__proto__是有爭議的管跺。在ES6中,它看起來是一個(gè)最終被很勉強(qiáng)地標(biāo)準(zhǔn)化了的十绑,幾十年前的自主擴(kuò)展功能硬鞍。實(shí)際上慧瘤,它屬于ES6的“Annex B”,這一部分羅列了JS感覺它僅僅為了兼容性的原因固该,而不得不標(biāo)準(zhǔn)化的東西锅减。

警告: 雖然我勉強(qiáng)贊同在一個(gè)對(duì)象字面定義中將__proto__作為一個(gè)鍵,但我絕對(duì)不贊同在對(duì)象屬性形式中使用它伐坏,就像o.__proto__怔匣。這種形式既是一個(gè)getter也是一個(gè)setter(同樣也是為了兼容性的原因),但絕對(duì)存在更好的選擇著淆。更多信息參見本系列的 this與對(duì)象原型劫狠。

對(duì)于給一個(gè)既存的對(duì)象設(shè)置[[Prototype]]拴疤,你可以使用ES6的工具Object.setPrototypeOf(..)《琅ⅲ考慮如下代碼:

var o1 = {
    // ..
};

var o2 = {
    // ..
};

Object.setPrototypeOf( o2, o1 );

注意: 我們將在第六章中再次討論Object呐矾。“Object.setPrototypeOf(..)靜態(tài)函數(shù)”提供了關(guān)于Object.setPrototypeOf(..)的額外細(xì)節(jié)懦砂。另外參見“Object.assign(..)靜態(tài)函數(shù)”來了解另一種將o2原型關(guān)聯(lián)到o1的形式蜒犯。

對(duì)象super

super通常被認(rèn)為是僅與類有關(guān)。然而荞膘,由于JS對(duì)象僅有原型而沒有類的性質(zhì)罚随,super是同樣有效的,而且在普通對(duì)象的簡約方法中行為幾乎一樣羽资。

考慮如下代碼:

var o1 = {
    foo() {
        console.log( "o1:foo" );
    }
};

var o2 = {
    foo() {
        super.foo();
        console.log( "o2:foo" );
    }
};

Object.setPrototypeOf( o2, o1 );

o2.foo();       // o1:foo
                // o2:foo

警告: super僅在簡約方法中允許使用淘菩,而不允許在普通的函數(shù)表達(dá)式屬性中。而且它還僅允許使用super.XXX形式(屬性/方法訪問)屠升,而不是super()形式潮改。

在方法o2.foo()中的super引用被靜態(tài)地鎖定在了o2,而且明確地說是o2[[Prototype]]腹暖。這里的super基本上是Object.getPrototypeOf(o2) —— 顯然被解析為o1 —— 這就是他如何找到并調(diào)用o1.foo()的汇在。

關(guān)于super的完整細(xì)節(jié),參見第三章的“類”脏答。

模板字面量

在這一節(jié)的最開始糕殉,我將不得不呼喚這個(gè)ES6特性的極其……誤導(dǎo)人的名稱,這要看在你的經(jīng)驗(yàn)中 模板(template) 一詞的含義是什么殖告。

許多開發(fā)者認(rèn)為模板是一段可復(fù)用的阿蝶,可重繪的文本,就像大多數(shù)模板引擎(Mustache黄绩,Handlebars赡磅,等等)提供的能力那樣。ES6中使用的 模板 一詞暗示著相似的東西宝与,就像一種聲明可以被重繪的內(nèi)聯(lián)模板字面量的方法。然而冶匹,這根本不是考慮這個(gè)特性的正確方式习劫。

所以,在我們繼續(xù)之前嚼隘,我把它重命名為它本應(yīng)被稱呼的名字:插值型字符串字面量(或者略稱為 插值型字面量)诽里。

你已經(jīng)十分清楚地知道了如何使用"'分隔符來聲明字符串字面量,而且你還知道它們不是(像有些語言中擁有的)內(nèi)容將被解析為插值表達(dá)式的 智能字符串飞蛹。

但是谤狡,ES6引入了一種新型的字符串字面量灸眼,使用反引號(hào)`作為分隔符。這些字符串字面量允許嵌入基本的字符串插值表達(dá)式墓懂,之后這些表達(dá)式自動(dòng)地被解析和求值焰宣。

這是老式的前ES6方式:

var name = "Kyle";

var greeting = "Hello " + name + "!";

console.log( greeting );            // "Hello Kyle!"
console.log( typeof greeting );     // "string"

現(xiàn)在,考慮這種新的ES6方式:

var name = "Kyle";

var greeting = `Hello ${name}!`;

console.log( greeting );            // "Hello Kyle!"
console.log( typeof greeting );     // "string"

如你所見捕仔,我們?cè)谝幌盗斜环g為字符串字面量的字符周圍使用了`..`匕积,但是${..}形式中的任何表達(dá)式都將立即內(nèi)聯(lián)地被解析和求值。稱呼這樣的解析和求值的高大上名詞就是 插值(interpolation)(比模板要準(zhǔn)確多了)榜跌。

被插值的字符串字面量表達(dá)式的結(jié)果只是一個(gè)老式的普通字符串闪唆,賦值給變量greeting

警告: typeof greeting == "string"展示了為什么不將這些實(shí)體考慮為特殊的模板值很重要钓葫,因?yàn)槟悴荒軐⑦@種字面量的未求值形式賦值給某些東西并復(fù)用它悄蕾。`..`字符串字面量在某種意義上更像是IIFE,因?yàn)樗詣?dòng)內(nèi)聯(lián)地被求值础浮。`..`字符串字面量的結(jié)果只不過是一個(gè)簡單的字符串帆调。

插值型字符串字面量的一個(gè)真正的好處是他們?cè)试S被分割為多行:

var text =
`Now is the time for all good men
to come to the aid of their
country!`;

console.log( text );
// Now is the time for all good men
// to come to the aid of their
// country!

在插值型字符串字面量中的換行將會(huì)被保留在字符串值中。

除非在字面量值中作為明確的轉(zhuǎn)義序列出現(xiàn)霸旗,回車字符\r(編碼點(diǎn)U+000D)的值或者回車+換行序列\r\n(編碼點(diǎn)U+000DU+000A)的值都會(huì)被泛化為一個(gè)換行字符\n(編碼點(diǎn)U+000A)贷帮。但不要擔(dān)心;這種泛化很少見而且很可能僅會(huì)在你將文本拷貝粘貼到JS文件中時(shí)才會(huì)發(fā)生诱告。

插值表達(dá)式

在一個(gè)插值型字符串字面量中撵枢,任何合法的表達(dá)式都被允許出現(xiàn)在${..}內(nèi)部,包括函數(shù)調(diào)用精居,內(nèi)聯(lián)函數(shù)表達(dá)式調(diào)用锄禽,甚至是另一個(gè)插值型字符串字面量!

考慮如下代碼:

function upper(s) {
    return s.toUpperCase();
}

var who = "reader";

var text =
`A very ${upper( "warm" )} welcome
to all of you ${upper( `${who}s` )}!`;

console.log( text );
// A very WARM welcome
// to all of you READERS!

當(dāng)我們組合變量who與字符串s時(shí)靴姿, 相對(duì)于who + "s"沃但,這里的內(nèi)部插值型字符串字面量`${who}s`更方便一些。有些情況下嵌套的插值型字符串字面量是有用的佛吓,但是如果你發(fā)現(xiàn)自己做這樣的事情太頻繁宵晚,或者發(fā)現(xiàn)你自己嵌套了好幾層時(shí),你就要小心一些维雇。

如果確實(shí)有這樣情況淤刃,你的字符串你值生產(chǎn)過程很可能可以從某些抽象中獲益。

警告: 作為一個(gè)忠告吱型,使用這樣的新發(fā)現(xiàn)的力量時(shí)要非常小心你代碼的可讀性逸贾。就像默認(rèn)值表達(dá)式和解構(gòu)賦值表達(dá)式一樣,僅僅因?yàn)槟? 做某些事情,并不意味著你 應(yīng)該 做這些事情铝侵。在使用新的ES6技巧時(shí)千萬不要做過了頭灼伤,使你的代碼比你或者你的其他隊(duì)友聰明。

表達(dá)式作用域

關(guān)于作用域的一個(gè)快速提醒是它用于解析表達(dá)式中的變量時(shí)咪鲜。我早先提到過一個(gè)插值型字符串字面量與IIFE有些相像狐赡,事實(shí)上這也可以考慮為作用域行為的一種解釋。

考慮如下代碼:

function foo(str) {
    var name = "foo";
    console.log( str );
}

function bar() {
    var name = "bar";
    foo( `Hello from ${name}!` );
}

var name = "global";

bar();                  // "Hello from bar!"

在函數(shù)bar()內(nèi)部嗜诀,字符串字面量`..`被表達(dá)的那一刻猾警,可供它查找的作用域發(fā)現(xiàn)變量的name的值為"bar"。既不是全局的name也不是foo(..)name隆敢。換句話說发皿,一個(gè)插值型字符串字面量在它出現(xiàn)的地方是詞法作用域的,而不是任何方式的動(dòng)態(tài)作用域拂蝎。

標(biāo)簽型模板字面量

再次為了合理性而重命名這個(gè)特性:標(biāo)簽型字符串字面量穴墅。

老實(shí)說,這是一個(gè)ES6提供的更酷的特性温自。它可能看起來有點(diǎn)兒奇怪玄货,而且也許一開始看起來一般不那么實(shí)用。但一旦你花些時(shí)間在它上面悼泌,標(biāo)簽型字符串字面量的用處可能會(huì)令你驚訝松捉。

例如:

function foo(strings, ...values) {
    console.log( strings );
    console.log( values );
}

var desc = "awesome";

foo`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]

讓我們花點(diǎn)兒時(shí)間考慮一下前面的代碼段中發(fā)生了什么。首先馆里,跳出來的最刺眼的東西就是foo`Everything...`;隘世。它看起來不像是任何我們?cè)?jīng)見過的東西。不是嗎鸠踪?

它實(shí)質(zhì)上是一種不需要( .. )的特殊函數(shù)調(diào)用丙者。標(biāo)簽 —— 在字符串字面量`..`之前的foo部分 —— 是一個(gè)應(yīng)當(dāng)被調(diào)用的函數(shù)的值。實(shí)際上营密,它可以是返回函數(shù)的任何表達(dá)式械媒,甚至是一個(gè)返回另一個(gè)函數(shù)的函數(shù)調(diào)用,就像:

function bar() {
    return function foo(strings, ...values) {
        console.log( strings );
        console.log( values );
    }
}

var desc = "awesome";

bar()`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]

但是當(dāng)作為一個(gè)字符串字面量的標(biāo)簽時(shí)评汰,函數(shù)foo(..)被傳入了什么纷捞?

第一個(gè)參數(shù)值 —— 我們稱它為strings —— 是一個(gè)所有普通字符串的數(shù)組(所有被插值的表達(dá)式之間的東西)。我們?cè)?code>strings數(shù)組中得到兩個(gè)值:"Everything is ""!"被去。

之后為了我們示例的方便兰绣,我們使用...收集/剩余操作符(見本章早先的“擴(kuò)散/剩余”部分)將所有后續(xù)的參數(shù)值收集到一個(gè)稱為values的數(shù)組中,雖說你本來當(dāng)然可以把它們留作參數(shù)strings后面單獨(dú)的命名參數(shù)编振。

被收集進(jìn)我們的values數(shù)組中的參數(shù)值,就是在字符串字面量中發(fā)現(xiàn)的,已經(jīng)被求過值的插值表達(dá)式的結(jié)果踪央。所以在我們的例子中values里唯一的元素顯然就是awesome臀玄。

你可以將這兩個(gè)數(shù)組考慮為:在values中的值原本是你拼接在stings的值之間的分隔符,而且如果你將所有的東西連接在一起畅蹂,你就會(huì)得到完整的插值字符串值健无。

一個(gè)標(biāo)簽型字符串字面量像是一個(gè)在插值表達(dá)式被求值之后,但是在最終的字符串被編譯之前的處理步驟液斜,允許你在從字面量中產(chǎn)生字符串的過程中進(jìn)行更多的控制累贤。

一般來說,一個(gè)字符串字面連標(biāo)簽函數(shù)(在前面的代碼段中是foo(..))應(yīng)當(dāng)計(jì)算一個(gè)恰當(dāng)?shù)淖址挡⒎祷厮倨幔阅憧梢允褂脴?biāo)簽型字符串字面量作為一個(gè)未打標(biāo)簽的字符串字面量來使用:

function tag(strings, ...values) {
    return strings.reduce( function(s,v,idx){
        return s + (idx > 0 ? values[idx-1] : "") + v;
    }, "" );
}

var desc = "awesome";

var text = tag`Everything is ${desc}!`;

console.log( text );            // Everything is awesome!

在這個(gè)代碼段中臼膏,tag(..)是一個(gè)直通操作,因?yàn)樗粚?shí)施任何特殊的修改示损,而只是使用reduce(..)來循環(huán)遍歷渗磅,并像一個(gè)未打標(biāo)簽的字符串字面量一樣,將stringsvalues拼接/穿插在一起检访。

那么實(shí)際的用法是什么始鱼?有許多高級(jí)的用法超出了我們要在這里討論的范圍。但這里有一個(gè)格式化美元數(shù)字的簡單想法(有些像基本的本地化):

function dollabillsyall(strings, ...values) {
    return strings.reduce( function(s,v,idx){
        if (idx > 0) {
            if (typeof values[idx-1] == "number") {
                // 看脆贵,也使用插值性字符串字面量医清!
                s += `$${values[idx-1].toFixed( 2 )}`;
            }
            else {
                s += values[idx-1];
            }
        }

        return s + v;
    }, "" );
}

var amt1 = 11.99,
    amt2 = amt1 * 1.08,
    name = "Kyle";

var text = dollabillsyall
`Thanks for your purchase, ${name}! Your
product cost was ${amt1}, which with tax
comes out to ${amt2}.`

console.log( text );
// Thanks for your purchase, Kyle! Your
// product cost was $11.99, which with tax
// comes out to $12.95.

如果在values數(shù)組中遇到一個(gè)number值,我們就在它前面放一個(gè)"$"并用toFixed(2)將它格式化為小數(shù)點(diǎn)后兩位有效卖氨。否則会烙,我們就不碰這個(gè)值而讓它直通過去。

原始字符串

在前一個(gè)代碼段中双泪,我們的標(biāo)簽函數(shù)接受的第一個(gè)參數(shù)值稱為strings持搜,是一個(gè)數(shù)組。但是有一點(diǎn)兒額外的數(shù)據(jù)被包含了進(jìn)來:所有字符串的原始未處理版本焙矛。你可以使用.raw屬性訪問這些原始字符串值葫盼,就像這樣:

function showraw(strings, ...values) {
    console.log( strings );
    console.log( strings.raw );
}

showraw`Hello\nWorld`;
// [ "Hello
// World" ]
// [ "Hello\nWorld" ]

原始版本的值保留了原始的轉(zhuǎn)義序列\n\n是兩個(gè)分離的字符),但處理過的版本認(rèn)為它是一個(gè)單獨(dú)的換行符村斟。但是贫导,早先提到的行終結(jié)符泛化操作,是對(duì)兩個(gè)值都實(shí)施的蟆盹。

ES6帶來了一個(gè)內(nèi)建函數(shù)孩灯,它可以用做字符串字面量的標(biāo)簽:String.raw(..)。它簡單地直通strings值的原始版本:

console.log( `Hello\nWorld` );
// Hello
// World

console.log( String.raw`Hello\nWorld` );
// Hello\nWorld

String.raw`Hello\nWorld`.length;
// 12

字符串字面量標(biāo)簽的其他用法包括國際化逾滥,本地化峰档,和許多其他的特殊處理。

箭頭函數(shù)

我們?cè)诒菊略缦冉佑|了函數(shù)中this綁定的復(fù)雜性,而且在本系列的 this與對(duì)象原型 中也以相當(dāng)?shù)钠v解過讥巡。理解普通函數(shù)中基于this的編程帶來的挫折是很重要的掀亩,因?yàn)檫@是ES6的新=>箭頭函數(shù)的主要?jiǎng)訖C(jī)。

作為與普通函數(shù)的比較欢顷,我們首先來展示一下箭頭函數(shù)看起來什么樣:

function foo(x,y) {
    return x + y;
}

// 對(duì)比

var foo = (x,y) => x + y;

箭頭函數(shù)的定義由一個(gè)參數(shù)列表(零個(gè)或多個(gè)參數(shù)槽棍,如果參數(shù)不是只有一個(gè),需要有一個(gè)( .. )包圍這些參數(shù))組成抬驴,緊跟著是一個(gè)=>符號(hào)炼七,然后是一個(gè)函數(shù)體。

所以布持,在前面的代碼段中豌拙,箭頭函數(shù)只是(x,y) => x + y這一部分,而這個(gè)函數(shù)的引用剛好被賦值給了變量foo鳖链。

函數(shù)體僅在含有多于一個(gè)表達(dá)式姆蘸,或者由一個(gè)非表達(dá)式語句組成時(shí)才需要用{ .. }括起來。如果僅含有一個(gè)表達(dá)式芙委,而且你省略了外圍的{ .. }逞敷,那么在這個(gè)表達(dá)式前面就會(huì)有一個(gè)隱含的return,就像前面的代碼段中展示的那樣灌侣。

這里是一些其他種類的箭頭函數(shù):

var f1 = () => 12;
var f2 = x => x * 2;
var f3 = (x,y) => {
    var z = x * 2 + y;
    y++;
    x *= 3;
    return (x + y + z) / 2;
};

箭頭函數(shù) 總是 函數(shù)表達(dá)式推捐;不存在箭頭函數(shù)聲明。而且很明顯它們都是匿名函數(shù)表達(dá)式 —— 它們沒有可以用于遞歸或者事件綁定/解除的命名引用 —— 但在第七章的“函數(shù)名”中將會(huì)講解為了調(diào)試的目的而存在的ES6函數(shù)名接口規(guī)則侧啼。

注意: 普通函數(shù)參數(shù)的所有功能對(duì)于箭頭函數(shù)都是可用的牛柒,包括默認(rèn)值,解構(gòu)痊乾,剩余參數(shù)皮壁,等等。

箭頭函數(shù)擁有漂亮哪审,簡短的語法蛾魄,這使得它們?cè)诒砻嫔峡雌饋韺?duì)于編寫簡潔代碼很有吸引力。確實(shí)湿滓,幾乎所有關(guān)于ES6的文獻(xiàn)(除了這個(gè)系列中的書目)看起來都立即將箭頭函數(shù)僅僅認(rèn)作“新函數(shù)”滴须。

這說明在關(guān)于箭頭函數(shù)的討論中,幾乎所有的例子都是簡短的單語句工具叽奥,比如那些作為回調(diào)傳遞給各種工具的箭頭函數(shù)扔水。例如:

var a = [1,2,3,4,5];

a = a.map( v => v * 2 );

console.log( a );               // [2,4,6,8,10]

在這些情況下,你的內(nèi)聯(lián)函數(shù)表達(dá)式很適合這種在一個(gè)單獨(dú)語句中快速計(jì)算并返回結(jié)果的模式朝氓,對(duì)于更繁冗的function關(guān)鍵字和語法來說箭頭函數(shù)確實(shí)看起來是一個(gè)很吸人魔市,而且輕量的替代品主届。

大多數(shù)人看著這樣簡潔的例子都傾向于發(fā)出“哦……!啊……待德!”的感嘆岂膳,就像我想象中你剛剛做的那樣!

然而我要警示你的是磅网,在我看來,使用箭頭函數(shù)的語法代替普通的筷屡,多語句函數(shù)涧偷,特別是那些可以被自然地表達(dá)為函數(shù)聲明的函數(shù),是某種誤用毙死。

回憶本章早前的字符串字面量標(biāo)簽函數(shù)dollabillsyall(..) —— 讓我們將它改為使用=>語法:

var dollabillsyall = (strings, ...values) =>
    strings.reduce( (s,v,idx) => {
        if (idx > 0) {
            if (typeof values[idx-1] == "number") {
                // look, also using interpolated
                // string literals!
                s += `$${values[idx-1].toFixed( 2 )}`;
            }
            else {
                s += values[idx-1];
            }
        }

        return s + v;
    }, "" );

在這個(gè)例子中燎潮,我做的唯一修改是刪除了functionreturn扼倘,和一些{ .. }确封,然后插入了=>和一個(gè)var。這是對(duì)代碼可讀性的重大改進(jìn)嗎再菊?呵呵爪喘。

實(shí)際上我會(huì)爭論,缺少return和外部的{ .. }在某種程度上模糊了這樣的事實(shí):reduce(..)調(diào)用是函數(shù)dollabillsyall(..)中唯一的語句纠拔,而且它的結(jié)果是這個(gè)調(diào)用的預(yù)期結(jié)果秉剑。另外,那些受過訓(xùn)練而習(xí)慣于在代碼中搜索function關(guān)鍵字來尋找作用域邊界的眼睛稠诲,現(xiàn)在需要搜索=>標(biāo)志侦鹏,在密集的代碼中這絕對(duì)會(huì)更加困難。

雖然不是一個(gè)硬性規(guī)則臀叙,但是我要說從=>箭頭函數(shù)轉(zhuǎn)換得來的可讀性略水,與被轉(zhuǎn)換的函數(shù)長度成反比。函數(shù)越長劝萤,=>能幫的忙越少渊涝;函數(shù)越短,=>的閃光之處就越多稳其。

我覺得這樣做更明智也更合理:在你需要短的內(nèi)聯(lián)函數(shù)表達(dá)式的地方采用=>驶赏,但保持你的一般長度的主函數(shù)原封不動(dòng)。

不只是簡短的語法既鞠,而是this

曾經(jīng)集中在=>上的大多數(shù)注意力都是它通過在你的代碼中除去function煤傍,return,和{ .. }來節(jié)省那些寶貴的擊鍵嘱蛋。

但是至此我們一直忽略了一個(gè)重要的細(xì)節(jié)蚯姆。我在這一節(jié)最開始的時(shí)候說過五续,=>函數(shù)與this綁定行為密切相關(guān)。事實(shí)上龄恋,=>箭頭函數(shù) 主要的設(shè)計(jì)目的 就是以一種特定的方式改變this的行為疙驾,解決在this敏感的編碼中的一個(gè)痛點(diǎn)。

節(jié)省擊鍵是掩人耳目的東西郭毕,至多是一個(gè)誤導(dǎo)人的配角它碎。

讓我們重溫本章早前的另一個(gè)例子:

var controller = {
    makeRequest: function(..){
        var self = this;

        btn.addEventListener( "click", function(){
            // ..
            self.makeRequest(..);
        }, false );
    }
};

我們使用了黑科技var self = this,然后引用了self.makeRequest(..)显押,因?yàn)樵谖覀儌鬟f給addEventListener(..)的回調(diào)函數(shù)內(nèi)部扳肛,this綁定將與makeRequest(..)本身中的this綁定不同。換句話說乘碑,因?yàn)?code>this綁定是動(dòng)態(tài)的挖息,我們通過self變量退回到了可預(yù)測的詞法作用域。

在這其中我們終于可以看到=>箭頭函數(shù)主要的設(shè)計(jì)特性了兽肤。在箭頭函數(shù)內(nèi)部套腹,this綁定不是動(dòng)態(tài)的,而是詞法的资铡。在前一個(gè)代碼段中电禀,如果我們?cè)诨卣{(diào)里使用一個(gè)箭頭函數(shù),this將會(huì)不出所料地成為我們希望它成為的東西害驹。

考慮如下代碼:

var controller = {
    makeRequest: function(..){
        btn.addEventListener( "click", () => {
            // ..
            this.makeRequest(..);
        }, false );
    }
};

前面代碼段的箭頭函數(shù)中的詞法this現(xiàn)在指向的值與外圍的makeRequest(..)函數(shù)相同鞭呕。換句話說,=>var self = this的語法上的替代品宛官。

var self = this(或者葫松,另一種選擇是,.bind(this)調(diào)用)通车紫矗可以幫忙的情況下腋么,=>箭頭函數(shù)是一個(gè)基于相同原則的很好的替代操作。聽起來很棒亥揖,是吧珊擂?

沒那么簡單。

如果=>取代var self = this.bind(this)可以工作费变,那么猜猜=>用于一個(gè) 不需要 var self = this就能工作的this敏感的函數(shù)會(huì)發(fā)生么摧扇?你可能會(huì)猜到它將會(huì)把事情搞砸。沒錯(cuò)挚歧。

考慮如下代碼:

var controller = {
    makeRequest: (..) => {
        // ..
        this.helper(..);
    },
    helper: (..) => {
        // ..
    }
};

controller.makeRequest(..);

雖然我們以controller.makeRequest(..)的方式進(jìn)行了調(diào)用扛稽,但是this.helper引用失敗了,因?yàn)檫@里的this沒有像平常那樣指向controller滑负。那么它指向哪里在张?它通過詞法繼承了外圍的作用域中的this用含。在前面的代碼段中,它是全局作用域帮匾,this指向了全局作用域啄骇。呃。

除了詞法的this以外瘟斜,箭頭函數(shù)還擁有詞法的arguments —— 它們沒有自己的arguments數(shù)組缸夹,而是從它們的上層繼承下來 —— 同樣還有詞法的supernew.target(參見第三章的“類”)。

所以螺句,關(guān)于=>在什么情況下合適或不合適明未,我們現(xiàn)在可以推論出一組更加微妙的規(guī)則:

  • 如果你有一個(gè)簡短的,單語句內(nèi)聯(lián)函數(shù)表達(dá)式壹蔓,它唯一的語句是某個(gè)計(jì)算后的值的return語句,并且 這個(gè)函數(shù)沒有在它內(nèi)部制造一個(gè)this引用猫态,并且 沒有自引用(遞歸佣蓉,事件綁定/解除),并且 你合理地預(yù)期這個(gè)函數(shù)絕不會(huì)變得需要this引用或自引用亲雪,那么你就可能安全地將它重構(gòu)為一個(gè)=>箭頭函數(shù)勇凭。
  • 如果你有一個(gè)內(nèi)部函數(shù)表達(dá)式,它依賴于外圍函數(shù)的var self = this黑科技或者.bind(this)調(diào)用來確保正確的this綁定义辕,那么這個(gè)內(nèi)部函數(shù)表達(dá)式就可能安全地變?yōu)橐粋€(gè)=>箭頭函數(shù)虾标。
  • 如果你有一個(gè)內(nèi)部函數(shù)表達(dá)式,它依賴于外圍函數(shù)的類似于var args = Array.prototype.slice.call(arguments)這樣的東西來制造一個(gè)arguments的詞法拷貝灌砖,那么這個(gè)內(nèi)部函數(shù)就可能安全地變?yōu)橐粋€(gè)=>箭頭函數(shù)璧函。
  • 對(duì)于其他的所有東西 —— 普通函數(shù)聲明,較長的多語句函數(shù)表達(dá)式基显,需要詞法名稱標(biāo)識(shí)符進(jìn)行自引用(遞歸等)的函數(shù)蘸吓,和任何其他不符合前述性質(zhì)的函數(shù) —— 你就可能應(yīng)當(dāng)避免=>函數(shù)語法。

底線:=>this撩幽,arguments库继,和super的詞法綁定有關(guān)。它們是ES6為了修正一些常見的問題而被有意設(shè)計(jì)的特性窜醉,而不是為了修正bug宪萄,怪異的代碼,或者錯(cuò)誤榨惰。

不要相信任何說=>主要是拜英,或者幾乎是,為了減少幾下?lián)翩I的炒作读串。無論你是省下還是浪費(fèi)了這幾下?lián)翩I聊记,你都應(yīng)當(dāng)確切地知道你打入的每個(gè)字母是為了做什么撒妈。

提示: 如果你有一個(gè)函數(shù),由于上述各種清楚的原因而不適合成為一個(gè)=>箭頭函數(shù)排监,但同時(shí)它又被聲明為一個(gè)對(duì)象字面量的一部分狰右,那么回想一下本章早先的“簡約方法”,它有簡短函數(shù)語法的另一種選擇舆床。

對(duì)于如何/為何選用一個(gè)箭頭函數(shù)棋蚌,如果你喜歡一個(gè)可視化的決策圖的話:

[圖片上傳失敗...(image-27a646-1515411105375)]

for..of循環(huán)

伴隨著我們熟知的JavaScriptforfor..in循環(huán),ES6增加了一個(gè)for..of循環(huán)挨队,它循環(huán)遍歷一組由一個(gè) 迭代器(iterator) 產(chǎn)生的值谷暮。

你使用for..of循環(huán)遍歷的值必須是一個(gè) 可迭代對(duì)象(iterable),或者它必須是一個(gè)可以被強(qiáng)制轉(zhuǎn)換/封箱(參見本系列的 類型與文法)為一個(gè)可迭代對(duì)象的值盛垦。一個(gè)可迭代對(duì)象只不過是一個(gè)可以生成迭代器的對(duì)象湿弦,然后由循環(huán)使用這個(gè)迭代器。

讓我們比較for..offor..in來展示它們的區(qū)別:

var a = ["a","b","c","d","e"];

for (var idx in a) {
    console.log( idx );
}
// 0 1 2 3 4

for (var val of a) {
    console.log( val );
}
// "a" "b" "c" "d" "e"

如你所見腾夯,for..in循環(huán)遍歷數(shù)組a中的鍵/索引颊埃,而for.of循環(huán)遍歷a中的值。

這是前面代碼段中for..of的前ES6版本:

var a = ["a","b","c","d","e"],
    k = Object.keys( a );

for (var val, i = 0; i < k.length; i++) {
    val = a[ k[i] ];
    console.log( val );
}
// "a" "b" "c" "d" "e"

而這是一個(gè)ES6版本的非for..of等價(jià)物蝶俱,它同時(shí)展示了手動(dòng)迭代一個(gè)迭代器(見第三章的“迭代器”):

var a = ["a","b","c","d","e"];

for (var val, ret, it = a[Symbol.iterator]();
    (ret = it.next()) && !ret.done;
) {
    val = ret.value;
    console.log( val );
}
// "a" "b" "c" "d" "e"

在幕后班利,for..of循環(huán)向可迭代對(duì)象要來一個(gè)迭代器(使用內(nèi)建的Symbol.iterator;參見第七章的“通用Symbols”)榨呆,然后反復(fù)調(diào)用這個(gè)迭代器并將它產(chǎn)生的值賦值給循環(huán)迭代的變量罗标。

在JavaScript標(biāo)準(zhǔn)的內(nèi)建值中,默認(rèn)為可迭代對(duì)象的(或提供可迭代能力的)有:

  • 數(shù)組
  • 字符串
  • Generators(見第三章)
  • 集合/類型化數(shù)組(見第五章)

警告: 普通對(duì)象默認(rèn)是不適用于for..of循環(huán)的积蜻。因?yàn)樗麄儧]有默認(rèn)的迭代器闯割,這是有意為之的,不是一個(gè)錯(cuò)誤竿拆。但是纽谒,我們不會(huì)進(jìn)一步探究這其中微妙的原因。在第三章的“迭代器”中如输,我們將看到如何為我們自己的對(duì)象定義迭代器鼓黔,這允許for..of遍歷任何對(duì)象來得到我們定義的一組值。

這是如何遍歷一個(gè)基本類型的字符串中的字符:

for (var c of "hello") {
    console.log( c );
}
// "h" "e" "l" "l" "o"

基本類型字符串"hello"被強(qiáng)制轉(zhuǎn)換/封箱為等價(jià)的String對(duì)象包裝器不见,它是默認(rèn)就是一個(gè)可迭代對(duì)象澳化。

for (XYZ of ABC)..中,XYZ子句既可以是一個(gè)賦值表達(dá)式也可以是一個(gè)聲明稳吮,這與forfor..in中相同的子句一模一樣缎谷。所以你可以做這樣的事情:

var o = {};

for (o.a of [1,2,3]) {
    console.log( o.a );
}
// 1 2 3

for ({x: o.a} of [ {x: 1}, {x: 2}, {x: 3} ]) {
  console.log( o.a );
}
// 1 2 3

與其他的循環(huán)一樣,使用break灶似,continue列林,return(如果是在一個(gè)函數(shù)中)瑞你,以及拋出異常,for..of循環(huán)可以被提前終止希痴。在任何這些情況下者甲,迭代器的return(..)函數(shù)(如果存在的話)都會(huì)被自動(dòng)調(diào)用,以便讓迭代器進(jìn)行必要的清理工作砌创。

注意: 可迭代對(duì)象與迭代器的完整內(nèi)容參見第三章的“迭代器”虏缸。

正則表達(dá)式擴(kuò)展

讓我們承認(rèn)吧:長久以來在JS中正則表達(dá)式都沒怎么改變過。所以一件很棒的事情是嫩实,在ES6中它們終于學(xué)會(huì)了一些新招數(shù)刽辙。我們將在這里簡要地講解一下新增的功能,但是正則表達(dá)式整體的話題是如此厚重甲献,以至于如果你需要復(fù)習(xí)一下的話你需要找一些關(guān)于它的專門章節(jié)/書籍(有許多T诅汀)。

Unicode標(biāo)志

我們將在本章稍后的“Unicode”一節(jié)中講解關(guān)于Unicode的更多細(xì)節(jié)晃洒。在此撵溃,我們將僅僅簡要地看一下ES6+正則表達(dá)式的新u標(biāo)志,它使這個(gè)正則表達(dá)式的Unicode匹配成為可能锥累。

JavaScript字符串通常被解釋為16位字符的序列,它們對(duì)應(yīng)于 基本多文種平面(Basic Multilingual Plane (BMP)) (http://en.wikipedia.org/wiki/Plane_%28Unicode%29)中的字符集歇。但是有許多UTF-16字符在這個(gè)范圍以外桶略,而且字符串可能含有這些多字節(jié)字符。

在ES6之前诲宇,正則表達(dá)式只能基于BMP字符進(jìn)行匹配际歼,這意味著在匹配時(shí)那些擴(kuò)展字符被看作是兩個(gè)分離的字符。這通常不理想姑蓝。

所以鹅心,在ES6中,u標(biāo)志告訴正則表達(dá)式使用Unicode(UTF-16)字符的解釋方式來處理字符串纺荧,這樣一來一個(gè)擴(kuò)展的字符將作為一個(gè)單獨(dú)的實(shí)體被匹配旭愧。

警告: 盡管名字的暗示是這樣,但是“UTF-16”并不嚴(yán)格地意味著16位≈嫦荆現(xiàn)代的Unicode使用21位输枯,而且像UTF-8和UTF-16這樣的標(biāo)準(zhǔn)大體上是指有多少位用于表示一個(gè)字符。

一個(gè)例子(直接從ES6語言規(guī)范中拿來的): ?? (G大調(diào)音樂符號(hào))是Unicode代碼點(diǎn)U+1D11E(0x1D11E)占贫。

如果這個(gè)字符出現(xiàn)在一個(gè)正則表達(dá)式范例中(比如/??/)桃熄,標(biāo)準(zhǔn)的BMP解釋方式將認(rèn)為它是需要被匹配的兩個(gè)字符(0xD834和0xDD1E)。但是ES6新的Unicode敏感模式意味著/??/u(或者Unicode的轉(zhuǎn)義形式/\u{1D11E}/u)將會(huì)把"??"作為一個(gè)單獨(dú)的字符在一個(gè)字符串中進(jìn)行匹配型奥。

你可能想知道為什么這很重要瞳收。在非Unicode的BMP模式下碉京,這個(gè)正則表達(dá)式范例被看作兩個(gè)分離的字符,但它仍然可以在一個(gè)含有"??"字符的字符串中找到匹配螟深,如果你試一下就會(huì)看到:

/??/.test( "??-clef" );         // true

重要的是匹配的長度谐宙。例如:

/^.-clef/ .test( "??-clef" );       // false
/^.-clef/u.test( "??-clef" );       // true

這個(gè)范例中的^.-clef說要在普通的"-clef"文本前面只匹配一個(gè)單獨(dú)的字符。在標(biāo)準(zhǔn)的BMP模式下血崭,這個(gè)匹配會(huì)失斘韵А(因?yàn)槭莾蓚€(gè)字符),但是在Unicode模式標(biāo)志位u打開的情況下夹纫,這個(gè)匹配會(huì)成功(一個(gè)字符)咽瓷。

另外一個(gè)重要的注意點(diǎn)是,u使像+*這樣的量詞實(shí)施于作為一個(gè)單獨(dú)字符的整個(gè)Unicode代碼點(diǎn)舰讹,而不僅僅是字符的 低端替代符(也就是符號(hào)最右邊的一半)茅姜。對(duì)于出現(xiàn)在字符類中的Unicode字符也是一樣,比如/[??-??]/u月匣。

注意: 還有許多關(guān)于u在正則表達(dá)式中行為的細(xì)節(jié)钻洒,對(duì)此Mathias Bynens(https://twitter.com/mathias)撰寫了大量的作品(https://mathiasbynens.be/notes/es6-unicode-regex)。

粘性標(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恰好位于正確的位置上。

這是一種可能的場景:如果你知道你關(guān)心的匹配總是會(huì)出現(xiàn)在一個(gè)數(shù)字(例如叉趣,0泞边,1020疗杉,等等)倍數(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í)際場景翩瓜∈馨猓考慮如下代碼:

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)志位辆毡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市甜害,隨后出現(xiàn)的幾起案子舶掖,更是在濱河造成了極大的恐慌,老刑警劉巖尔店,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眨攘,死亡現(xiàn)場離奇詭異,居然都是意外死亡嚣州,警方通過查閱死者的電腦和手機(jī)鲫售,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來避诽,“玉大人龟虎,你說我怎么就攤上這事∩陈” “怎么了鲤妥?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拱雏。 經(jīng)常有香客問我棉安,道長,這世上最難降的妖魔是什么铸抑? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任贡耽,我火速辦了婚禮,結(jié)果婚禮上鹊汛,老公的妹妹穿的比我還像新娘蒲赂。我一直安慰自己,他們只是感情好刁憋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布滥嘴。 她就那樣靜靜地躺著,像睡著了一般至耻。 火紅的嫁衣襯著肌膚如雪若皱。 梳的紋絲不亂的頭發(fā)上镊叁,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音走触,去河邊找鬼晦譬。 笑死,一個(gè)胖子當(dāng)著我的面吹牛互广,可吹牛的內(nèi)容都是我干的敛腌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼惫皱,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼迎瞧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起逸吵,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤凶硅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后扫皱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體足绅,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年韩脑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了氢妈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡段多,死狀恐怖首量,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情进苍,我是刑警寧澤加缘,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站觉啊,受9級(jí)特大地震影響拣宏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杠人,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一勋乾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗡善,春花似錦辑莫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蜒程,卻和暖如春绅你,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昭躺。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工忌锯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人领炫。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓偶垮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親帝洪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子似舵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

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

  • 特別說明,為便于查閱葱峡,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 450評(píng)論 0 0
  • 特別說明砚哗,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 418評(píng)論 0 0
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持砰奕,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券蛛芥,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 3,657評(píng)論 2 27
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持军援,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券仅淑,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 1,401評(píng)論 0 2
  • 木槿的槿/文 最近,《生命時(shí)報(bào)》一篇《性格決定你活多久》登上微博熱搜空厌。爭強(qiáng)好勝的人容易患高血壓庐船、心臟病、冠心渤案醉鳖;隨...
    木槿Sherry閱讀 1,921評(píng)論 1 21