抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

在 JavaScript 中,有很多常见的语法仍然有很多地方容易产生困惑、造成误解。

语句与表达式

语句(statement)和表达式(expression)常常被混为一谈,但他们二者之间有细微的差别。语句相当于一句话,而表达式更类似于一条短语。

JavaScript 中表达式可以返回一个结果值:

let a = 3 * 6;
const b = a;
b;

上述带有letconst的语句被称为“声明语句”(declaration statement),而不带声明关键词的a = 3 * 6;则是“赋值表达式”。

乍一看可能还是不能太过于区分他们之间的区别,但是有些地方会让我们强制区分他们。在现代 SPA 框架中,模板语法中通常只允许插入 JavaScript 表达式,而不允许插入语句。例如在常见的 Vue 和 React 中:

<template>
  <span>{{ 3 * 6 }}</span>
  <span>{{ a }}</span>
  <span>{{ let a = 3 * 6 }}</span>  // 错误
</template>

语句的结果值

基本上所有的语句都有一个结果值(undefined 也算)。

如果经常和控制台打交道,应该会发现很多时候控制台都会给我们返回一个 undefined。其实这就是语句所返回的值,只不过大部分情况下都是 undefined。

代码块中的返回值是最后一个语句/表达式的结果:

let a ;
// undefined
if (true) {
    a = 3 + 1;
}
// 4

目前代码块语句返回的值无法被拿到:

b = if (true) {
    a = 3 + 1;
}
// VM268:1 Uncaught SyntaxError: Unexpected token 'if'

当然也有变通的方法 evil:

b = eval("if (true) {a = 3 + 1;}");
b; // 4

除了万恶的 evil,ES7 还有一项“do 表达式”提案,目前环境还没实现:

b = do { 
    if (true) {
        a = 3 + 1;
    }
};
b; // 4

表达式的副作用

副作用通常是指执行了语句之后,除了返回值或赋值,还有其他变量等被修改。

最常见的副作用的表达式是函数调用:

const foo = () => {
  a = a + 1;
};

let a = 1;
foo(); // 结果:undefined,副作用:a 的值被修改

上下文规则

在 JavaScript 语法规则中,有时候同样的语法在不同的情况下会有不同的解释。这些语法规则孤立起来会很难理解。

大括号

大括号的作用有很多,随着 JavaScript 的演进可能会出现更多类似的情况。

对象常量

大括号可以使用表达式的方式定义对象:

const a = {
    foo: bar()
}

大括号内的内容被赋值给变量 a,因而它是一个对象常量。

标签

将上述声明语句去掉,这时的大括号可不是一个孤立的对象常量。因为这里的{...}被看作了一个普通的代码块。这里的foo: bar()被解释为标签语句。

{
    foo: bar()
}

标签语句配合 continue 和 break 语句可以实现类似于 goto 的方式进行跳转。但 goto 是一种极为糟糕的编码方式,所以 JavaScript 并不支持真正的 goto 语句。

代码块

有一个强制类型转换的坑经常被提到:

[] + {} // "[object Object]"
{} + [] // 0

看上去简直就是 JavaScript 在和我们开玩笑。其实不然,上述情况并不是非常难以理解。

第一行是普通的强制类型转换,二者都会被强制转换字符串,而[]被转换为了''{}被转换为了"[object Object]",所以他们相加的最后结果就是"[object Object]"

第二行看上去有点莫名其妙,换个位置结果就变了。这里其实{}并没有参与运算,而是被当作了一段独立的空代码块。真正参与运算的是+[],最终被转换为数字 0。

对象解构

从 ES6 开始{...}也可用于解构赋值。

const foo = () => {
  return {
    name: 'xfy',
    age: 18,
  };
};

const { name, age } = foo();
console.log(name, age);

同理,函数形参也可以利用解构赋值。

else if 和可选代码块

其实else if并不存在于 JavaScript 语法中,只是多个if else只包含单条语句的时候可以省略代码块的{...}

实际上else if是这样的:

if (a) {
  console.log(a);
} else {
  if (b) {
    console.log(b);
  } else {
    console.log(c);
  }
}

运算符优先级

多数情况下,JavaScript 的数学运算符与真正的数学一样,按照真正的数学的顺序进行运算。但 JavaScript 的语句并不是数学公式,而且语句中通常会充斥着各种各样的运算符。他们也是有各自的运算优先级的。

短路

and 与 or 操作拥有“短路”(short circuiting)的特性,别急,我们还没走错片场。在 JavaScript 中,他们分别是&&||运算符。

短路的意思是,当左边的操作数能够得出结果时,就可以忽略右边的操作数。

对于&&来说,左边的操作数为假值时,则就没有必要再判断右边的值。同理,对于||来说,左边的操作数为真值时,也没有必要再判断右边的操作数。

关联

JavaScript 的代码是从上往下执行的,而单条语句是从左往右执行的。但在特定的运算符下,某些语句需要先求得右边的值。

例如:

true ? false : true ? true : true;

这段代码乍一看很令人头疼,但它确实说明运算符的关联最容易理解的例子。在这里,第二个三目运算符的结果会影响第一个三目运算符,所以这里要执行右关联,也就是需要先求得第二个三目运算符的结果。

虽然是右关联,但它的运算顺序还是从左到右。a ? b : c;还是会先执行 a 然后 b c。

自动分号

有时 JavaScript 会自动为代码补上分号,即分号自动插入机制(Automatic Semicolon Insertion,ASI)。

ASI只会在换行符处起作用,而不会在代码中间插入分号。

常见的情况是在 return 后自动加上,这样 return 后换行的代码就会不生效。return 语句可以跨越多行,但是其后必须右换行符以外的代码。

const foo = () => {
  return (
    1 + 2
  );
};

纠错机制

是否应该完全依赖 ASI 来编码,这是 JavaScript 社区中最具争议性的话题之一。

支持的一方认为 ASI 大有裨益,能省略掉那些不必要的;,让代码更简洁。此外 ASI 让许多;变得可有可无,因此只要代码没问题,有没有;都一样。

反方则认为 ASI 机制问题太多,对于缺乏经验的初学者尤其如此,因为自动插入;会无意改变代码的逻辑。有些人还认为依赖 linter 这些工具找出错误,而不是依赖 JavaScript 引擎。

事实上,现在多数;可以通过 ESlint 这样的 linter 工具来在格式化代码是插入。这种方式相比较用引擎来找出错误好很多,而且它不会在运行时改变代码原有的意思。

小结

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

评论