在很多时候,词法和语法是一个意思
var a = 3 * 6;
var b = a;
b;
这里,3 * 6 是一个表达式(结果为 18)。第二行的 a 也是一个表达式,第三行的 b 也是。
语句都有一个结果值(undefined 也算)
大部分表达式没有副作用,最常见的有副作用的表达式是函数调用
function foo() {
a = a + 1;
}
var a = 1;
foo(); // 结果值:undefined。副作用:a的值被改变
var a = 42;
var b = a++;
a; // 43
b; // 42
var a = 42;
a++; // 42
a; // 43
++a; // 44
a; // 44
++ 在前面时,如 ++a,它的副作用(将 a 递增)产生在表达式返回结果值之前,而 a++ 的副作用则产生在之后。
可以使用 , 语句系列逗号运算符(statement-series comma operator)将多个独立的表达式语句串联成一个语句:
var a = 42, b;
b = ( a++, a );
a; // 43
b; // 43
对象常量
// 假定函数bar()已经定义
var a = {
foo: bar()
};
{ … } 被赋值给 a,因而它是一个对象常量。
标签
假定函数bar()已经定义
foo 是语句 bar() 的标签
// 标签为foo的循环
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 foo;
}
console.log( i, j );
}
}
// 0 0
// 0 1
// 0 2
// 0 3
// 1 0
// 1 1
// 1 2
// 停止! 1 3
JSON 被普遍认为是 JavaScript 语言的一个真子集
JSON 的确是 JavaScript 语法的一个子集,但是 JSON 本身并不是合法的 JavaScript 语法
JSON-P(将 JSON 数据封装为函数调用,比如 foo({“a”:42}))通过将 JSON 数据传递给函数来实现对其的访问。JSON-P 能将 JSON 转换为合法的
JS 语法。
代码块
[] + {}; // "[object Object]"
{} + []; // 0
第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理。[] 会被强制类型转换为 “”,而 {} 会被强制类型转换为 “[object Object]”。
但在第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需要分号,所以这里不存在语法上的问题。最后 + [] 将 [] 显式强制类型转换(参见第 4 章)为 0。
对象解构
function getData() {
// ..
return {
a: 42,
b: "foo"
};
}
var { a, b } = getData();
console.log( a, b ); // 42 "foo"
{ … } 还可以用作函数命名参数(named function argument)的对象解构(object destructuring),方便隐式地用对象属性赋值
function foo({ a, b, c }) {
// 不再需要这样:
// 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]
else if和可选代码块
事实上 JavaScript 没有 else if,但 if 和 else 只包含单条语句的时候可以省略代码块的,else 也是如此,所以我们经常用到的 else if 实际上是这样的
if (a) {
// ..
}
else {
if (b) {
// ..
}
else {
// ..
}
}
else if 极为常见,能省掉一层代码缩进,所以很受青睐。但这只是我们自己发明的用法,切勿想当然地认为这些都属于JS语法的范畴。
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++)
用"," 来连接一系列语句的时候,它的优先级最低,其他操作数的优先级都比它高
var a = 42;
var b = "foo";
var c = false;
var d = a && b || c ? c || b ? a : c && b : a;
d; // 42
&& 先执行,然后是 ||。
对 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将这种现象称为“短路”(即执行最短路径)
以 a && b 为例,如果 a 是一个假值,足以决定 && 的结果,就没有必要再判断 b 的值。同样对于 a || b,如果 a 是一个真值,也足以决定 || 的结果,也就没有必要再判断 b 的值
回顾一下前面多个运算符串联在一起的例子:
a && b || c ? c || b ? a : c && b : a
// 执行顺序
(a && b || c) ? (c || b) ? a : (c && b) : a
因为 && 运算符的优先级高于 ||,而 || 的优先级又高于 ? :,因此表达式 (a && b || c) 先于包含它的 ? : 运算符执行。
&& 和 || 运算符先于 ? : 执行,那么如果多个相同优先级的运算符同时出现,又该如何处理呢?
JS的默认执行顺序:这里遵循从左到右的顺序
? : 是右关联,另一个右关联(组合)的例子是 = 运算符
true ? false : true ? true : true; // false
true ? false : (true ? true : true); // false
(true ? false : true) ? true : true; // true
可以看出,? : 是右关联,并且它的组合方式会影响返回结果
看一下比较复杂的例子:
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
// 现在来逐一执行。
(1) (a && b) 结果为 "foo"。
(2) "foo" || c 结果为 "foo"。
(3) 第一个 ? 中,"foo" 为真值。
(4) (c || b) 结果为 "foo"。
(5) 第二个 ? 中,"foo" 为真值。
(6) a 的值为 42。
因此,最后结果为 42
&& > || > ? : > = > ++
TDZ 指的是由于代码中的变量还没有初始化而不能被引用的情况。
向函数传递参数时,arguments 数组中的对应单元会和命名参数建立关联(linkage)以得到相同的值。相反,不传递参数就不会建立关联。
function foo(a) {
a = 42;
console.log( arguments[0] );
}
foo( 2 ); // 42 (linked)
foo(); // undefined (not linked)
实际上,它是 JS语言引擎底层实现的一个抽象泄漏(leaky abstraction),并不是语言本身的特性,arguments 数组已经被废止
finally 中的代码总是会在 try 之后执行,如果有 catch 的话则在 catch 之后执行。也可以将 finally 中的代码看作一个回调函数,即无论出现什么情况最后一定会被调用。
如果 try 中有 return 语句会出现什么情况呢?
function foo() {
try {
return 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// 42
如果 finally 中抛出异常(无论是有意还是无意),函数就会在此处终止。如果此前 try 中已经有 return 设置了返回值,则该值会被丢弃:
function foo() {
try {
return 42;
}
finally {
throw "Oops!";
}
console.log( "never runs" );
}
console.log( foo() );
// Uncaught Exception: Oops!
finally中的return回覆盖try和catch中的return的返回值
可以把switch看作 if…else if…else… 的简化版本
switch (a) {
case 2:
// 执行一些代码
break;
case 42:
// 执行另外一些代码
break;
default:
// 执行缺省代码
}
default 是可选的,并非必不可少(虽然惯例如此)。break 相关规则对 default 仍然适用
var a = 10;
switch (a) {
case 1:
case 2:
// 永远执行不到这里
default:
console.log( "default" );
case 3:
console.log( "3" );
break;
case 4:
console.log( "4" );
}
// default
// 3
首先遍历并找到所有匹配的 case,如果没有匹配则执行default 中的代码。因为其中没有 break,所以继续执行已经遍历过的 case 3 代码块,直
到 break 为止。
JavaScript 语法规则中的许多细节需要我们多花点时间和精力来了解。从长远来看,这有助于更深入地掌握这门语言。
语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表达式可以是简单独立的,否则可能会产生副作用。
JavaScript 语法规则之上是语义规则(也称作上下文)。例如,{ } 在不同情况下的意思不尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。
JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。
ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制,它会在需要的地方自动插入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略),或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。
JavaScript 中有很多错误类型,分为两大类:早期错误(编译时错误,无法被捕获)和运行时错误(可以通过 try…catch 来捕获)。所有语法错误都是早期错误,程序有语法错误则无法运行。
函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们挖了不少坑。因此,尽量不要使用 arguments,如果非用不可,也切勿同时使用 arguments和其对应的命名参数。
finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑,特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌弄巧成拙。
switch 相对于 if…else if… 来说更为简洁。需要注意的一点是,如果对其理解得不够透彻,稍不注意就很容易出错。