3月阅读周·你不知道的JavaScript | 语法——构成可运行的程序代码的词法规则

举报
叶一一 发表于 2024/03/26 09:45:46 2024/03/26
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读两个月。《你不知道的JavaScript》分上中下三卷,内容相对较多。3月份,我计划先读前面两卷。已读完书籍:《架构简洁之道》、《深入浅出的...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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。

1-1.png

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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。