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
一样。a
和b
表达式都评估当时存储在那些变量中的值,也恰好是18
。
而且,三行中的每一行都是包含表达式的语句。var a = 3 * 6
和var b = a
被称为"声明语句",因为它们都声明了一个变量(并且可选地为它赋值)。a = 3 * 6
和b =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
的报告,因为42
是if
块的完成值,它接受了它的最后赋值表达式语句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
:带标签的跳转。continue
和break
语句都可以选择接受指定的标签,在这种情况下,程序流“跳转”类似于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
的迭代,但标签循环跳转也跳过迭代11
和2 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
。但if
和else
语句如果只包含一个语句,则允许省略其附加块周围的{}
。你之前见过很多次,就是:
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的版本关于&&
和||
有趣的是,他们选择并返回其中一个操作数,而不仅仅是得到true
或false
。如果只有两个操作数和一个操作符,这很容易解释。
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) || c
或a && (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.cool
的opts
部分充当了一种防护,因为如果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 && b
或b && 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
现在让我们来解决他:
(a && b)
就是"foo"
"foo" || c
就是"foo"
对于第一个
?
来说,"foo"
是truthy的(c || b)
是"foo"
对于第二个
?
,"foo"
是truthy的a
是42
就是这样的,我们完成了!答案就是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
语句的一部分来处理吗?如果,
在b
和c
之间的任何地方(甚至是另一条线),它肯定会是的。但由于没有一个,
,JS反而假定有一个隐含的;
(在换行符处)在b
后面。因此c;
被保留为独立的表达式语句。
同样的:
var a = 42, b = "foo";
a
b // "foo"
这仍然是一个没有错误的有效程序,因为表达式语句也接受ASI。
在某些地方ASI很有帮助,比如:
var a = 42;
do {
// ..
} while (a) // <-- ; expected here!
a;
语法需要一个;
在do..while
循环之后,但不是在while
或for
循环之后。但大多数开发人员都不记得了!因此,ASI很有帮助地介入并插入一个。
正如我们在本章前面所述,语句块不需要;
终止,所以不需要ASI:
var a = 42;
while (a) {
// ..
} // <-- no ; expected here
a;
ASI介入的另一个主要案例是break
、continue
、return
和(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
);
}
相同的推理适用于break
,continue
和yield
。
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不仅具有不同的错误子类型(TypeError
,ReferenceError
,SyntaxError
等),而且语法定义了在编译时要强制执行的某些错误。
特别是,长期以来一直存在许多应该被捕获并报告为“早期错误”的特定条件(在编译期间)。任何直接语法错误都是早期错误(例如,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
即使默认参数值应用于a
和b
参数,如果在这些插槽中没有传递参数,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..finally
你可能熟悉try..catch
块是如何工作的。但是你有没有停下来考虑可以与之配对的finally
子句?事实上,你是否意识到try
只需要catch
或finally
,但两者都可以在需要时出现。
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(..)
语句使用。
在try
里throw
具有相同的行为:
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!
其他非线性控制语句(如continue
和break
)表现出类似的return
和throw
行为,这一点不足为奇:
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
有着覆盖前一个try
或catch
子句中的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"
通常,在函数中忽略return
与return;
相同或者甚至与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
让我们简要探讨一下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章)。通常,switch
在case
语句中与绝对值一起使用,如上所示,因此严格匹配是合适的。
然而,你可能希望强制相等(就是==
,参考第四章),为了做到强制相等,你可能需要对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
匹配本身仍然严格,在这里介于true
和true
之间。如果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
,严格匹配失败。在这个案例里,修复方法是强制表达式显式地为true
或false
,例如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
以使代码更好更清晰,而不是聪明过头或令人困惑。
switch
为if..else if ..
声明提供了一些不错的简写,但要注意关于其行为的许多常见的简化假设。如果你不小心的话,有几个怪癖可能会坑到你,但switch
也有一些巧妙的隐藏技巧!
Last updated