You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

544 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# JavaScript语法你知道哪些JavaScript语句
你好我是winter。
我们在上一节课中已经讲过了JavaScript语法的顶层设计接下来我们进入到更具体的内容。
JavaScript遵循了一般编程语言的“语句-表达式”结构,多数编程语言都是这样设计的。我们在上节课讲的脚本,或者模块都是由语句列表构成的,这一节课,我们就来一起了解一下语句。
在JavaScript标准中把语句分成了两种声明和语句不过这里的区分逻辑比较奇怪所以这里我还是按照自己的思路给你整理一下。
普通语句:
![](https://static001.geekbang.org/resource/image/81/55/8186219674547691cf59e5c095304d55.png)
声明型语句:
![](https://static001.geekbang.org/resource/image/0e/38/0e5327528df12d1eaad52c4005efff38.jpg)
我们根据上面的分类,来遍历学习一下这些语句。
## 语句块
我们可以这样去简单理解,语句块就是一对大括号。
```JavaScript
{
var x, y;
x = 10;
y = 20;
}
```
语句块的意义和好处在于让我们可以把多行语句视为同一行语句这样if、for等语句定义起来就比较简单了。不过我们需要注意的是语句块会产生作用域我们看一个例子
```JavaScript
{
let x = 1;
}
console.log(x); // 报错
```
这里我们的let声明仅仅对语句块作用域生效于是我们在语句块外试图访问语句块内的变量x就会报错。
## 空语句
空语句就是一个独立的分号,实际上没什么大用。我们来看一下:
```JavaScript
;
```
空语句的存在仅仅是从语言设计完备性的角度考虑,允许插入多个分号而不抛出错误。
## if语句
if语句是条件语句。我想对多数人来说if语句都是熟悉的老朋友了也没有什么特别需要注意的用法但是为了我们课程的完备性这里还是要讲一下。
if语句示例如下
```JavaScript
if(a < b)
console.log(a);
```
if语句的作用是在满足条件时执行它的内容语句这个语句可以是一个语句块这样就可以实现有条件地执行多个语句了。
if语句还有else结构用于不满足条件时执行一种常见的用法是利用语句的嵌套能力把if和else连写成多分支条件判断
```JavaScript
if(a < 10) {
//...
} else if(a < 20) {
//...
} else if(a < 30) {
//...
} else {
//...
}
```
这段代码表示四个互斥的分支分别在满足a<10a<20a<30和其它情况时执行。
## switch语句
switch语句继承自JavaJava中的switch语句继承自CC++,原本switch语句是跳转的变形,所以我们如果要用它来实现分支,必须要加上break
其实switch原本的设计是类似goto的思维。我们看一个例子:
```JavaScript
switch(num) {
case 1:
print(1);
case 2:
print 2;
case 3:
print 3;
}
```
这段代码当num1时输出1 2 3,当num2时输出2 3,当num3时输出3。如果我们要把它变成分支型,则需要在每个case后加上break
```JavaScript
switch(num) {
case 1:
print 1;
break;
case 2:
print 2;
break;
case 3:
print 3;
break;
}
```
C时代,switch生成的汇编代码性能是略优于if else的,但是对JavaScript来说,则无本质区别。我个人的看法是,现在switch已经完全没有必要使用了,应该用if else结构代替。
## 循环语句
循环语句应该也是你所熟悉的语句了,这里我们把重点放在一些新用法上。
### while循环和do while循环
这两个都是历史悠久的JavaScript语法了,示例大概如下:
```JavaScript
let a = 100
while(a--) {
console.log("*");
}
```
```JavaScript
let a = 101;
do {
console.log(a);
} while(a < 100)
```
注意,这里do while循环无论如何至少会执行一次。
### 普通for循环
首先我们来看看普通的for循环。
```JavaScript
for(i = 0; i < 100; i++)
console.log(i);
for(var i = 0; i < 100; i++)
console.log(i);
for(let i = 0; i < 100; i++)
console.log(i);
var j = 0;
for(const i = 0; j < 100; j++)
console.log(i);
```
这里为了配合新语法,加入了允许letconst,实际上,const在这里是非常奇葩的东西,因为这里声明和初始化的变量,按惯例是用于控制循环的,但是它如果是const就没法改了。
我想,这一点可能是从保持letconst一致性的角度考虑的吧。
### for in循环
for in 循环枚举对象的属性,这里体现了属性的enumerable特征。
```JavaScript
let o = { a: 10, b: 20}
Object.defineProperty(o, "c", {enumerable:false, value:30})
for(let p in o)
console.log(p);
```
这段代码中,我们定义了一个对象o,给它添加了不可枚举的属性c,之后我们用for in循环枚举它的属性,我们会发现,输出时得到的只有ab
如果我们定义c这个属性时,enumerabletrue,则for in循环中也能枚举到它。
### for of循环和for await of循环
for of循环是非常棒的语法特性。
我们先看下基本用法,它可以用于数组:
```JavaScript
for(let e of [1, 2, 3, 4, 5])
console.log(e);
```
但是实际上,它背后的机制是iterator机制。
我们可以给任何一个对象添加iterator,使它可以用于for of语句,看下示例:
```JavaScript
let o = {
[Symbol.iterator]:() => ({
_value: 0,
next(){
if(this._value == 10)
return {
done: true
}
else return {
value: this._value++,
done: false
};
}
})
}
for(let e of o)
console.log(e);
```
这段代码展示了如何为一个对象添加iterator。但是,在实际操作中,我们一般不需要这样定义iterator,我们可以使用generator function
```JavaScript
function* foo(){
yield 0;
yield 1;
yield 2;
yield 3;
}
for(let e of foo())
console.log(e);
```
这段代码展示了generator functionfoo的配合。
此外,JavaScript还为异步生成器函数配备了异步的for of,我们来看一个例子:
```JavaScript
function sleep(duration) {
return new Promise(function(resolve, reject) {
setTimeout(resolve,duration);
})
}
async function* foo(){
i = 0;
while(true) {
await sleep(1000);
yield i++;
}
}
for await(let e of foo())
console.log(e);
```
这段代码定义了一个异步生成器函数,异步生成器函数每隔一秒生成一个数字,这是一个无限的生成器。
接下来,我们使用for await of来访问这个异步生成器函数的结果,我们可以看到,这形成了一个每隔一秒打印一个数字的无限循环。
但是因为我们这个循环是异步的,并且有时间延迟,所以,这个无限循环的代码可以用于显示时钟等有意义的操作。
## return
return语句用于函数中,它终止函数的执行,并且指定函数的返回值,这是大家非常熟悉语句了,也没有什么特殊之处。
```JavaScript
function squre(x){
return x * x;
}
```
这段代码展示了return的基本用法。它后面可以跟一个表达式,计算结果就是函数返回值。
## break语句和continue语句
break语句用于跳出循环语句或者switch语句,continue语句用于结束本次循环并继续循环。
这两个语句都属于控制型语句,用法也比较相似,所以我们就一起讲了。需要注意的是,它们都有带标签的用法。
```JavaScript
outer:for(let i = 0; i < 100; i++)
inner:for(let j = 0; j < 100; j++)
if( i == 50 && j == 50)
break outer;
outer:for(let i = 0; i < 100; i++)
inner:for(let j = 0; j < 100; j++)
if( i >= 50 && j == 50)
continue outer;
```
带标签的breakcontinue可以控制自己被外层的哪个语句结构消费,这可以跳出复杂的语句结构。
## with语句
with语句是个非常巧妙的设计,但它把JavaScript的变量引用关系变得不可分析,所以一般都认为这种语句都属于糟粕。
但是历史无法改写,现在已经无法去除with了。我们来了解一下它的基本用法即可。
```JavaScript
let o = {a:1, b:2}
with(o){
console.log(a, b);
}
```
with语句把对象的属性在它内部的作用域内变成变量。
## try语句和throw语句
try语句和throw语句用于处理异常。它们是配合使用的,所以我们就放在一起讲了。在大型应用中,异常机制非常重要。
```JavaScript
try {
throw new Error("error");
} catch(e) {
console.log(e);
} finally {
console.log("finally");
}
```
一般来说,throw用于抛出异常,但是单纯从语言的角度,我们可以抛出任何值,也不一定是异常逻辑,但是为了保证语义清晰,不建议用throw表达任何非异常逻辑。
try语句用于捕获异常,用throw抛出的异常,可以在try语句的结构中被处理掉:try部分用于标识捕获异常的代码段,catch部分则用于捕获异常后做一些处理,而finally则是用于执行后做一些必须执行的清理工作。
catch结构会创建一个局部的作用域,并且把一个变量写入其中,需要注意,在这个作用域,不能再声明变量e了,否则会出错。
catch中重新抛出错误的情况非常常见,在设计比较底层的函数时,常常会这样做,保证抛出的错误能被理解。
finally语句一般用于释放资源,它一定会被执行,我们在前面的课程中已经讨论过一些finally的特征,即使在try中出现了returnfinally中的语句也一定要被执行。
## debugger语句
debugger语句的作用是:通知调试器在此断点。在没有调试器挂载时,它不产生任何效果。
介绍完普通语句,我们再来看看声明型语句。声明型语句跟普通语句最大区别就是声明型语句响应预处理过程,普通语句只有执行过程。
## var
var声明语句是古典的JavaScript中声明变量的方式。而现在,在绝大多数情况下,letconst都是更好的选择。
我们在上一节课已经讲解了var声明对全局作用域的影响,它是一种预处理机制。
如果我们仍然想要使用var,我的个人建议是,把它当做一种“保障变量是局部”的逻辑,遵循以下三条规则:
* 声明同时必定初始化;
* 尽可能在离使用的位置近处声明;
* 不要在意重复声明。
例如:
```JavaScript
var x = 1, y = 2;
doSth(x, y);
for(var x = 0; x < 10; x++)
doSth2(x);
```
这个例子中,两次声明了变量x,完成了两段逻辑,这两个x意义上可能不一定相关,这样,不论我们把代码复制粘贴在哪里,都不会出错。
当然,更好的办法是使用let改造,我们看看如何改造:
```JavaScript
{
let x = 1, y = 2;
doSth(x, y);
}
for(let x = 0; x < 10; x++)
doSth2(x);
```
这里我用代码块限制了第一个x的作用域,这样就更难发生变量命名冲突引起的错误了。
## let和const
letconst是都是变量的声明,它们的特性非常相似,所以我们放在一起讲了。letconst是新设计的语法,所以没有什么硬伤,非常地符合直觉。letconst的作用范围是iffor等结构型语句。
我们看下基本用法:
```JavaScript
const a = 2;
if(true){
const a = 1;
console.log(a);
}
console.log(a);
```
这里的代码先在全局声明了变量a,接下来又在if内声明了aif内构成了一个独立的作用域。
constlet语句在重复声明时会抛错,这能够有效地避免变量名无意中冲突:
```JavaScript
let a = 2
const a = 1;
```
这段代码中,先用let声明了a,接下来又试图使用const声明变量a,这时,就会产生错误。
letconst声明虽然看上去是执行到了才会生效,但是实际上,它们还是会被预处理。如果当前作用域内有声明,就无法访问到外部的变量。我们来看这段代码:
```JavaScript
const a = 2;
if(true){
console.log(a); //抛错
const a = 1;
}
```
这里在if的作用域中,变量a声明执行到之前,我们访问了变量a,这时会抛出一个错误,这说明const声明仍然是有预处理机制的。
在执行到const语句前,我们的JavaScript引擎就已经知道后面的代码将会声明变量a,从而不允许我们访问外层作用域中的a
## class声明
我们在之前的课程中,已经了解过class相关的用法。这里我们再从语法的角度来看一遍:
```JavaScript
class a {
}
```
class最基本的用法只需要class关键字、名称和一对大括号。它的声明特征跟constlet类似,都是作用于块级作用域,预处理阶段则会屏蔽外部变量。
```JavaScript
const a = 2;
if(true){
console.log(a); //抛错
class a {
}
}
```
class内部,可以使用constructor关键字来定义构造函数。还能定义getter/setter和方法。
```JavaScript
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea();
}
// Method
calcArea() {
return this.height * this.width;
}
}
```
这个例子来自MDN,它展示了构造函数、getter和方法的定义。
以目前的兼容性,class中的属性只能写在构造函数中,相关标准正在TC39讨论。
需要注意,class默认内部的函数定义都是strict模式的。
## 函数声明
函数声明使用 function 关键字。
在上一节课中,我们已经讨论过函数声明对全局作用域的影响了。这一节课,我们来看看函数声明具体的内容,我们先看一下函数声明的几种类型。
```JavaScript
function foo(){
}
function* foo(){
yield 1;
yield 2;
yield 3;
}
async function foo(){
await sleep(3000);
}
async function* foo(){
await sleep(3000);
yield 1;
}
```
\*的函数是generator,我们在前面的部分已经见过它了。生成器函数可以理解为返回一个序列的函数,它的底层是iterator机制。
async函数是可以暂停执行,等待异步操作的函数,它的底层是Promise机制。异步生成器函数则是二者的结合。
函数的参数,可以只写形参名,现在还可以写默认参数和指定多个参数,看下例子:
```JavaScript
function foo(a = 1, ...other) {
console.log(a, other)
}
```
这个形式可以代替一些对参数的处理代码,表意会更加清楚。
## 结语
今天我们一起学习了语句家族,语句分成了普通语句和声明型语句。
普通语句部分,建议你把重点放在循环语句上面。声明型语句我觉得都很重要,尤其是它们的行为。熟练掌握了它们,我们就可以在工作中去综合运用它们,从而减少代码中的错误。新特性大多可以帮助我们发现代码中的错误。
最后留一个小作业,请你找出所有具有Symbol.iterator的原生对象,并且看看它们的for of遍历行为。