第二章:语法 2

特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS

ES5 Getter/Setter

技术上讲,ES5定义了getter/setter字面形式,但是看起来它们没有被太多地使用,这主要是由于缺乏转译器来处理这种新的语法(其实,它是ES5中加入的唯一的主要新语法)。所以虽然它不是一个ES6的新特性,我们也将简单地复习一下这种形式,因为它可能会随着ES6的向前发展而变得有用得多。

考虑如下代码:

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字面形式也可以出现在类中;参见第三章。

警告: 可能不太明显,但是setter字面量必须恰好有一个被声明的参数;省略它或罗列其他的参数都是不合法的语法。这个单独的必须参数 可以 使用解构和默认值(例如,set id({ id: v = 0 }) { .. }),但是收集/剩余...是不允许的(set id(...v) { .. })。

计算型属性名

你可能曾经遇到过像下面的代码段那样的情况,你的一个或多个属性名来自于某种表达式,因此你不能将它们放在对象字面量中:

var prefix = "user_";

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

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

ES6为对象字面定义增加了一种语法,它允许你指定一个应当被计算的表达式,其结果就是被赋值属性名。考虑如下代码:

var prefix = "user_";

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

任何合法的表达式都可以出现在位于对象字面定义的属性名位置的[ .. ]内部。

很有可能,计算型属性名最经常与Symbol(我们将在本章稍后的“Symbol”中讲解)一起使用,比如:

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

Symbol.toStringTag是一个特殊的内建值,我们使用[ .. ]语法求值得到,所以我们可以将值"really cool thing"赋值给这个特殊的属性名。

计算型属性名还可以作为简约方法或简约generator的名称出现:

var o = {
    ["f" + "oo"]() { .. }   // 计算型简约方法
    *["b" + "ar"]() { .. }  // 计算型简约generator
};

设置[[Prototype]]

我们不会在这里讲解原型的细节,所以关于它的更多信息,参见本系列的 this与对象原型

有时候在你声明对象字面量的同时给它的[[Prototype]]赋值很有用。下面的代码在一段时期内曾经是许多JS引擎的一种非标准扩展,但是在ES6中得到了标准化:

var o1 = {
    // ..
};

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

o2是用一个对象字面量声明的,但它也被[[Prototype]]链接到了o1。这里的__proto__属性名还可以是一个字符串"__proto__",但是要注意它 不能 是一个计算型属性名的结果(参见前一节)。

客气点儿说,__proto__是有争议的。在ES6中,它看起来是一个最终被很勉强地标准化了的,几十年前的自主扩展功能。实际上,它属于ES6的“Annex B”,这一部分罗列了JS感觉它仅仅为了兼容性的原因,而不得不标准化的东西。

警告: 虽然我勉强赞同在一个对象字面定义中将__proto__作为一个键,但我绝对不赞同在对象属性形式中使用它,就像o.__proto__。这种形式既是一个getter也是一个setter(同样也是为了兼容性的原因),但绝对存在更好的选择。更多信息参见本系列的 this与对象原型

对于给一个既存的对象设置[[Prototype]],你可以使用ES6的工具Object.setPrototypeOf(..)。考虑如下代码:

var o1 = {
    // ..
};

var o2 = {
    // ..
};

Object.setPrototypeOf( o2, o1 );

注意: 我们将在第六章中再次讨论Object。“Object.setPrototypeOf(..)静态函数”提供了关于Object.setPrototypeOf(..)的额外细节。另外参见“Object.assign(..)静态函数”来了解另一种将o2原型关联到o1的形式。

对象super

super通常被认为是仅与类有关。然而,由于JS对象仅有原型而没有类的性质,super是同样有效的,而且在普通对象的简约方法中行为几乎一样。

考虑如下代码:

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仅在简约方法中允许使用,而不允许在普通的函数表达式属性中。而且它还仅允许使用super.XXX形式(属性/方法访问),而不是super()形式。

在方法o2.foo()中的super引用被静态地锁定在了o2,而且明确地说是o2[[Prototype]]。这里的super基本上是Object.getPrototypeOf(o2) —— 显然被解析为o1 —— 这就是他如何找到并调用o1.foo()的。

关于super的完整细节,参见第三章的“类”。

模板字面量

在这一节的最开始,我将不得不呼唤这个ES6特性的极其……误导人的名称,这要看在你的经验中 模板(template) 一词的含义是什么。

许多开发者认为模板是一段可复用的,可重绘的文本,就像大多数模板引擎(Mustache,Handlebars,等等)提供的能力那样。ES6中使用的 模板 一词暗示着相似的东西,就像一种声明可以被重绘的内联模板字面量的方法。然而,这根本不是考虑这个特性的正确方式。

所以,在我们继续之前,我把它重命名为它本应被称呼的名字:插值型字符串字面量(或者略称为 插值型字面量)。

你已经十分清楚地知道了如何使用"'分隔符来声明字符串字面量,而且你还知道它们不是(像有些语言中拥有的)内容将被解析为插值表达式的 智能字符串

但是,ES6引入了一种新型的字符串字面量,使用反引号`作为分隔符。这些字符串字面量允许嵌入基本的字符串插值表达式,之后这些表达式自动地被解析和求值。

这是老式的前ES6方式:

var name = "Kyle";

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

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

现在,考虑这种新的ES6方式:

var name = "Kyle";

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

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

如你所见,我们在一系列被翻译为字符串字面量的字符周围使用了`..`,但是${..}形式中的任何表达式都将立即内联地被解析和求值。称呼这样的解析和求值的高大上名词就是 插值(interpolation)(比模板要准确多了)。

被插值的字符串字面量表达式的结果只是一个老式的普通字符串,赋值给变量greeting

警告: typeof greeting == "string"展示了为什么不将这些实体考虑为特殊的模板值很重要,因为你不能将这种字面量的未求值形式赋值给某些东西并复用它。`..`字符串字面量在某种意义上更像是IIFE,因为它自动内联地被求值。`..`字符串字面量的结果只不过是一个简单的字符串。

插值型字符串字面量的一个真正的好处是他们允许被分割为多行:

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!

在插值型字符串字面量中的换行将会被保留在字符串值中。

除非在字面量值中作为明确的转义序列出现,回车字符\r(编码点U+000D)的值或者回车+换行序列\r\n(编码点U+000DU+000A)的值都会被泛化为一个换行字符\n(编码点U+000A)。但不要担心;这种泛化很少见而且很可能仅会在你将文本拷贝粘贴到JS文件中时才会发生。

插值表达式

在一个插值型字符串字面量中,任何合法的表达式都被允许出现在${..}内部,包括函数调用,内联函数表达式调用,甚至是另一个插值型字符串字面量!

考虑如下代码:

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!

当我们组合变量who与字符串s时, 相对于who + "s",这里的内部插值型字符串字面量`${who}s`更方便一些。有些情况下嵌套的插值型字符串字面量是有用的,但是如果你发现自己做这样的事情太频繁,或者发现你自己嵌套了好几层时,你就要小心一些。

如果确实有这样情况,你的字符串你值生产过程很可能可以从某些抽象中获益。

警告: 作为一个忠告,使用这样的新发现的力量时要非常小心你代码的可读性。就像默认值表达式和解构赋值表达式一样,仅仅因为你 做某些事情,并不意味着你 应该 做这些事情。在使用新的ES6技巧时千万不要做过了头,使你的代码比你或者你的其他队友聪明。

表达式作用域

关于作用域的一个快速提醒是它用于解析表达式中的变量时。我早先提到过一个插值型字符串字面量与IIFE有些相像,事实上这也可以考虑为作用域行为的一种解释。

考虑如下代码:

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!"

在函数bar()内部,字符串字面量`..`被表达的那一刻,可供它查找的作用域发现变量的name的值为"bar"。既不是全局的name也不是foo(..)name。换句话说,一个插值型字符串字面量在它出现的地方是词法作用域的,而不是任何方式的动态作用域。

标签型模板字面量

再次为了合理性而重命名这个特性:标签型字符串字面量

老实说,这是一个ES6提供的更酷的特性。它可能看起来有点儿奇怪,而且也许一开始看起来一般不那么实用。但一旦你花些时间在它上面,标签型字符串字面量的用处可能会令你惊讶。

例如:

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

var desc = "awesome";

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

让我们花点儿时间考虑一下前面的代码段中发生了什么。首先,跳出来的最刺眼的东西就是foo`Everything...`;。它看起来不像是任何我们曾经见过的东西。不是吗?

它实质上是一种不需要( .. )的特殊函数调用。标签 —— 在字符串字面量`..`之前的foo部分 —— 是一个应当被调用的函数的值。实际上,它可以是返回函数的任何表达式,甚至是一个返回另一个函数的函数调用,就像:

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

var desc = "awesome";

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

但是当作为一个字符串字面量的标签时,函数foo(..)被传入了什么?

第一个参数值 —— 我们称它为strings —— 是一个所有普通字符串的数组(所有被插值的表达式之间的东西)。我们在strings数组中得到两个值:"Everything is ""!"

之后为了我们示例的方便,我们使用...收集/剩余操作符(见本章早先的“扩散/剩余”部分)将所有后续的参数值收集到一个称为values的数组中,虽说你本来当然可以把它们留作参数strings后面单独的命名参数。

被收集进我们的values数组中的参数值,就是在字符串字面量中发现的,已经被求过值的插值表达式的结果。所以在我们的例子中values里唯一的元素显然就是awesome

你可以将这两个数组考虑为:在values中的值原本是你拼接在stings的值之间的分隔符,而且如果你将所有的东西连接在一起,你就会得到完整的插值字符串值。

一个标签型字符串字面量像是一个在插值表达式被求值之后,但是在最终的字符串被编译之前的处理步骤,允许你在从字面量中产生字符串的过程中进行更多的控制。

一般来说,一个字符串字面量标签函数(在前面的代码段中是foo(..))应当计算一个恰当的字符串值并返回它,所以你可以使用标签型字符串字面量作为一个未打标签的字符串字面量来使用:

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!

在这个代码段中,tag(..)是一个直通操作,因为它不实施任何特殊的修改,而只是使用reduce(..)来循环遍历,并像一个未打标签的字符串字面量一样,将stringsvalues拼接/穿插在一起。

那么实际的用法是什么?有许多高级的用法超出了我们要在这里讨论的范围。但这里有一个格式化美元数字的简单想法(有些像基本的本地化):

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数组中遇到一个number值,我们就在它前面放一个"$"并用toFixed(2)将它格式化为小数点后两位有效。否则,我们就不碰这个值而让它直通过去。

原始字符串

在前一个代码段中,我们的标签函数接受的第一个参数值称为strings,是一个数组。但是有一点儿额外的数据被包含了进来:所有字符串的原始未处理版本。你可以使用.raw属性访问这些原始字符串值,就像这样:

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

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

原始版本的值保留了原始的转义序列\n\n是两个分离的字符),但处理过的版本认为它是一个单独的换行符。但是,早先提到的行终结符泛化操作,是对两个值都实施的。

ES6带来了一个内建函数,它可以用做字符串字面量的标签:String.raw(..)。它简单地直通strings值的原始版本:

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

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

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

字符串字面量标签的其他用法包括国际化,本地化,和许多其他的特殊处理。

箭头函数

我们在本章早先接触了函数中this绑定的复杂性,而且在本系列的 this与对象原型 中也以相当的篇幅讲解过。理解普通函数中基于this的编程带来的挫折是很重要的,因为这是ES6的新=>箭头函数的主要动机。

作为与普通函数的比较,我们首先来展示一下箭头函数看起来什么样:

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

// 对比

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

箭头函数的定义由一个参数列表(零个或多个参数,如果参数不是只有一个,需要有一个( .. )包围这些参数)组成,紧跟着是一个=>符号,然后是一个函数体。

所以,在前面的代码段中,箭头函数只是(x,y) => x + y这一部分,而这个函数的引用刚好被赋值给了变量foo

函数体仅在含有多于一个表达式,或者由一个非表达式语句组成时才需要用{ .. }括起来。如果仅含有一个表达式,而且你省略了外围的{ .. },那么在这个表达式前面就会有一个隐含的return,就像前面的代码段中展示的那样。

这里是一些其他种类的箭头函数:

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;
};

箭头函数 总是 函数表达式;不存在箭头函数声明。而且很明显它们都是匿名函数表达式 —— 它们没有可以用于递归或者事件绑定/解除的命名引用 —— 但在第七章的“函数名”中将会讲解为了调试的目的而存在的ES6函数名接口规则。

注意: 普通函数参数的所有功能对于箭头函数都是可用的,包括默认值,解构,剩余参数,等等。

箭头函数拥有漂亮,简短的语法,这使得它们在表面上看起来对于编写简洁代码很有吸引力。确实,几乎所有关于ES6的文献(除了这个系列中的书目)看起来都立即将箭头函数仅仅认作“新函数”。

这说明在关于箭头函数的讨论中,几乎所有的例子都是简短的单语句工具,比如那些作为回调传递给各种工具的箭头函数。例如:

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

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

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

在这些情况下,你的内联函数表达式很适合这种在一个单独语句中快速计算并返回结果的模式,对于更繁冗的function关键字和语法来说箭头函数确实看起来是一个很吸人,而且轻量的替代品。

大多数人看着这样简洁的例子都倾向于发出“哦……!啊……!”的感叹,就像我想象中你刚刚做的那样!

然而我要警示你的是,在我看来,使用箭头函数的语法代替普通的,多语句函数,特别是那些可以被自然地表达为函数声明的函数,是某种误用。

回忆本章早前的字符串字面量标签函数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;
    }, "" );

在这个例子中,我做的唯一修改是删除了functionreturn,和一些{ .. },然后插入了=>和一个var。这是对代码可读性的重大改进吗?呵呵。

实际上我会争论,缺少return和外部的{ .. }在某种程度上模糊了这样的事实:reduce(..)调用是函数dollabillsyall(..)中唯一的语句,而且它的结果是这个调用的预期结果。另外,那些受过训练而习惯于在代码中搜索function关键字来寻找作用域边界的眼睛,现在需要搜索=>标志,在密集的代码中这绝对会更加困难。

虽然不是一个硬性规则,但是我要说从=>箭头函数转换得来的可读性,与被转换的函数长度成反比。函数越长,=>能帮的忙越少;函数越短,=>的闪光之处就越多。

我觉得这样做更明智也更合理:在你需要短的内联函数表达式的地方采用=>,但保持你的一般长度的主函数原封不动。

不只是简短的语法,而是this

曾经集中在=>上的大多数注意力都是它通过在你的代码中除去functionreturn,和{ .. }来节省那些宝贵的击键。

但是至此我们一直忽略了一个重要的细节。我在这一节最开始的时候说过,=>函数与this绑定行为密切相关。事实上,=>箭头函数 主要的设计目的 就是以一种特定的方式改变this的行为,解决在this敏感的编码中的一个痛点。

节省击键是掩人耳目的东西,至多是一个误导人的配角。

让我们重温本章早前的另一个例子:

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

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

我们使用了黑科技var self = this,然后引用了self.makeRequest(..),因为在我们传递给addEventListener(..)的回调函数内部,this绑定将与makeRequest(..)本身中的this绑定不同。换句话说,因为this绑定是动态的,我们通过self变量退回到了可预测的词法作用域。

在这其中我们终于可以看到=>箭头函数主要的设计特性了。在箭头函数内部,this绑定不是动态的,而是词法的。在前一个代码段中,如果我们在回调里使用一个箭头函数,this将会不出所料地成为我们希望它成为的东西。

考虑如下代码:

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

前面代码段的箭头函数中的词法this现在指向的值与外围的makeRequest(..)函数相同。换句话说,=>var self = this的语法上的替代品。

var self = this(或者,另一种选择是,.bind(this)调用)通常可以帮忙的情况下,=>箭头函数是一个基于相同原则的很好的替代操作。听起来很棒,是吧?

没那么简单。

如果=>取代var self = this.bind(this)可以工作,那么猜猜=>用于一个 不需要 var self = this就能工作的this敏感的函数会发生么?你可能会猜到它将会把事情搞砸。没错。

考虑如下代码:

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

controller.makeRequest(..);

虽然我们以controller.makeRequest(..)的方式进行了调用,但是this.helper引用失败了,因为这里的this没有像平常那样指向controller。那么它指向哪里?它通过词法继承了外围的作用域中的this。在前面的代码段中,它是全局作用域,this指向了全局作用域。呃。

除了词法的this以外,箭头函数还拥有词法的arguments —— 它们没有自己的arguments数组,而是从它们的上层继承下来 —— 同样还有词法的supernew.target(参见第三章的“类”)。

所以,关于=>在什么情况下合适或不合适,我们现在可以推论出一组更加微妙的规则:

  • 如果你有一个简短的,单语句内联函数表达式,它唯一的语句是某个计算后的值的return语句,并且 这个函数没有在它内部制造一个this引用,并且 没有自引用(递归,事件绑定/解除),并且 你合理地预期这个函数绝不会变得需要this引用或自引用,那么你就可能安全地将它重构为一个=>箭头函数。
  • 如果你有一个内部函数表达式,它依赖于外围函数的var self = this黑科技或者.bind(this)调用来确保正确的this绑定,那么这个内部函数表达式就可能安全地变为一个=>箭头函数。
  • 如果你有一个内部函数表达式,它依赖于外围函数的类似于var args = Array.prototype.slice.call(arguments)这样的东西来制造一个arguments的词法拷贝,那么这个内部函数就可能安全地变为一个=>箭头函数。
  • 对于其他的所有东西 —— 普通函数声明,较长的多语句函数表达式,需要词法名称标识符进行自引用(递归等)的函数,和任何其他不符合前述性质的函数 —— 你就可能应当避免=>函数语法。

底线:=>thisarguments,和super的词法绑定有关。它们是ES6为了修正一些常见的问题而被有意设计的特性,而不是为了修正bug,怪异的代码,或者错误。

不要相信任何说=>主要是,或者几乎是,为了减少几下击键的炒作。无论你是省下还是浪费了这几下击键,你都应当确切地知道你打入的每个字母是为了做什么。

提示: 如果你有一个函数,由于上述各种清楚的原因而不适合成为一个=>箭头函数,但同时它又被声明为一个对象字面量的一部分,那么回想一下本章早先的“简约方法”,它有简短函数语法的另一种选择。

对于如何/为何选用一个箭头函数,如果你喜欢一个可视化的决策图的话:

for..of循环

伴随着我们熟知的JavaScriptforfor..in循环,ES6增加了一个for..of循环,它循环遍历一组由一个 迭代器(iterator) 产生的值。

你使用for..of循环遍历的值必须是一个 可迭代对象(iterable),或者它必须是一个可以被强制转换/封箱(参见本系列的 类型与文法)为一个可迭代对象的值。一个可迭代对象只不过是一个可以生成迭代器的对象,然后由循环使用这个迭代器。

让我们比较for..offor..in来展示它们的区别:

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循环遍历数组a中的键/索引,而for.of循环遍历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"

而这是一个ES6版本的非for..of等价物,它同时展示了手动迭代一个迭代器(见第三章的“迭代器”):

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循环向可迭代对象要来一个迭代器(使用内建的Symbol.iterator;参见第七章的“通用Symbols”),然后反复调用这个迭代器并将它产生的值赋值给循环迭代的变量。

在JavaScript标准的内建值中,默认为可迭代对象的(或提供可迭代能力的)有:

  • 数组
  • 字符串
  • Generators(见第三章)
  • 集合/类型化数组(见第五章)

警告: 普通对象默认是不适用于for..of循环的。因为他们没有默认的迭代器,这是有意为之的,不是一个错误。但是,我们不会进一步探究这其中微妙的原因。在第三章的“迭代器”中,我们将看到如何为我们自己的对象定义迭代器,这允许for..of遍历任何对象来得到我们定义的一组值。

这是如何遍历一个基本类型的字符串中的字符:

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

基本类型字符串"hello"被强制转换/封箱为等价的String对象包装器,它是默认就是一个可迭代对象。

for (XYZ of ABC)..中,XYZ子句既可以是一个赋值表达式也可以是一个声明,这与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

与其他的循环一样,使用breakcontinuereturn(如果是在一个函数中),以及抛出异常,for..of循环可以被提前终止。在任何这些情况下,迭代器的return(..)函数(如果存在的话)都会被自动调用,以便让迭代器进行必要的清理工作。

注意: 可迭代对象与迭代器的完整内容参见第三章的“迭代器”。

正则表达式扩展

让我们承认吧:长久以来在JS中正则表达式都没怎么改变过。所以一件很棒的事情是,在ES6中它们终于学会了一些新招数。我们将在这里简要地讲解一下新增的功能,但是正则表达式整体的话题是如此厚重,以至于如果你需要复习一下的话你需要找一些关于它的专门章节/书籍(有许多!)。

Unicode标志

我们将在本章稍后的“Unicode”一节中讲解关于Unicode的更多细节。在此,我们将仅仅简要地看一下ES6+正则表达式的新u标志,它使这个正则表达式的Unicode匹配成为可能。

JavaScript字符串通常被解释为16位字符的序列,它们对应于 基本多文种平面(Basic Multilingual Plane (BMP)) (http://en.wikipedia.org/wiki/Plane_%28Unicode%29)中的字符。但是有许多UTF-16字符在这个范围以外,而且字符串可能含有这些多字节字符。

在ES6之前,正则表达式只能基于BMP字符进行匹配,这意味着在匹配时那些扩展字符被看作是两个分离的字符。这通常不理想。

所以,在ES6中,u标志告诉正则表达式使用Unicode(UTF-16)字符的解释方式来处理字符串,这样一来一个扩展的字符将作为一个单独的实体被匹配。

警告: 尽管名字的暗示是这样,但是“UTF-16”并不严格地意味着16位。现代的Unicode使用21位,而且像UTF-8和UTF-16这样的标准大体上是指有多少位用于表示一个字符。

一个例子(直接从ES6语言规范中拿来的): 𝄞 (G大调音乐符号)是Unicode代码点U+1D11E(0x1D11E)。

如果这个字符出现在一个正则表达式范例中(比如/𝄞/),标准的BMP解释方式将认为它是需要被匹配的两个字符(0xD834和0xDD1E)。但是ES6新的Unicode敏感模式意味着/𝄞/u(或者Unicode的转义形式/\u{1D11E}/u)将会把"𝄞"作为一个单独的字符在一个字符串中进行匹配。

你可能想知道为什么这很重要。在非Unicode的BMP模式下,这个正则表达式范例被看作两个分离的字符,但它仍然可以在一个含有"𝄞"字符的字符串中找到匹配,如果你试一下就会看到:

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

重要的是匹配的长度。例如:

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

这个范例中的^.-clef说要在普通的"-clef"文本前面只匹配一个单独的字符。在标准的BMP模式下,这个匹配会失败(因为是两个字符),但是在Unicode模式标志位u打开的情况下,这个匹配会成功(一个字符)。

另外一个重要的注意点是,u使像+*这样的量词实施于作为一个单独字符的整个Unicode代码点,而不仅仅是字符的 低端替代符(也就是符号最右边的一半)。对于出现在字符类中的Unicode字符也是一样,比如/[💩-💫]/u

注意: 还有许多关于u在正则表达式中行为的细节,对此Mathias Bynens(https://twitter.com/mathias)撰写了大量的作品(https://mathiasbynens.be/notes/es6-unicode-regex)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容