Chapter5-grammer

You Don't Know JS: Types & Grammar

Chapter 5: Grammar

我们要解决的最后一个主要话题是JavaScript的语言语法是如何工作的(也就是语法)。你可能认为自己知道如何写JS,但语言语法的各个部分存在很多细微差别导致混淆和误解,因此我们希望深入研究这些部分并搞清楚一些概念。

注意: “语法(grammar)”一词对读者来说可能比“语法(句法,syntax)”一词更不熟悉。在许多方面,它们是类似的术语,描述了语言如何工作的规则。存在微妙的差异,但它们对我们在这里的讨论大多无关紧要。JavaScript的语法是一种结构化的方式,用于描述语法(运算符,关键字等)如何组合成格式良好的有效程序。换句话说,讨论没有语法(grammar)的句法(syntax)会遗漏许多重要的细节。所以我们在本章中的重点是最准确地描述为语法,即使语言的原始句法是开发人员直接与之交互。

Statements & Expressions

开发人员认为术语“声明”和“表达”大致相同是很常见的。但是在这里我们需要区分这两者,因为我们的JS程序中存在一些非常重要的差异。

为了区分,让我们借用你可能更熟悉的术语:英语。

“句子”是表达思想的一个完整的词汇形式。由一个或多个“短语”组成,每个短语都可以用标点符号或连词(“和”,“或”等)连接。短语本身可以由较小的短语组成。有些短语是不完整的,自己完成不了多少,而其他短语可以独立存在。这些规则统称为英语语法(grammar)。

所以它与JavaScript语法一致。语句是句子,表达式是短语,运算符是连词/标点符号。

JS中的每个表达式都可以评估为单个特定值结果。例如:

var a = 3 * 6;
var b = a;
b;

在此片段中,3 * 6是表达式(计算值18)。但是第二行上的a也是表达式,就像第三行中的b一样。ab表达式都评估当时存储在那些变量中的值,也恰好是18

而且,三行中的每一行都是包含表达式的语句。var a = 3 * 6var b = a被称为"声明语句",因为它们都声明了一个变量(并且可选地为它赋值)。a = 3 * 6b =a(除去var)称为赋值表达式。

第三行只包含表达式b,但它本身也是一个声明(虽然不是一个非常有趣的声明!)。

这通常被称为“表达式声明”。

Statement Completion Values

这是一个鲜为人知的事实,语句都有完成值(即使该值只是undefined)。

你怎么才能看到一个语句的完成值呢?

最明显的答案是在浏览器的开发人员控制台中键入语句,因为当你执行它时,控制台默认会报告它执行的最新语句的完成值。

我们考虑var b = a。该声明的完成值是什么? b = a赋值表达式导致结果是分配的值(上面的18),但var语句本身结果是undefined。为什么?因为var语句是在规范中定义的。如果你把var a = 42;放入你的控制台,你会看到undefined的输出而不是42

注意: 从技术上讲,它比这复杂一点。在ES5规范第12.2节“变量声明”中,VariableDeclaration算法实际上确实返回一个值(一个包含声明的变量名称的string - 怪异,呵呵!),但是这个值基本上被VariableStatement算法吞噬了(除了for..in循环使用),它强制一个空的(也就是undefined的)完成值。

事实上,如果你在控制台(或在javascript环境REPL--read/evaluate/print/loop工具中)进行了大量的代码实验,那么你可能在许多不同的语句之后看到了undefined的报告,也许从来没有意识到为什么或那是什么。

但是控制台打印出的完成值不是我们可以在程序中使用的东西。那么我们如何捕获完成值呢?

这是一项更复杂的任务。在我们解释如何解释之前,让我们探讨一下 为什么 要这样做。

我们需要考虑其他类型的语句完成值。例如,任何常规{..}块都具有完成值,就是其最后包含的语句/表达式的完成值。

考虑下面的代码:

var b;

if (true) {
    b = 4 + 38;
}

如果你在你的控制台/ REPL中键入它,你可能会看到42的报告,因为42if块的完成值,它接受了它的最后赋值表达式语句b = 4 + 38的完成值。

换句话说,块的完成值类似于块中最后一个语句值的 隐式 返回。

注意: 这在概念上与CoffeeScript等语言相似,它具有与function中最后一个语句值相同的函数的隐式return值。

但是有一个明显的问题。这种代码不起作用:

var a, b;

a = if (true) {
    b = 4 + 38;
};

我们无法捕获语句的完成值,并以任何简单的句法/语法方式将其分配到另一个变量中(至少现在还没有!)。

所以,我们可以做什么?

注意: 仅用于演示目的 - 实际上不要在您的实际代码中执行以下操作!

我们可以使用备受诟病的eval(..)(有时发音为“evil”)函数来捕获此完成值。

var a, b;

a = eval( "if (true) { b = 4 + 38; }" );

a;    // 42

耶耶耶,这也太难看了。但是他工作了!它说明了语句完成值是一个真实的东西,不仅可以在我们的控制台中捕获,而且可以在我们的程序中捕获。

有一个名为“表达式”的ES7提案。以下是它的工作原理:

var a, b;

a = do {
    if (true) {
        b = 4 + 38;
    }
};

a;    // 42

do {..}表达式执行一个块(其中包含一个或多个语句),块内的最终语句完成值成为do表达式的完成值,然后可以将其分配给a,如上所示。

一般的想法是能够将语句视为表达式 - 它们可以显示在其他语句中 - 而无需将它们包装在内联函数表达式中并执行显式return..

目前,语句完成值只不过是一些琐事。但随着JS的发展,它们可能会扮演更重要的角色,并且希望do {..}表达式能够减少使用eval(..)之类的东西的诱惑。

警告: 重复我之前的警告:避免使用eval(..)。这是认真的。有关更多说明,请参阅本系列的"Scope & Closures"标题。

Expression Side Effects

大多数表达式没有副作用。例如:

var a = 2;
var b = a + 3;

表达式a + 3本身不具有副作用,例如改变a。它有一个结果,即5,结果在b = a + 3的语句中被赋值给b

具有(可能的)副作用的表达式的最常见示例是函数调用表达式:

function foo() {
    a = a + 1;
}

var a = 1;
foo();        // result: `undefined`, side effect: changed `a`

但是,还有其他副作用表达式。例如:

var a = 42;
var b = a++;

表达式a ++有两个不同的行为。首先,它返回a的当前值,即42(然后将其分配给b)。但接下来,它会改变a自身的值,将其递增1

var a = 42;
var b = a++;

a;    // 43
b;    // 42

许多开发人员会错误地认为b具有值43就像a一样。但是,混淆来自于没有充分考虑++运算符的副作用的时间。

++增量运算符和 - 减量运算符都是一元运算符(参见第4章),可以在后缀(“后”)位置或前缀(“前”)位置使用。

var a = 42;

a++;    // 42
a;        // 43

++a;    // 44
a;        // 44

++在前缀位置使用作为++ a时,其副作用(递增a)发生在从表达式返回值之前,而不是在使用++之后返回。

注意: 你认为++ a ++是合法的句法吗?如果你尝试它,你将收到ReferenceError错误,但为什么?因为副作用运算符需要变量引用来定位其副作用。对于++ a ++,首先评估a ++部分(因为运算符优先级 - 见下文),它在增量之前返回a的值。但后来它试图评估++ 42,它(如果你尝试的话)给出了相同的ReferenceError错误,因为++不能直接对像42这样的值产生副作用。

有时会错误地认为你可以通过将它包装在()对中来封装++的后效应,例如:

var a = 42;
var b = (a++);

a;    // 43
b;    // 42

不幸的是,()本身没有定义一个新的包装表达式,它将在a ++表达式的后副作用之后进行评估,正如我们所希望的那样。事实上,即使它确实如此,一个++首先返回42,除非你有另一个表达式在++的副作用之后重新评估a,你不会从该表达式得到43,所以b将不会被赋值43

但是有一个选项: , 声明系列逗号运算符。此运算符允许你将多个独立表达式语句串联到一个语句中:

var a = 42, b;
b = ( a++, a );

a;    // 43
b;    // 43

注意: (..)在这里包围a++, a是需要的。原因是运算符优先级,我们将在本章后面介绍。

表达式a++, a,表示第二个语句表达式在第一个a ++语句表达式的副作用之后被计算,这意味着它返回43值以赋值给b

副作用运算符的另一个例子是delete。正如我们在第2章中所示,delete用于从object或插槽中删除array中的属性。但它通常只是作为独立声明调用:

var obj = {
    a: 42
};

obj.a;            // 42
delete obj.a;    // true
obj.a;            // undefined

如果请求的操作有效/允许,则delete运算符的结果值为true,否则为false。但是运算符的副作用是它删除了属性(或数组槽)。

注意: 有效/允许是什么意思?不存在的属性或存在且可配置的属性(请参阅本系列的this&Object Prototypes标题的第3章)将从delete运算符返回true。否则,结果将为false或错误。

副作用运算符的最后一个例子是=赋值运算符,它可能同时是明显的和非显而易见的。

考虑下面的例子:

var a;

a = 42;        // 42
a;            // 42

看起来似乎=a = 42是表达式的副作用运算符。但是如果我们检查a = 42语句的结果值,它就是刚刚分配的值(42),因此将相同值赋值给a本质上是一个副作用。

提示: 关于副作用的相同推理适用于复合赋值运算符,如+ =- =等。例如, a = b += 2首先被作为b += 2处理(这个就是b = b + 2),然后将结果通过=赋值给a

这种赋值表达式(或语句)赋值的行为主要用于链式赋值,例如:

var a, b, c;

a = b = c = 42;

这里,c = 42被评估为42(具有将42分配给c的副作用),然后b = 42被评估为42(具有将42分配给b的副作用),并且最终评估a = 42(具有将42分配给a的副作用)。

警告: 开发人员使用链式赋值所犯的常见错误就像var a = b = 42。虽然这看起来像是同样的事情,但事实并非如此。如果在没有单独的var b(作用域内的某个地方)正式声明b的情况下发生该声明,那么var a = b = 42将不会直接声明b。根据strict模式,可能会抛出错误或创建全局的变量(请参阅本系列的“Scope & Closures”标题)。

另一种需要考虑的方案:

function vowels(str) {
    var matches;

    if (str) {
        // pull out all the vowels
        matches = str.match( /[aeiou]/g );

        if (matches) {
            return matches;
        }
    }
}

vowels( "Hello World" ); // ["e","o","o"]

这是有效的,许多开发人员比较喜欢这样做。但是使用我们利用赋值副作用的惯用语,我们可以通过将两个if语句组合成一个来简化:

function vowels(str) {
    var matches;

    // pull out all the vowels
    if (str && (matches = str.match( /[aeiou]/g ))) {
        return matches;
    }
}

vowels( "Hello World" ); // ["e","o","o"]

注意: ( .. )包围matches = str.match..是必要的。原因是运算符优先级,我们将在本章后面的“Operator Precedence”一节中介绍。

我更喜欢这种较短的风格,因为我认为这两个条件实际上是相关的,连在一起比分开要更清楚。但是和JS中的大多数风格选择一样,认为哪一个 更好 纯粹是个人意见。

Contextual Rules

JavaScript语法规则中有很多地方,相同的语法意味着不同的东西,这取决于它在何处/如何使用。这种事情,孤立地使用,会引起相当多的困惑。

我们不会在这里详尽地列出所有这些案例,而只是列举一些常见案例。

{ .. } Curly Braces

有两个主要的地方(还有更多来自JS的演变!),你的代码中会出现一对{..}花括号。我们来看看他们的每一个。

Object Literals

首先,作为object字面量:

// 假设这里有一个 `bar()` 函数被定义

var a = {
    foo: bar()
};

我们怎么知道这是一个object文字?因为{..}是一个被分配给a的值。

注意: a引用称为“l值”(又称左边值),因为它是赋值的目标。{..}对是一个“r值”(又名右边值),因为它仅用作值(在这种情况下作为赋值的来源)。

Labels

如果我们删除上述代码段的var a =部分会发生什么?

// 假设这里有一个 `bar()` 函数被定义

{
    foo: bar()
}

许多开发人员认为{..}只是一个独立的object字面量,不会被分配到任何地方。但它实际上完全不同。

这里,{ .. }只是一个常规的代码块。它在JavaScript中并不是非常惯用(在其他语言中更是如此!)拥有这样的独立{..}块,但它是完全有效的JS语法。结合let块级作用域声明时,它可能特别有用(请参阅本系列中的Scope&Closures标题)。

这里的{..}代码块在功能上几乎与附加到某个语句的代码块相同,如for / while循环,if条件等。

但是,如果它是一个正常的代码块,那么奇怪的是foo: bar()语法是什么,以及它是如何合法的?

这是因为JavaScript中有一个鲜为人知(并且坦率地说,不鼓励使用)的功能称为“带标签的语句”。foo是语句bar()的标签(已省略其尾随; -- 请参阅本章后面的“Automatic Semicolons”)。但标签声明的重点是什么?

如果JavaScript有一个goto语句,理论上你可以说goto foo并在代码中跳转到该位置。goto通常被认为是糟糕的编码习惯,因为它们使代码更难理解(也就是“意大利面条代码”),所以javascript没有一个通用的goto是一件非常好的事情。

但是,JS确实支持一种有限的特殊形式的goto:带标签的跳转。continuebreak语句都可以选择接受指定的标签,在这种情况下,程序流“跳转”类似于goto。考虑下面的代码:

// `foo` labeled-loop
foo: for (var i=0; i<4; i++) {
    for (var j=0; j<4; j++) {
        // whenever the loops meet, continue outer loop
        if (j == i) {
            // jump to the next iteration of
            // the `foo` labeled-loop
            continue foo;
        }

        // skip odd multiples
        if ((j * i) % 2 == 1) {
            // normal (non-labeled) `continue` of inner loop
            continue;
        }

        console.log( i, j );
    }
}
// 1 0
// 2 0
// 2 1
// 3 0
// 3 2

注意: continue foo并不意味着“转到'foo'标记的位置继续”,而是“继续使用下一次迭代标记'foo'的循环。”所以,它并不是一个任意的goto

如你所见,我们跳过了奇数倍3 1的迭代,但标签循环跳转也跳过迭代112 2

也许一个稍微有用的标记跳转形式是在内部循环内使用break __,你想要突破外部循环。

也许更有用的标记跳转形式是从一个内部循环中break__,在这个内部循环中,你希望从外部循环中断。如果没有标记的break,这种相同的逻辑有时会很难编写:

// `foo` labeled-loop
foo: for (var i=0; i<4; i++) {
    for (var j=0; j<4; j++) {
        if ((i * j) >= 3) {
            console.log( "stopping!", i, j );
            // break out of the `foo` labeled loop
            break foo;
        }

        console.log( i, j );
    }
}
// 0 0
// 0 1
// 0 2
// 0 3
// 1 0
// 1 1
// 1 2
// stopping! 1 3

注意: break foo并不意味着“转向'foo'标记的位置继续,”而是“跳出标记为'foo'的循环/块并继续它后面的部分。”不完全是传统意义上的goto,啊哈?

上述的非标记break选项可能需要涉及一个或多个函数,共享范围变量访问等。它很可能比标记break更令人困惑,因此使用带标签的break可能是更好的选择。

标签可以应用于非循环块,但只有break可以引用这样的非循环标签。可以在任何标记的块中执行标记的break___,但是你不能continue ___非循环标签,也不能从一个块中进行非标记的break

function foo() {
    // `bar` labeled-block
    bar: {
        console.log( "Hello" );
        break bar;
        console.log( "never runs" );
    }
    console.log( "World" );
}

foo();
// Hello
// World

标记的循环/块是非常罕见的,并且经常不被赞成。如果可能的话,最好避免使用它们;例如,使用函数调用而不是循环跳转。但也许有些情况可能会有用。如果你打算使用带标签的跳转,请务必使用大量注释记录你正在做的事情!

人们普遍认为JSON是JS的一个子集,所以一个JSON字符串(如{"a":42} - 注意到JSON要求的属性名称周围的引号!)被认为是一个有效的JavaScript程序。不对! 尝试将{"a":42}放入JS控制台,你将收到错误消息。

那是因为语句标签周围没有引号,所以"a"不是有效的标签,因此:不能在它之后。

因此,JSON确实是JS语法的一个子集,但JSON本身并不是有效的JS语法。

沿着这些方向的一个非常常见的误解是,如果你要将JS文件加载到只有JSON内容的<script src = ..>标记中(例如来自API调用),数据将被读取为有效的JavaScript,但程序无法访问。JSON-P(在函数调用中包装JSON数据的做法,如foo({"a": 42}))通常被称为通过将值发送到程序的一个函数来解决这种不可访问性。

不对! 完全有效的JSON值{"a": 42}本身实际上会抛出一个JS错误,因为它被解释为带有无效标签的语句块。但是foo({"a":42})是有效的js,因为在这里,{"a":42}是传递给foo(..)的对象字面量值。所以,正确地说,JSON-P使JSON成为有效的JS语法!

Blocks

另一个常被引用的JS问题(与强制相关 - 见第4章)是:

[] + {}; // "[object Object]"
{} + []; // 0

这似乎意味着+运算符根据第一个操作数是[]还是{}给出不同的结果。但这实际上与它无关!

在第一行,{}出现在+运算符的表达式中,因此被解释为实际值(空object)。第4章解释说[]被强制为"",因此{}也被强制转换为字符串值:"[object Object]"

但是在第二行,{}被解释为一个独立的{}空块(它什么都不做)。块不需要用分号来终止它们,所以这里缺少一个不是问题。最后,+ []是一个表达式,它 明确地强制 (见第4章)[]为一个number,即0值。

Object Destructuring

从ES6开始,您将看到另一个显示{..}对的地方是“解构分配”(有关更多信息,请参阅本系列的ES6和Beyond标题),特别是object解构。考虑:

function getData() {
    // ..
    return {
        a: 42,
        b: "foo"
    };
}

var { a, b } = getData();

console.log( a, b ); // 42 "foo"

你可能会说,var { a , b } = ..是ES6解构分配的一种形式,这大致相当于:

var res = getData();
var a = res.a;
var b = res.b;

注意: {a,b}实际上是{a:a,b:b}的ES6解构缩写,因此两者都有效,但预计较短的{a,b}将成为首选形式。

使用{..}对的对象解构也可以用于命名函数参数,对于同一种隐式对象属性赋值,它是语法糖:

function foo({ a, b, c }) {
    // no need for:
    // var a = obj.a, b = obj.b, c = obj.c
    console.log( a, b, c );
}

foo( {
    c: [1,2,3],
    a: 42,
    b: "foo"
} );    // 42 "foo" [1, 2, 3]

因此,我们使用{..}对的上下文完全决定了它们的含义,这说明了句法(syntax)和语法(grammer)之间的区别。理解这些细微差别以避免JS引擎的意外解释非常重要。

else if And Optional Blocks

人们普遍误解javascript有一个else if子句,因为你可以这样做:

if (a) {
    // ..
}
else if (b) {
    // ..
}
else {
    // ..
}

但是这里有一个隐藏的JS语法特征:这里没有else if。但ifelse语句如果只包含一个语句,则允许省略其附加块周围的{}。你之前见过很多次,就是:

if (a) doSomething( a );

许多JS风格指南都会坚持让你总是在一个语句块周围使用{},例如:

if (a) { doSomething( a ); }

但是,完全相同的语法规则适用于else子句,因此,可能你一直编码的else if形式实际 上被解析为:

if (a) {
    // ..
}
else {
    if (b) {
        // ..
    }
    else {
        // ..
    }
}

if (b) { .. } else { .. }是跟在else后面的单个语句,所以你可以将周围的{}放入或不放入。换句话说,当你使用else if时,你在技术上打破了常见的样式指南规则,只是使用单个if语句定义你的else

当然,else if惯用法非常普遍,导致缩进程度较低,因此具有吸引力。无论你采用哪种方式,只需在你自己的样式指南/规则中明确指出,并且不要假定像else if这样的东西是直接语法规则。

Operator Precedence

正如我们在第4章中所述,JavaScript的版本关于&&||有趣的是,他们选择并返回其中一个操作数,而不仅仅是得到truefalse。如果只有两个操作数和一个操作符,这很容易解释。

var a = 42;
var b = "foo";

a && b;    // "foo"
a || b;    // 42

但是当涉及两个操作符和三个操作数时呢?

var a = 42;
var b = "foo";
var c = [1,2,3];

a && b || c; // ???
a || b && c; // ???

要理解这些表达式导致的结果,我们需要了解当表达式中存在多个表达式时,运算符处理方式的规则。

这些规则称为“运算符优先级”。

我敢打赌,大多数读者都认为他们对运算符的优先权有很好的把握。但正如我们在本系列丛书中所涵盖的所有其他内容一样,我们将深入了解这种理解,看看它究竟是多么坚固,并希望在此过程中学到一些新的东西。

回想一下上面的例子:

var a = 42, b;
b = ( a++, a );

a;    // 43
b;    // 43

但是如果我们移除()会发生什么?

var a = 42, b;
b = a++, a;

a;    // 43
b;    // 42

等等!为什么这会改变分配给b的值?

因为,运算符的优先级低于=运算符。因此,b = a++, a被解释为(b = a++), a。因为(正如我们前面所解释的)a++有后副作用,所以给b赋的值是在++改变a之前的值42

这只是需要理解运算符优先级的简单问题。如果你打算使用,作为一个语句系列运算符,重要的是要知道它实际上具有最低优先级。其他所有运算符的绑定都将比,更紧密。

现在,回忆一下之前的这个例子:

if (str && (matches = str.match( /[aeiou]/g ))) {
    // ..
}

我们说赋值周围的()是必需的,但为什么呢?因为&&的优先级高于=,所以没有()强制绑定,表达式将被视为(str && matches) = str.match ...。但这将是一个错误,因为(str && matches)的结果不是一个变量,而是一个值(在这种情况下是undefined的),因此它不能是在=赋值的左侧!

好的,所以你可能认为你已经将这个运算符优先级搞懂了。

让我们继续讨论一个更复杂的例子(我们将在本章的下几部分中介绍),以真正测试你的理解:

var a = 42;
var b = "foo";
var c = false;

var d = a && b || c ? c || b ? a : c && b : a;

d;        // ??

好吧,邪恶,我承认了。没有人会像这样写一串表达式,对吧?可能不是,但我们将用它来检查将多个操作符链接在一起的各种问题,这是一项非常常见的任务。

上面的结果是42。但是这并没有什么意思,除非我们自己能找到这个答案,而不仅仅是将其插入到JS程序中让JavaScript对其进行排序。

让我们深入研究。

第一个问题 - 你甚至可能没想到 - 是,第一部分(a && b || c)是否表现得像(a && b) || ca && (b || c)?你知道吗?你能说服自己他们实际上是不同的吗?

(false && true) || true;    // true
false && (true || true);    // false

所以,有证据证明他们是不同的。但是,false && true || true有什么样的表现?答案:

false && true || true;        // true
(false && true) || true;    // true

所以我们有答案。首先评估&&运算符,然后评估||运算符。

但这只是因为从左到右的处理?让我们颠倒运算符的顺序:

true || false && false;        // true

(true || false) && false;    // false -- nope
true || (false && false);    // true -- winner, winner!

现在我们已经证明&&先被评估,然后是||,在这种情况下,这实际上与通常预期的从左到右的处理相反。

那是什么导致了这种行为? 运算符优先级。

每种语言都定义了自己的运算符优先级列表。令人沮丧的是,JS开发人员阅读了JS列表的是多么罕见。

如果你知道的话,上面的例子就不会让你困惑,因为你已经知道&&||更优先。但我敢打赌,一定有相当数量的读者必须稍微考虑一下。

注意: 不幸的是,JS规范并没有真正将其操作符优先列表放在一个方便的单一位置。你必须解析并理解所有的语法规则。因此,我们将尝试以更方便的格式布置更常见和有用的位。有关运算符优先级的完整列表,请参阅MDN站点上的“Operator Precedence”(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)。

Short Circuited

在第4章中,我们在旁注中提到了像&&||这样的运算符的“短路”特性。现在让我们再详细介绍一下。

对于&&||如果左侧操作数足以确定操作的结果,则 不会评估 右侧操作数。因此,名称“短路”(如果可能的话,它将采用早期的捷径)。

例如,使用a&& b,如果a是falsy的,则不会评估b,因为&&操作数的结果已经确定,所以没有必要费心去检查b。同样,使用a|| b,如果a是truthy的,操作数的结果已经确定,所以没有理由检查b

这种短路非常有用,通常用于:

function doSomething(opts) {
    if (opts && opts.cool) {
        // ..
    }
}

opts && opts.coolopts部分充当了一种防护,因为如果opts未设置(或不是object),则表达式opts.cool会抛出错误。opts测试失败加上短路意味着opts.cool甚至不会被评估,因此没有错误!

同样,你可以使用||短路:

function doSomething(opts) {
    if (opts.cache || primeCache()) {
        // ..
    }
}

在这里,我们首先检查opts.cache,如果它存在,我们不调用primeCache()函数,从而避免可能不必要的工作。

Tighter Binding

但是让我们把注意力转回到早期的复杂语句示例和所有链式运算符,特别是? :三元运算符部分。? :运算符具有比&&||更多或更少的优先级?

a && b || c ? c || b ? a : c && b : a

这更像是这样的:

a && b || (c ? c || (b ? a : c) && b : a)

或是这样的?

(a && b || c) ? (c || b) ? a : (c && b) : a

答案是第二个。但是为什么?

因为&&||更优先,并且||? :更优先。

所以,(a && b || c)首先在? :参与之前被评估了。另一种常见的解释方式是&&||“绑定更紧密”,而不是? :。如果相反,那么c ? c ...会更紧密的绑定,它会表现为(作为第一选择),如a && b || ( c ? c ..)

Associativity

所以,&&||运算符首先绑定,然后是? :运算符。但是同样优先的多个运算符呢?他们总是从左到右或从右到左处理?

通常,运算符是左关联的或右关联的,指的是 分组是从左进行还是从右进行。

值得注意的是,关联性与从左到右或从右到左处理不同。

但是,为什么处理是从左到右还是从右到左都很重要?因为表达式可能有副作用,例如函数调用:

var a = foo() && bar();

这里,foo()首先被评估,然后可能是bar(),这取决于foo()表达式的结果。如果bar()foo()之前被调用,肯定会得到不同的程序行为。

但是这种行为只是从左到右处理(JavaScript中的默认行为!) - 它与&&的关联性无关。在那个例子中,因为这里只有一个&&,因而没有相关的分组,所以关联性甚至没有发挥作用。

但是使用类似a && b && c的表达式,分组 隐式发生,这意味着将首先评估a && bb && c

从技术上讲,a && b && c将被处理为(a && b) && c,因为&&是左关联的(顺便说一下,||也是如此)。然而,右关联替代方法a && (b && c)的行为方式与此类似。对于相同的值,相同的表达式以相同的顺序计算。

注意: 如果假设&&是右关联的,那么它的处理方式与手动使用()创建像a && (b && c)这样的分组相同。但这仍然 不意味c将在b之前处理。相关性并不意味着从右到左的评估,它意味着从右到左的 分组 。无论哪种方式,无论分组/关联性如何,评估的严格排序将是a,然后是b,然后是c(又名从左到右)。

所以&&||是左联想的并不重要,除了准确讨论他们的定义。

但情况并非总是如此。根据左关联性和右关联性,一些运算符的行为会有很大不同。

考虑一下? :(“三元”或“条件”)运算符:

a ? b : c ? d : e;

? :是右关联的,所以哪个分组表示它将如何处理?

  • a ? b : (c ? d : e)

  • (a ? b : c) ? d : e

答案是a ? b : (c ? d : e)。与上的&&||不同,这里的右关联性实际上很重要,因为对于某些(但不是全部!)值的组合来说(a ? b : c) ? d : e的行为将会不同。

一个这样的例子:

true ? false : true ? true : true;        // false

true ? false : (true ? true : true);    // false
(true ? false : true) ? true : true;    // true

即使最终结果相同,更多细微差别潜伏在其他值组合中。考虑:

true ? false : true ? true : false;        // false

true ? false : (true ? true : false);    // false
(true ? false : true) ? true : false;    // false

从那种情况来看,相同的最终结果意味着分组没有实际意义。然而:

var a = true, b = false, c = true, d = true, e = false;

a ? b : (c ? d : e); // false, evaluates only `a` and `b`
(a ? b : c) ? d : e; // false, evaluates `a`, `b` AND `e`

那么,我们已经清楚地证明了这一点? :是右关联的,并且关于运算符与自身链接的行为方式,它实际上很重要。

右关联性(分组)的另一个例子是=运算符。回想一下本章前面的链式赋值示例:

var a, b, c;

a = b = c = 42;

我们之前断言a = b = c = 42是首先评估c = 42赋值,然后b = ..,最后a = ...,为什么?由于右关联性,它实际上像这样对待声明:a = (b = (c = 42))

还记得本章前面的运行复杂赋值表达式的示例吗?

var a = 42;
var b = "foo";
var c = false;

var d = a && b || c ? c || b ? a : c && b : a;

d;        // 42

有了我们的优先级和关联性知识,我们现在应该能够将代码分解为分组行为,如下所示:

((a && b) || c) ? ((c || b) ? a : (c && b)) : a

或者,如果将其缩进更容易理解:

(
  (a && b)
    ||
  c
)
  ?
(
  (c || b)
    ?
  a
    :
  (c && b)
)
  :
a

现在让我们来解决他:

  1. (a && b)就是"foo"

  2. "foo" || c就是"foo"

  3. 对于第一个?来说,"foo"是truthy的

  4. (c || b)"foo"

  5. 对于第二个?"foo"是truthy的

  6. a42

就是这样的,我们完成了!答案就是42,正如我们之前看到的那样。那真的不是很难,是吗?

Disambiguation

现在你应该更好地掌握运算符优先级(和关联性),并且更容易理解具有多个链式运算符的代码将如何表现。

但是一个重要的问题仍然存在:我们是否都应该编写理解代码的代码,并且完全依赖于运算符优先级/关联性的所有规则?我们是否应该在需要强制执行不同的处理绑定/顺序时使用()手动分组?

或者,另一方面,我们是否应该认识到即使这些规则实际上是可以学习的,但仍有足够的理由来忽略自动优先/关联性?如果是这样,我们是否应该总是使用()手动分组并消除对这些自动行为的依赖?

这场辩论具有高度的主观性,与第4章关于 隐性 强制的辩论非常对称。大多数开发人员对这两种辩论都有相同的看法:要么他们接受行为并且使用,要么他们放弃这两种行为并坚持手工/显性的惯用法。

当然,我不能像第四章那样,为读者明确地回答这个问题。。但是我向你展示了优点和缺点,并希望更深刻地理解能让你做出明智的决定,而不是跟风别人的决定。

在我看来,这里有一个重要的立场。我们应该将运算符优先级/关联性和()手动分组混合到我们的程序中 - 我在第4章中以相同的方式对 隐式 强制的健康/安全使用进行了论证,但肯定不会无限制地认可它。

例如, if(a && b && c)..对我来说完全没有问题,我就不会为了明确的强调关联性而去if((a && b) && c)..,因为我认为这是过度的冗长。

另一方面,如果我需要链接两个? :条件运算符在一起,我当然会使用()手动分组来清楚地说明我的逻辑是什么。

因此,我的建议类似于第4章:使用运算符优先级/关联性可以使代码更短更清晰,但在有助于创建清晰度和减少混淆的地方使用()手动分组。

Automatic Semicolons

ASI(自动分号插入)是指javascript假定;在JS程序的某些地方,即使你没有将其放在那里,他就会进行ASI。

为什么会这样做?因为如果你省略一个必要的;你的程序会失败。这是不可原谅的。ASI允许JS容忍某些地方;通常认为没有必要。

值得注意的是,ASI只会在换行符存在时生效。分号不会插入到行的中间。

基本上,如果JS解析器解析一个可能发生解析器错误的行(应该是缺失的;),并且它可以合理地插入;,那么它会这样做。插入什么是合理的?只有当某些语句的结尾和该行的换行符之间只有空白和/或注释时。

考虑下面的代码:

var a = 42, b
c;

JS应该将下一行的c作为var语句的一部分来处理吗?如果,bc之间的任何地方(甚至是另一条线),它肯定会是的。但由于没有一个,,JS反而假定有一个隐含的;(在换行符处)在b后面。因此c;被保留为独立的表达式语句。

同样的:

var a = 42, b = "foo";

a
b    // "foo"

这仍然是一个没有错误的有效程序,因为表达式语句也接受ASI。

在某些地方ASI很有帮助,比如:

var a = 42;

do {
    // ..
} while (a)    // <-- ; expected here!
a;

语法需要一个;do..while循环之后,但不是在whilefor循环之后。但大多数开发人员都不记得了!因此,ASI很有帮助地介入并插入一个。

正如我们在本章前面所述,语句块不需要;终止,所以不需要ASI:

var a = 42;

while (a) {
    // ..
} // <-- no ; expected here
a;

ASI介入的另一个主要案例是breakcontinuereturn和(es6)yield关键字:

function foo(a) {
    if (!a) return
    a *= 2;
    // ..
}

return语句不会作用于a * = 2表达式,因为ASI假设为;终止return声明。当然,return语句很容易跨多行,只是在return除了换行符之外没有任何其他内容。

function foo(a) {
    return (
        a * 2 + 3 / 12
    );
}

相同的推理适用于breakcontinueyield

Error Correction

JS社区中最激烈的 宗教战争 之一(除了制表符与空格之外)是否严重/完全依赖ASI。

大多数(但不是全部)分号是可选的,但是两个;for(..)..循环头中是必需的。

在本次辩论的专业方面,许多开发人员认为ASI是一种有用的机制,允许他们通过省略除严格要求的所有;(非常少)来编写更简洁(更“漂亮”)的代码。经常声称ASI使得很多;是可选的,所以一个 不带它们 而正确编写的程序与 带着它们 而正确编写的程序没什么区别。

在辩论的另一方面,许多其他开发人员将断言,有太多的地方可能是偶然的陷阱,特别是对于那些无意识的新的,经验不足的开发人员来说,这些地方的;意外插入会改变其含义。类似地,一些开发人员会争辩说,如果他们省略了分号,这完全是一个错误,他们希望他们的工具(linters等)能够在JS引擎纠正错误之前抓住它。

让我分享一下我的观点。严格阅读规范意味着ASI是一个“纠错”程序。你可能会问什么样的错误?具体来说,解析器错误。 换句话说,为了让解析器失败更少,ASI让它更宽容。

但宽容什么?在我看来,解析器错误 发生的唯一方法是给出一个不正确/错误的程序来解析。因此,虽然ASI正在严格的纠正解析器错误,但是它得到此类错误的唯一方法是 首先出现程序编写错误 - 省略语法规则要求的分号。

所以,更直言不讳地说,当我听到有人声称他们想要省略“可选的分号”时,我的大脑会将这种说法翻译成“我想编写破坏解析的程序,但仍然可以使用它”。

我发现这是一个可笑的立场,以及节省几次键盘敲击和让更“漂亮的代码”充其量只是软弱的论点。

此外,我不同意这与空格与制表符的争论是一样的——这纯粹是表面化的——但我认为这是一个基本问题,即编写符合语法要求的代码,而不依赖于语法异常的代码,而这些代码几乎不会滑过。

另一种看待它的方式是,依赖ASI本质上是把换行看作是重要的“空白”。像Python这样的其他语言具有真正重要的空白。但是就今天的JavaScript来说,认为它拥有有意义的换行真的合适吗?

我的看法:使用分号,只要你知道它们是“必需的”,并将你对ASI的假设限制在最低限度。

但是,不要只听我的话。早在2012年,JavaScript的创建者Brendan Eich(http://brendaneich.com/2012/04/the-infernal-semicolon/)表示如下:

这个故事的寓意:ASI(正式来说)是一种语法错误纠正程序。如果你开始编码就认为他好像是一个普遍的重要换行规则,那你就会遇到麻烦。 .. 如果回到1995年五月的那十天,我希望我使换行在JS中更有意义。注意不要使用ASI,就好像它给了JS重要的换行符。

Errors

与运行时期间发生的所有其他错误相比,JavaScript不仅具有不同的错误子类型(TypeErrorReferenceErrorSyntaxError等),而且语法定义了在编译时要强制执行的某些错误。

特别是,长期以来一直存在许多应该被捕获并报告为“早期错误”的特定条件(在编译期间)。任何直接语法错误都是早期错误(例如,a = ,),但语法也定义了语法上有效但不允许的东西。

由于你的代码尚未开始执行,因此try..catch无法捕获这些错误。他们将无法解析/编译你的程序。

提示: 规范中没有关于浏览器(和开发者工具)应该如何报告错误的具体要求。因此,你可能会在以下错误示例中看到浏览器之间的差异,报告的错误的特定子类型或包含的错误消息文本的内容。

一个简单的例子是在正则表达式字面量中使用语法。这里的JS语法没有任何问题,但无效的正则表达式会抛出一个早期错误:

var a = /+foo/;        // Error!

赋值的目标必须是标识符(或生成一个或多个标识符的ES6解构表达式),因此该位置中的42之类的值是非法的,可以立即报告:

var a;
42 = a;        // Error!

ES5的strict模式定义了更多的早期错误。例如,在strict模式中,函数参数名称不能重复:

function foo(a,b,a) { }                    // just fine

function bar(a,b,a) { "use strict"; }    // Error!

另一个strict模式早期错误是具有多个同名属性的对象字面量:

(function(){
    "use strict";

    var a = {
        b: 42,
        b: 43
    };            // Error!
})();

注意: 从语义上讲,这样的错误在技术上不是句法错误,而是更多的语法错误 - 上面的片段在语法上是有效的。但由于没有GrammarError类型,因此某些浏览器使用SyntaxError

Using Variables Too Early

ES6定义了一个名为TDZ("Temporal Dead Zone")的新概念(坦白地说容易混淆)。

TDZ指的是代码中还不能进行变量引用的地方,因为它还没有达到所需的初始化。

最明显的例子是使用ES6 let块级作用域:

{
    a = 2;        // ReferenceError!
    let a;
}

赋值a=2正在(它确实是在块范围{}中)在它被let a声明初始化之前访问a变量,所以它在tdz中表示a,并抛出一个错误。

有趣的是,虽然typeof有一个例外,对于未声明的变量是安全的(见第1章),但没有为TDZ的引用做出这样的安全例外:

{
    typeof a;    // undefined
    typeof b;    // ReferenceError! (TDZ)
    let b;
}

Function Arguments

使用ES6默认参数值是TDZ违规的另一个示例(请参阅本系列的ES6和Beyond标题):

var b = 3;

function foo( a = 42, b = a + b + 5 ) {
    // ..
}

赋值中的b引用将发生在参数b的TDZ中(不是请求外部b的引用),因此它将引发错误。但是,赋值中的a很好,因为到那时它已经经过参数a的TDZ。

使用ES6的默认参数值时,如果省略参数,或者在其位置传递undefined的值,则会将默认值应用于参数:

function foo( a = 42, b = a + 1 ) {
    console.log( a, b );
}

foo();                    // 42 43
foo( undefined );        // 42 43
foo( 5 );                // 5 6
foo( void 0, 7 );        // 42 7
foo( null );            // null 1

注意: null被强制转换为a + 1表达式中的0值。有关详细信息,请参阅第4章。

从ES6默认参数值的角度来看,省略参数和传递undefined的值之间没有区别。但是,有一种方法可以检测某些情况下的差异:

function foo( a = 42, b = a + 1 ) {
    console.log(
        arguments.length, a, b,
        arguments[0], arguments[1]
    );
}

foo();                    // 0 42 43 undefined undefined
foo( 10 );                // 1 10 11 10 undefined
foo( 10, undefined );    // 2 10 11 10 undefined
foo( 10, null );        // 2 10 null 10 null

即使默认参数值应用于ab参数,如果在这些插槽中没有传递参数,arguments数组也不会有数据。

相反,如果显式传递undefined参数,则该参数的arguments数组中将存在一个条目,但它将是undefined的, 与应用于同一插槽的命名参数的默认值不同。

虽然ES6默认参数值可以在arguments数组槽和相应的命名参数变量之间创建差异,但在ES5中,同样的不连续性也可能以复杂的方式出现:

function foo(a) {
    a = 42;
    console.log( arguments[0] );
}

foo( 2 );    // 42 (linked)
foo();        // undefined (not linked)

如果传递参数,则arguments槽和命名参数将链接到始终具有相同的值。如果省略参数,则不会发生此类链接。

但在strict模式下,无论如何都不存在联系:

function foo(a) {
    "use strict";
    a = 42;
    console.log( arguments[0] );
}

foo( 2 );    // 2 (not linked)
foo();        // undefined (not linked)

几乎可以肯定,依赖任何这样的链接都是一个坏主意,事实上,链接本身是一个泄漏的抽象,它公开了引擎的底层实现细节,而不是一个正确设计的特性。

使用arguments数组已被弃用(特别是支持ES6 ... rest参数 - 请参阅本系列的ES6和Beyond标题),但这并不意味着它一切都很糟糕。

在ES6之前,arguments是获取所有传递参数的数组以传递给其他函数的唯一方法,这证明是非常有用的。你还可以将命名参数与arguments数组混合并且是安全的,只要你遵循一个简单的规则:永远不要同时引用命名参数及其对应的arguments槽。

如果你避免这种不良做法,你将永远不会暴露泄漏的联系行为。

function foo(a) {
    console.log( a + arguments[1] ); // safe!
}

foo( 10, 32 );    // 42

try..finally

你可能熟悉try..catch块是如何工作的。但是你有没有停下来考虑可以与之配对的finally子句?事实上,你是否意识到try只需要catchfinally,但两者都可以在需要时出现。

finally子句中的代码总是运行(无论如何都会),它总是在try(和catch ,如果存在)完成之后运行, 并且在任何其他代码运行之前。从某种意义上说,你可以将finally子句中的代码视为在回调函数中,无论块的其余部分如何运行,都会始终调用该函数。

那么如果在try子句中有一个return语句会发生什么?它显然会返回一个值,对吗?但接收该值的调用代码是在finally之前还是之后运行?

function foo() {
    try {
        return 42;
    }
    finally {
        console.log( "Hello" );
    }

    console.log( "never runs" );
}

console.log( foo() );
// Hello
// 42

return 42立即运行,他设置了foo()调用的完成值。此操作完成try子句,接下来finally子句立即运行。只有这样才能完成foo()函数,以便返回其完成值以供console.log(..)语句使用。

trythrow具有相同的行为:

function foo() {
    try {
        throw 42;
    }
    finally {
        console.log( "Hello" );
    }

    console.log( "never runs" );
}

console.log( foo() );
// Hello
// Uncaught Exception: 42

现在,如果在finally子句中抛出(意外或有意)异常,它将覆盖该函数的主要完成。如果try块中的先前return已为该函数设置了完成值,则该值将被舍弃。

function foo() {
    try {
        return 42;
    }
    finally {
        throw "Oops!";
    }

    console.log( "never runs" );
}

console.log( foo() );
// Uncaught Exception: Oops!

其他非线性控制语句(如continuebreak)表现出类似的returnthrow行为,这一点不足为奇:

for (var i=0; i<10; i++) {
    try {
        continue;
    }
    finally {
        console.log( i );
    }
}
// 0 1 2 3 4 5 6 7 8 9

console.log(i)语句在循环迭代结束时运行,这是由continue语句引起的。但是,它仍然在i++迭代更新语句之前运行,这就是为什么打印的值是0..9而不是1..10

注意: ES6在生成器中添加了一个yield语句(参见本系列的Async和Performance标题),它在某些方面可以看作是一个中间return语句。但是,与return不同,在生成器重新开始之前,yield不会完成,这意味着try { .. yield .. }尚未完成。因此,附加的finally子句不会像return一样在yield之后运行。

finally中的return有着覆盖前一个trycatch子句中的return的特殊能力,但是仅在return被明确调用的情况下:

function foo() {
    try {
        return 42;
    }
    finally {
        // no `return ..` here, so no override
    }
}

function bar() {
    try {
        return 42;
    }
    finally {
        // override previous `return 42`
        return;
    }
}

function baz() {
    try {
        return 42;
    }
    finally {
        // override previous `return 42`
        return "Hello";
    }
}

foo();    // 42
bar();    // undefined
baz();    // "Hello"

通常,在函数中忽略returnreturn;相同或者甚至与return undefined;相同,但是在finally块中,省略return并不像一个重写的return undefined;,它只是让之前的return生效。

事实上,如果我们finally将结合标记的break(在本章前面讨论过),我们可以真正提高疯狂度:

function foo() {
    bar: {
        try {
            return 42;
        }
        finally {
            // break out of `bar` labeled block
            break bar;
        }
    }

    console.log( "Crazy" );

    return "Hello";
}

console.log( foo() );
// Crazy
// Hello

但是......不要这样做。我是认真的。使用finally +标记的break来有效地取消return,是你正在尽最大努力创建最容易混淆的代码。我敢打赌,任何注释都无法救赎这段代码。

switch

让我们简要探讨一下switch语句,这是一种if..else if..else ..语句链的一种语法简写。

switch (a) {
    case 2:
        // do something
        break;
    case 42:
        // do another thing
        break;
    default:
        // fallback to here
}

你可以看到,它计算一次a,然后将结果值与每个case表达式匹配(这里只是简单的值表达式)。如果找到匹配,则执行将在该匹配的case下开始,并且将一直持续到遇到break或者直到找到switch块的结尾。

这可能不会让你感到惊讶,但是有一些关于switch的怪异行为你以前可能没有注意到。

首先,表达式a和每个case表达式之间的匹配与===算法相同(参见第4章)。通常,switchcase语句中与绝对值一起使用,如上所示,因此严格匹配是合适的。

然而,你可能希望强制相等(就是==,参考第四章),为了做到强制相等,你可能需要对switch语句进行一点“黑客”处理:

var a = "42";

switch (true) {
    case a == 10:
        console.log( "10 or '10'" );
        break;
    case a == 42:
        console.log( "42 or '42'" );
        break;
    default:
        // never gets here
}
// 42 or '42'

这可以工作,是因为case子句可以有任何表达式(不仅仅是简单的值),这意味着他将严格的匹配表达式的结果和测试表达式(true)。因为a==42在这里结果为true,所以匹配成功。

尽管==switch匹配本身仍然严格,在这里介于truetrue之间。如果case表达式结果是truthy的,但不是严格的true(看第四章),这不会工作。例如,如果在表达式中使用“逻辑运算符”,例如||&&,则可能会坑到你:

var a = "hello world";
var b = 10;

switch (true) {
    case (a || b == 10):
        // never gets here
        break;
    default:
        console.log( "Oops" );
}
// Oops

因为(a || b == 10)的结果是"hello world"不是true,严格匹配失败。在这个案例里,修复方法是强制表达式显式地为truefalse,例如case !!(a || b==10):(见第4章)。

最后,default子句是可选的,不一定要在末尾出现(尽管这是强约定)。即使在default子句里,同样的规则也适用于遇到break或不遇到break

var a = 10;

switch (a) {
    case 1:
    case 2:
        // never gets here
    default:
        console.log( "default" );
    case 3:
        console.log( "3" );
        break;
    case 4:
        console.log( "4" );
}
// default
// 3

注意: 正如前面关于带标签的break的讨论,case子句中的break也可以标记。

此代码段处理的方式是首先通过所有case子句匹配,找不到匹配,然后返回到default子句并开始执行。由于那里没有break,它会在已经跳过的case3块中继续执行,然后一旦它到达该break就停止。

虽然这种循环逻辑在JavaScript中显然是可能的,但它几乎不可能形成合理或可理解的代码。如果你发现自己想要创建这样的循环逻辑流,那就请你得好好的想想了,如果你真的这样做,请确保包含大量的代码注释来解释你的目的!

Review

JavaScript语法有很多细微的差别,作为开发人员,我们应该花费更多的时间来关注这些细微的差别。一点点的努力可以巩固你对语言的更深层次的了解。

声明和表达式在英语中都有类似物 - 声明就像句子,表达式就像短语。表达式可以是纯/自包含的,也可以有副作用。

JavaScript语法在纯句法之上层叠语义使用规则(也称为上下文)。例如,程序中各个位置使用的{}对可以表示语句块,对象字面量,(ES6)解构赋值或(ES6)命名函数参数。

JavaScript运算符都有明确定义的优先级规则(首先绑定在其他之前)和关联性(多个运算符表达式如何隐式分组)。一旦你学会了这些规则,由你来决定优先级/关联性是否过于隐含于自己的用处,或者它们是否有助于编写更短,更清晰的代码。

ASI(自动分号插入)是JS引擎中内置的解析器错误更正机制,允许它在某些情况下插入假定的;在需要插入的地方,省略了插入,并且插入修复了解析器错误。争论这种行为是否意味着更多;是可选的(可以/应该省略,为了更干净的代码)或者是否意味着省略它们会导致JS引擎只为你清理错误。

JavaScript有几种类型的错误,但不太知道它有两个错误分类:“早期”(编译器抛出,不可捕获)和“运行时”(可以try..catch)。所有句法错误显然是在程序运行之前就停止程序的早期错误,但也有其他错误。

函数参数与其正式声明的命名参数有一个有趣的关系。具体来说,如果你不小心,arguments数组有许多漏洞抽象行为。如果可以,请避免使用arguments,但如果必须使用它,则一定要避免在arguments中使用位置槽的同时为同一参数使用命名参数。

finally子句附加到了try(或者try..catch),在执行处理顺序方面提供了一些非常有趣的怪癖。其中一些怪癖可能会有所帮助,但是可能会造成很多混乱,特别是如果与标记的块结合使用。与往常一样,使用finally以使代码更好更清晰,而不是聪明过头或令人困惑。

switchif..else if ..声明提供了一些不错的简写,但要注意关于其行为的许多常见的简化假设。如果你不小心的话,有几个怪癖可能会坑到你,但switch也有一些巧妙的隐藏技巧!

Last updated