JS的语法

在很多时候,词法和语法是一个意思

1、语句与表达式

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

1.1、上下文规则

对象常量

// 假定函数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语法的范畴。

2、运算符优先级

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

&& 先执行,然后是 ||。

2.1、短路

对 && 和 || 来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将这种现象称为“短路”(即执行最短路径)

以 a && b 为例,如果 a 是一个假值,足以决定 && 的结果,就没有必要再判断 b 的值。同样对于 a || b,如果 a 是一个真值,也足以决定 || 的结果,也就没有必要再判断 b 的值

2.2、?:运算符

回顾一下前面多个运算符串联在一起的例子:

a && b || c ? c || b ? a : c && b : a
// 执行顺序
(a && b || c) ? (c || b) ? a : (c && b) : a

因为 && 运算符的优先级高于 ||,而 || 的优先级又高于 ? :,因此表达式 (a && b || c) 先于包含它的 ? : 运算符执行。

2.3、关联

&& 和 || 运算符先于 ? : 执行,那么如果多个相同优先级的运算符同时出现,又该如何处理呢?

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

&& > || > ? : > = > ++

3、函数参数

TDZ 指的是由于代码中的变量还没有初始化而不能被引用的情况。

向函数传递参数时,arguments 数组中的对应单元会和命名参数建立关联(linkage)以得到相同的值。相反,不传递参数就不会建立关联。

function foo(a) {
	a = 42;
console.log( arguments[0] );
}
foo( 2 ); // 42 (linked)
foo(); // undefined (not linked)

实际上,它是 JS语言引擎底层实现的一个抽象泄漏(leaky abstraction),并不是语言本身的特性,arguments 数组已经被废止

4、try…finally

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的返回值

5、switch

可以把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 为止。

6、总结

JavaScript 语法规则中的许多细节需要我们多花点时间和精力来了解。从长远来看,这有助于更深入地掌握这门语言。

语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表达式可以是简单独立的,否则可能会产生副作用。

JavaScript 语法规则之上是语义规则(也称作上下文)。例如,{ } 在不同情况下的意思不尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。

JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。

ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制,它会在需要的地方自动插入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略),或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。

JavaScript 中有很多错误类型,分为两大类:早期错误(编译时错误,无法被捕获)和运行时错误(可以通过 try…catch 来捕获)。所有语法错误都是早期错误,程序有语法错误则无法运行。

函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们挖了不少坑。因此,尽量不要使用 arguments,如果非用不可,也切勿同时使用 arguments和其对应的命名参数。

finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑,特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌弄巧成拙。

switch 相对于 if…else if… 来说更为简洁。需要注意的一点是,如果对其理解得不够透彻,稍不注意就很容易出错。

你可能感兴趣的:(javascript,开发语言,ecmascript)