3月阅读周·你不知道的JavaScript | 语法——构成可运行的程序代码的词法规则
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读两个月。
《你不知道的JavaScript》分上中下三卷,内容相对较多。3月份,我计划先读前面两卷。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》。
当前阅读周书籍:《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》。
语法
语句和表达式
语句的结果值
语句都有一个结果值,获得结果值最直接的方法是在浏览器开发控制台中输入语句,默认情况下控制台会显示所执行的最后一条语句的结果值。
以赋值表达式b = a为例,其结果值是赋给b的值(18),但规范定义var的结果值是undefined。如果在控制台中输入var a = 42会得到结果值undefined,而非42。
例如:
var b;
if (true) {
b = 4 + 38;
}
在控制台/REPL中输入以上代码应该会显示42,即最后一个语句/表达式b = 4 +38的结果值。
代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。
ES7规范有一项“do表达式”(do expression)提案:
var a, b;
a = do {
if (true) {
b = 4 + 38;
}
};
a; // 42
do { .. }表达式执行一个代码块(包含一个或多个语句),并且返回其中最后一个语句的结果值,然后赋值给变量a。
表达式的副作用
最常见的有副作用(也可能没有)的表达式是函数调用:
function foo() {
a = a + 1;
}
var a = 1;
foo(); // 结果值:undefined。副作用:a的值被改变
其他一些表达式也有副作用,比如:
var a = 42;
var b = a++;
a; // 43
b; // 42
a++首先返回变量a的当前值42(再将该值赋给b),然后将a的值加1,所以a的值为43,b的值是42。
++在前面时,如++a,它的副作用(将a递增)产生在表达式返回结果值之前,而a++的副作用则产生在之后。
再如delete运算符。delete用来删除对象中的属性和数组中的单元。它通常以单独一个语句的形式出现:
var obj = {
a: 42,
};
obj.a; // 42
delete obj.a; // true
obj.a; // undefined
如果操作成功,delete返回true,否则返回false。其副作用是属性被从对象中删除(或者单元从array中删除)。
上下文规则
1、运算符优先级
下面两种情况会用到大括号{ .. }(随着JavaScript的演进会出现更多类似的情况)。
(1) 对象常量
用大括号定义对象常量(object literal):
// 假定函数bar()已经定义
var a = {
foo: bar(),
};
{ .. }被赋值给a,因而它是一个对象常量。
(2) 标签
{
foo: bar();
}
{ .. }在这里只是一个普通的代码块。{ .. }和for/while循环以及if条件语句中代码块的作用基本相同。
2、代码块
[] + {}; // "[object Object]"
{} + []; // 0
第一行代码中,{}出现在+运算符表达式中,因此它被当作一个值(空对象)来处理。[]会被强制类型转换为"",而{}会被强制类型转换为"[object Object]"。
第二行代码中,{}被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需要分号,所以这里不存在语法上的问题。最后+ []将[]显式强制类型转换为0。
3、对象解构
{ .. }也可用于“解构赋值”,特别是对象的解构。
{ .. }还可以用作函数命名参数(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]
4、else if和可选代码块
else if极为常见,能省掉一层代码缩进,所以很受青睐。如下的常见代码:
if (a) {
// ..
} else if (b) {
// ..
} else {
// ..
}
JavaScript没有else if,但if和else只包含单条语句的时候可以省略代码块的{ }。所以上面的代码实际上是:
if (a) {
// ..
} else {
if (b) {
// ..
} else {
// ..
}
}
if (b) { .. } else { .. }实际上是跟在else后面的一个单独的语句,所以带不带{ }都可以。换句话说,else if不符合前面介绍的编码规范,else中是一个单独的if语句。
运算符优先级
超过一个运算符时表达式的执行顺序的规则,被称为运算符优先级。
1、运算符的优先级比=低
var a = 42, b;
b = a++, a;
a; // 43
b; // 42
所以在上面代码中,b = a++, a其实可以理解为(b = a++), a。
2、&&运算符的优先级高于=
if (str && (matches = str.match(/[aeiou]/g))) {
// ..
}
如果没有( )对其中的表达式进行绑定(bind)的话,就会执行作(str && matches) = str.match..。这样会出错,由于(str && matches)的结果并不是一个变量,而是一个undefined值,因此它不能出现在=运算符的左边!
3、&&运算符先于||执行
true || (false && false); // true
(true || false) && false; // false
true || (false && false); // true
上面的代码结果可知执行顺序并非想象中的的从左到右,原因就在于运算符优先级。
自动分号
JavaScript会自动为代码行补上缺失的分号,即自动分号插入(ASI)。
var a = 42, b
c;
b; // undefined
c; // c is not defined
如果b和c之间出现,的话(即使另起一行), c会被作为var语句的一部分来处理。在上例中,JavaScript判断b之后应该有;,所以c;被处理为一个独立的表达式语句。
纠错机制
大多数情况下,分号并非必不可少,不过for( .. ) ..循环头部的两个分号是必需的。
对ASI来说,解析器报错的唯一原因就是代码中缺失了必要的分号。
建议在所有需要的地方加上分号,将对ASI的依赖降到最低。
错误
JavaScript不仅有各种类型的运行时错误(TypeError、ReferenceError、SyntaxError等),它的语法中也定义了一些编译时错误。
在编译阶段发现的代码错误叫作“早期错误”(early error)。语法错误是早期错误的一种(如a = ,)。另外,语法正确但不符合语法规则的情况也存在。
这些错误在代码执行之前是无法用try..catch来捕获的,相反,它们还会导致解析/编译失败。
提前使用变量
ES6规范定义了一个新概念,叫作TDZ(Temporal Dead Zone,暂时性死区)。
TDZ指的是由于代码中的变量还没有初始化而不能被引用的情况。
对此,最直观的例子是ES6规范中的let块作用域:
{
a = 2; // ReferenceError!
let a;
}
函数参数
对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的单元,而不是默认值。
即使将命名参数和arguments数组混用也不会出错,只需遵守一个原则,即不要同时访问命名参数和其对应的arguments数组单元。
function foo(a) {
console.log(a + arguments[1]); // 安全!
}
foo(10, 32); // 42
try..finally
finally中的代码总是会在try之后执行,如果有catch的话则在catch之后执行。也可以将finally中的代码看作一个回调函数,即无论出现什么情况最后一定会被调用。
function foo() {
try {
return 42;
} finally {
console.log('Hello');
}
console.log('never runs');
}
console.log(foo());
上面的代码中,return 42先执行,并将foo()函数的返回值设置为42。然后try执行完毕,接着执行finally。最后foo()函数执行完毕,console.log(..)显示返回值。
所以先输出Hello,再输出42。
switch
switch,可以把它看作if..else if..else..的简化版本:
switch (a) {
case 2:
// 执行一些代码
break;
case 42:
// 执行另外一些代码
break;
default:
// 执行缺省代码
}
这里a与case表达式逐一进行比较。如果匹配就执行该case中的代码,直到break或者switch代码块结束。
总结
我们来总结一下本篇的主要内容:
- JavaScript表达式可以是简单独立的,否则可能会产生副作用。
- JavaScript语法规则之上是语义规则(也称作上下文)。例如,{ }在不同情况下的意思不尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。
- JavaScript详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。
- JavaScript中有很多错误类型,分为两大类:早期错误(编译时错误,无法被捕获)和运行时错误(可以通过try..catch来捕获)。所有语法错误都是早期错误,程序有语法错误则无法运行。
- ASI(自动分号插入)是JavaScript引擎的代码解析纠错机制,它会在需要的地方自动插入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略),或者由于分号缺失导致的错误是否都可以交给JavaScript引擎来处理。
- 函数参数和命名参数之间的关系非常微妙。尤其是arguments数组,它的抽象泄漏给我们挖了不少坑。因此,尽量不要使用arguments,如果非用不可,也切勿同时使用arguments和其对应的命名参数。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)