语句

141 阅读13分钟

语句就是JavaScript中的句子或命令。JavaScript语句以分号结尾。

1. 表达式语句

JavaScript中最简单的一种语句就是有副效应的表达式。赋值语句是一种主要的表达式语句。

// 赋值
greeting="Hello"+name; 
i*=3;

// 递增递减
counter++;

// 删除
delete o.x

// 函数调用
console.log('test')

2. 复合语句和空语句

语句块将多个语句组合成一个复合语句。

{
    x=Math.PI;
    cx=Math.cos(x);
    console.log("cos(n)="+cx);
}

块不以分号结尾。

很多JavaScript语句也包含子语句。例如,while循环语句只包含一个作为循环体的语句。

空语句是这样的

;

空语句偶尔会有用,比如创建一个空循环体的循环。比如下面的for循环:

//初始化一个数组 
for(let i=0;i<a.length;a[i++]=0);

JavaScript语法要求有一条语句作为循环体,此时空语句(就一个分号)可以派上用场。

意外地在for、while循环或if语句地右括号后面加上分号会导致难以发现的隐患。

if((a===0)||(b===0)); //这行什么也不做 
o=null; //而这行始终都会执行

3. 条件语句

条件语句根据指定表达式的值执行或跳过执行某些语句,因此条件语句是代码中需要决策的地方,有时候也被称作“分支”。

  • if JavaScript的规则(与多数编程语言一样)是,默认情况下else子句属于最近的if语句。为了让这个例子更清晰、易读、易理解、易维护、易调试,应该使用花括号:
if(expression) 
    statement
if(i===j){ 
    if(i===j){ 
        console.log("i equals k"); 
    }
} else { 
    console.log("i doesn't equal j"); 
}
  • switch

所有分析都依赖同一个表达式的值时这并不是最好的办法。因为多个if语句重复对一个表达式进行求值太浪费了。此时最合适的语句是switch语句。

switch(expression){ statements }
switch(n){
   case 1:                   //如果n===1,从这里开始执行
       //执行第一个代码块
       break;                //到这里停止
   case 2:                   //如果n===2,从这里开始执行
      //执行第二个代码块       
      break;                 //到这里停止
   case 3:                   //如果n===3,从这里开始执行
      //执行第三个代码块
      break;                 //到这里停止
   default:
      //执行第四个代码块
      break;                 //到这里停止
}

注意代码中每个case末尾的break关键字。这个break语句(本章后面会介绍)将导致解释器跳到switch语句末尾(或“跑出” switch语句),继续执行后面的语句。switch语句中的case子句只指定了预期代码的起点,并没有指定终点。在没有break语句的情况下,switch语句从匹配其表达式值的case代码块开始执行,一直执行到代码块结束。这种情况偶尔是有用的,比如让代码执行流从某个case标签直接“穿透”到下一个case标签。但99%的时候还是需要注意用break语句来结束每个case (不过在函数中使用switch时,可以使用return语句而非break语句。这两个关键字都可以终止switch语句,阻止执行流进入下一个case)。

因为case表达式是在运行时求值的,所以JavaScript的switch语句与C、C++和Java的switch语句相比有很大差别(特别是性能更低)。在上述其他语言中,case表达式一定是相同类型的编译时常量,而switch语句通常可以编译为效率极高的跳转表。

4 循环语句

while

就像if语句是JavaScript的基本条件控制语句一样,while语句是JavaScript的基本循环语句

while(expression) 
    statement

执行while语句时,解释器首先会求值表达式。

执行while语句时,解释器首先会求值表达式。如果这个表达式的值是假值,则解释器会跳过作为循环体的语句,继续执行程序中的下一条语句。而如果表达式是真值,则解释器会执行语句并且重复,即跳回循环的开头再次求值表达式。

do/while

do/while循环与while循环类似,区别是对循环表达式的测试在循环底部而不是顶部。这意味着循环体始终会至少执行一次。

do statement 
while (expression)

for

for语句提供了比while语句更方便的循环结构。for语句简化了遵循常见模式的循环。

初始化、测试和更新是对循环变量的三个关键操作。for语句将这三个操作分别设定为一个表达式,让这些表达式成为循环语法中明确的部分*:

for(initialize;test;increment) 
    statement

等价于

initialize; 
while(test){ 
    statement increment; 
}

有很多复杂的循环,而且有时候每次迭代要改变的循环变量还不止一个。这种情况是JavaScript中的逗号操作符常见的唯一用武之地。因为逗号操作符可以把多个初始化和递增表达式组合成一个表达式,从而满足for循环的语法要求

let i,j,sum=0; 
for(i=0,j=10;i<10;i++,j--){ 
    sum+=i*j; 
}

对for循环而言,三个表达式中任何一个都可以省略,只有两个分号是必需的。for(;;)与while(true)一样可以编写死循环。

for循环遍历了一个链表数据结构,返回了列表中的最后一个对象(即第一个没有next属性的对象):

function tail(o){ 
    for(;o.next;o=o.next) return o; 
}

for/of

for/of循环专门用于可迭代对象。数组、字符串、集合和映射都是可迭代的。

  • 数组迭代
let data=[1,2,3,4,5,6,7,8,9],sum=0; 
for(let element of data){ 
    sum+=element; 
} 
// sum => 45

数组迭代是“实时”的,即迭代过程中的变化可能会影响迭代的输出。

  • 对象的迭代
let keys="" 
for(let k of Object.keys(o)){ 
    keys+=k 
} 
// =>"xyz"
let sum=0;
for(let v of Object.values(o)){ 
    sum+=v 
} 
// =>6
let pairs="" 
for(let [k,v] of Object.entries(o)){ 
    pairs+=k+v 
} 
// =>"x1y2z3"
  • 字符串迭代
let frequency={};
for(let letter of "mississippi"){
    if(frequency[letter]){
        frequency[letter]++;
    }else{
        frequency[letter]=1;
    }
}

// frequency =>{m: 1, i: 4, s: 4, p: 2}

按Unicode码遍历的。

  • 集合迭代
let text="Na na na na na na na na Batman"
let wordSet=new Set(text.split(" "))
let unique=[]
for(let word of wordSet){
    unique.push(word)
}
// unique => ["Na", "na", "Batman"]
let m=new Map([[1,"one"]]) 
for(let [key,value] of m){ 
    key //=>1 
    value //=>"one" 
}
  • 异步迭代 从异步可迭代流中读取模块并将其打印出来
async function printStream(stream){ 
    for await (let chunk of stream){ 
        console.log(chunk) 
    } 
}

for/in

for(variable in object) statement

  • 执行顺序
    • JS解释器首先求值object表达式。如果它求值为null或undefined,解释器会跳过循环并转移到下一个语句。
    • 每次迭代前,解释器都会求值variable表达式,并将属性名字(字符串值)赋值给它。
  • 枚举内容
    • for/in并不会枚举对象的所有属性,比如它不会枚举名字为符号的属性。
    • 而对于名字为字符串的属性,它只会遍历可枚举的属性。
    • JavaScript核心定义的各种内部方法是不可枚举的。除了内部方法,内部对象的不少其他属性也是不可枚举的。
    • 默认情况下,我们手写代码定义的属性和方法都是可枚举的。
  • 其它情况
    • 继承的可枚举属性,也可以被for/in循环枚举。这意味着如果你使用for/in循环,并且代码中会定义所有对象继承的属性,那你的循环就有可能出现意外结果。为此,很多程序员更愿意基于Object.keys()使用for/of循环,而不是使用for/in循环。

    • 如果for/in循环的循环体删除一个尚未被枚举的属性,则该属性就不会再被枚举了。如果循环体在对象上又定义了新属性,则新属性可能会(也可能不会被枚举)

跳转语句

跳转语句会导致JavaScript解释器跳转到源代码中的新位置。

  • break语句会让解释器跳转到循环末尾或跳转到其他语句。

  • continue语句会让解释器跳出循环体并返回循环顶部开始新一轮迭代。

  • return语句会让解释器从函数调用跳转回调用位置,同时提供调用返回的值。

  • yield语句是一种在生成器函数中间返回的语句。

  • throw语句回抛出异常,设计用来与try/catch/finally语句共同使用,后者可以构成异常处理代码块。 抛出异常是一种复杂的跳转语句:当有异常被抛出时,解释器会跳转到最近的闭合异常处理程序,可能是在同一个函数内部,也可能会上溯到函数调用栈的顶端。

语句标签

identifier: statement

给语句加标签之后,就相对于给它起了个名字,可以在程序的任何地方通过这个名字来引用它。任何语句都有标签,但只有那些给语句体的语句加标签时才有意义,比如循环语句和条件语句。给循环起个名字,然后在循环体中可以使用break和continue退出循环或跳到循环顶部开始下一次迭代。break和continue是JavaScript中唯一使用语句标签的语句。

mainloop:while(token !== null){ 
    //省略的代码 
    continue mainloop; //跳到命名循环的下一次迭代 
    //省略的其他代码 
}

这些标签与变量和函数不在同一个命名空间中,因此同一个标识符即可作为语句标签,也可以作为变量和函数名。

标签的语句本身也可以再加标签,这意味着任何语句都可以有多个标签。

break语句

当break后面跟一个标签时,它会跳转到具有指定标签的包含语句的末尾或终止该语句。

break labelname;

break与labelname中间不允许出现换行符。这主要是因为JavaScript会主动插入省略的分号

想中断一个并非最接近的包含循环或switch语句,就要使用这种带标签的break语句。

let matrix=getData();       //从某个地方取得一个数值的二维数组
//现在计算矩阵中所有数值之和
let sum=0,success=false;
//从一个加标签的语句开始,如果出错可以中断
computeSum:if(matrix){
   for(let x=0;x<matrix.length;x++){
       let row=matrix[x];
       if(!row) break computeSum;
       for(let y=0;y<row.length;y++){
           let cell=row[y];
           if(isNaN(cell))  break computSum;
           sum+=cell;
       }
   }
   success=true;
}
//break语句跳转到这里。如果此时success==false
//那说明得到的matrix出了问题。否则,sum会包含
//这个矩阵中所有单元的和          

最后要注意,无论带不带标签,break语句都不能把控制权转移到函数边界之外。

continue语句

continue不会退出循环,而是从头开始执行循环的下一次迭代。

continue语句也可以带标签。

continue语句都只能在循环体内使用。

return语句

函数调用是表达式,而所有表达式都有值。函数中的return语句指定了函数调用的返回值。

return语句只能出现在函数体内。

function square(x){ return x*x; }            //函数有一个return语句
square(2//=>4

如果没有return语句,函数调用会依次执行函数体中的每个语句,直至函数末尾,然后返回其调用者。此时,调用表达式求值为undefined。return语句常常是函数中的最后一条语句,但并非必须是最后一条。函数体在执行时,只要执行到return语句,就会返回其调用者,而不管这个return语句后面是否还有其他语句。

return语句后面也可以不带expression,从而导致函数向调用者返回undefined。

yield语句

只能用在ES6新增的生成器函数中,以回送生成的值序列中的下一个值,同时又不会真正返回

//回送一系列整数的生成器函数
function* range(from,to){
   for(let i=from;i<=to;i++){
        yield i;
   }
}

throw

异常是一种信号,表示发生了某种意外情形或错误。抛出错误是为了表面发生了这种错误或意外情形。捕获(catch)异常则是要处理它,即采取必要或对应的措施以从异常中恢复。

throw expression;

expression可能求值为任何类型的值,可以抛出一个表示错误的数值,也可以抛出一个包含可读的错误消息的字符串。JavaScript解释器在抛出错误时会使用Error类及其子类,当然我们也可以在自己的代码中使用这些类。Error对象有一个name属性和一个message属性,分别用于指定错误类型和保存传入构造函数的字符串。下面这个例子会收到无效参数时抛出一个Error对象

function factorial(x){
    //如果收到的参数无效,则抛出异常
    if(x<0) throw new Error("x must not be negative");
    //否则,计算一个值并正常返回
    let f;
    for(f=1;x>1;f*=x,x--);
    return f;
}
factorial(4)
=>24
factorial(-1)
=>VM1628:3 Uncaught Error: x must not be negative
    at factorial (<anonymous>:3:19)
    at <anonymous>:1:1

抛出异常时,JavaScript解释器会立即停止正常程序的执行并跳到最近的异常处理程序。异常处理程序是使用try/catch/finally语句中的catch子句编写的。如果发生异常的代码块没有关联的catch子句,解释器会检查最接近的上一层代码块,看是否有与之关联的异常处理程序。这个过程一直持续,直至找到处理程序。如果函数中抛出了异常,但函数体内没有处理这个异常的try/catch/finally语句,则异常会向上传播到调用函数的代码。在这种情况下,异常是沿JavaScript方法的词法结构和调用栈向上传播的。如果没有找到任何异常处理程序,则将异常作为错误报告给用户。

try/catch/finally

try/catch/finally语句是JavaScript的异常处理机制。这个语句的try子句用于定义要处理其中异常的代码块。try块后面紧跟着catch子句,catch是一个语句块,在try块中发生异常时会被调用。catch子句后面是finally块,其中包含清理代码,无论try块中发生了什么,这个块中的代码一定会执行。catch和finally块都是可选的,但只要有try块,就必须有它们两中的一个。try、catch和finally块都以花括号开头和结尾。花括号是语法要求的部分,即使语句中只包含一条语句也不能省略。

try{
   //正常情况下,这里的代码会从头到尾执行不会出现问题。
   //但有时候也可能抛出异常:
   //直接通过throw语句抛出,或者由于调用了一个抛出异常的方法而抛出
}
catch(e){
   //当且仅当try块抛出异常时,才会执行这个
   //块中的语句。这里的语句可以使用局部变量
   //e引用被抛出的Error对象。这个块可以以某种方式来处理异常,
   //也可以什么都不做以忽略异常,还可以通过throw重新抛出异常
}
finally{
  //无论try块中发生什么
  //这个块中包含的语句都会被执行。无论try块是否终止,这些语句
  //都会被执行:
  //    (1)正常情况下,在到达try块底部时被执行
  //    (2)由于break、continue或return语句而执行
  //    (3)由于上面的catch子句处理了异常而执行
  //    (4)由于异常未被处理而继续传播而执行
}

只要执行了try块中的任何代码,finally子句就一定会执行,无论try块中的代码是怎么执行完的。因此finally子句经常用于执行完try子句之后执行代码清理。

如果finally子句执行了子句抛出异常,该异常会代替正被抛出的其他异常。如果finally子句执行了return语句,则相应方法正常返回,即使有被抛出且尚未处理的异常。

//模拟for(initialize;test;increment)循环体
initialize;
while(test){
   try{ body; }
   finally{ increment; }
}

不过需要注意的是,包含break语句的body在while循环与在for循环中的行为会有所不同(在while循环中,break会导致在退出循环前额外执行一次increment)。因此即使使用finally子句,也不能完全通过while来模拟for循环。

其他语句

with

with会运行一个代码块,就好像指定对象的属性是该代码块作用域的变量一样。

这个语句创建了一个临时作用域,以object的属性作为变量,然后在这个作用域中执行statement。

with在严格模式下是被禁用的,在非严格模型下也应该认为已经废弃了。换句话说,尽可能不去使用它。使用with的JavaScript代码很难优化,与不使用with的等价代码相比运行速度明显慢得多。

debugger

包含debugger的程序在运行时,实现可以(但不说必需)执行某种调试操作。实践中,这个语句就像一个断点,执行中的JavaScript会停止

“use strict”

“use strict"是ES5引入的一个指令。指令不是语句

"use strict"与常规语句有两个重要的区别。

  • 不包含任何语言关键字:指令是由(包含在单引号或双引号中的)特殊字符串字面量构成的表达式语句。
  • 只能出现在脚本或函数体的开头,位于所有其他真正的语句之前。

"use strict"指令的目的是表示(在i奥本或函数中)它后面的代码是严格代码。如果脚本中有"use strict"指令,则脚本的顶级(非函数)代码是严格代码。如果函数体是在严格代码中定义的,或者函数体中有一个"use strict"指令,那它就是严格代码。如果严格代码中调用了eval(),那么传给eval()的代码也是严格代码;如果传给eval()的字符串包含了"use strict"指令,那么传给eval()的代码也是严格代码;除了显示声明为严格的代码外,任何位于class体或ES6模块中的代码全部默认为严格代码,而无须把"use strict"指令显式地写出来。

严格模式是JavaScript的一个受限制的子集,这个子集修复了重要的语言缺陷,提供了更强的错误检查,也增强了安全性。例如:

  • 不允许使用with语句。
  • 所有变量都必须声明。如果把值赋给一个标识符,而这个标识符是没有严格声明的变量、函数、函数参数、catch子句参数或全局对象的属性,都会导致抛出一个ReferenceError(在非严格模式下,给全局对象的属性赋值会隐式声明一个全局变量,即给全局对象添加一个新属性)。
  • 函数如果作为函数(而非方法)被调用,其this值为undefined(在非严格模式下,作为函数调用的函数始终以全局对象作为this的值)。
  • 如果函数传给call()或apply()调用,则this就是作为第一个参数传给call()或apply()的值(在非严格模式下,null和undefined值会被替换为全局对象,而非对象值会被转换为对象)。
  • 给不可写的属性赋值或尝试在不可扩展的对象上创建新属性会抛出TypeError。
  • 传给eval()的代码不能像在非严格模式下那样在调用者的作用域中声明变量或定义函数。这种情况下定义的变量和函数会存在于一个为eval()创建的新作用域中。这个作用域在eval()返回时就会被销毁。
  • 函数中的Arguments对象保存着一份传给函数的值的静态副本。在非静态模式下,这个Arguments对象具有“魔法”行为,即这个数组中的元素与函数的命名参数引用相同的值。
  • 如果delete操作符后面跟一个未限定的标识符,比如变量、函数或函数参数,则会导致抛出SyntaxError(在非严格模式下,这样的delete表达式什么也不做,且返回false)。
  • 尝试删除一个不可配置的属性会导致抛出TypeError(在非严格模式下,这个尝试会失败,且delete表达式会求值为false)。
  • 对象字面量定义两个或多个同名属性是语法错误(在非严格模式下,不会发生错误)。
  • 函数声明中有两个或多个同名参数是语法错误(在非严格模式下,不会发生错误) 在严格模式下,不允许使用八进制整数字面量(以0开头后面没有x)(在非严格模式下,某些实现允许使用八进制字面量)
  • 标识符eval和arguments被当作关键字,不允许修改它们的值。不能给这些标识符赋值,不能把它们声明为变量,不能把它们用作函数名或者函数参数名,也不能把它们作为catch块的标识符使用。
  • 检查调用栈的能力是受限制的。arguments.caller和arguments.callee在严格模式中都会抛出TypeError。严格模式函数也有caller和arguments属性,但读取它们会抛出TypeError(某些实现在非严格函数中定义了这些非标准属性)。