第4章 语句表达式和运算符


绩效

章节 代码量(行)
4.1 0
4.2 0
4.3 61

尽管 JavaScript 在语法结构上,有不少地方和 Java 类似,但它有一些自己独有的语句。同样地,在 JavaScript 中,有很多和 Java 相似的运算符和表达式。不过,因为隐式的数据类型转换在 JavaScript中非常普遍,所以与 Java 相比,在使用这一语言的过程中还有其他一些必须注意的地方。

4.1 表达式和语句的构成

JavaScript 的源代码本质上是一个语句的集合。语句是由语句和表达式所构成的。表达式则由表达式和运算符所构成。这种在自身的定义中递归地使用自身的定义方式,在程序设计语言中相当常见。

有人可能会觉得,这种使用了自身的定义方式即使在经过了无限次循环之后,也无法真正地定义出一个概念。不过事实上,语句和表达式都具有不需要用到自身定义的定义方式。因此,这种递归的定义是不会无限循环下去的。对于语句来说,最终都可以被分解为保留字、表达式与符号(括号或是分号等)。也就是说,即使在一条语句中包含其他语句,只要对这条被包含的语句继续进行分解,最终都会到达仅包含保留字、表达式与符号的状态。对于表达式来说,虽然也能在一句表达式中包含其他的表达式,不过只要对所包含的表达式继续进行分解,最终总是能达到仅包含标识符(变量名或是函数名)、字面量(即直接写出其值的数值或是字符串)与运算符(符号或是保留字)的状态。

4.2 保留字

表 4.1 JavaScript 的保留字

名称 说明
关键字 请参见表4.2
今后的保留字 请参见表4.3
null 字面量
true 字面量
false 字面量

表 4.2 关键字

break do instanceof typeof
case else new var
catch finally return void
continue for switch while
default if throw delete
in try

表 4.3 今后的保留字

class enum extends super const export
import implements let private public yield
interface package protected statics

4.3 标识符

标识符是开发者在程序中所定义的单词,例如变量名或是函数名。虽说标识符中可以使用的字符是有所限制的,不过只要不与保留字中的单词重复就没有问题,所以实际上可以生成无限多的标识符。其具体的命名规则如下。

  • 必须是除保留字以外的单词。
  • 必须是除 true、false、null 以外的单词。
  • 必须是以 Unicode 的(非空)字符开始,之后接有Unicode 字符或是数字的单词。
  • 单词的长度并无限制。

不能使用和保留字相同的单词作为标识符。例如,如果有一个函数被命名为 do,那么就会引起 SyntaxError。不过,如果仅仅是在标识符的字符中包含了保留字就没有问题,例如 doit 这样的函数名就是合法的。

var do = 7;
console.log("输出do的值:");
console.log(do);
var doit = 8;
console.log("输出doit的值:");
console.log(doit);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
e:\HMV\JavaScript\JavaScript.js:1
var do = 7;
    ^^

SyntaxError: Unexpected token 'do'
    at wrapSafe (internal/modules/cjs/loader.js:915:16)
    at Module._compile (internal/modules/cjs/loader.js:963:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
    at Module.load (internal/modules/cjs/loader.js:863:32)
    at Function.Module._load (internal/modules/cjs/loader.js:708:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
    at internal/main/run_main_module.js:17:47

[Done] exited with code=1 in 0.193 seconds

true、false 和 null 这三个单词是字面量,不能被用作标识符。就好比数值字面量 1 或是字符串字面量 "abc" 不能被用作标识符一样,这三个字面量也不能被用作标识符。

JavaScript 中的标识符都是以 Unicode 字符所组成的单词。Unicode 字符中包含了日文字符(日文汉字以及平片假名),所以从语法上来说,是可以使用日文作为变量名以及函数名的。不过,由于一些历史原因以及习惯用法,并不推荐使用日文作为标识符。在实际的编程过程中应当遵循以下的规则,即应该使用以英文字符(大写或是小写的英文字符)、_(下划线字符)或是 $(美元字符)开始,之后接有英文字符、_$、数字(0 至 9)的单词。

var hzh = 'hzh';
var HZH = 'HZH';
var _hzh = '_hzh';
var $hzh = '$hzh';

var hzh_ = 'hzh_';
var hzh$ = 'hzh$';
var hzh6 = 'hzh6';

var HZH_ = 'HZH_';
var HZH$ = 'HZH$';
var HZH9 = 'HZH9';

var _hzh_ = '_hzh_';
var _hzh$ = '_hzh$';
var _hzh12 = '_hzh12';

var $hzh_ = '$hzh_';
var $hzh$ = '$hzh$';
var $hzh15 = '$hzh15';

var hzh_$ = 'hzh_$';
var hzh_0 = 'hzh_0';
var hzh$0 = 'hzh$0';

var HZH_$ = 'HZH_$';
var HZH_0 = 'HZH_0';
var HZH$0 = 'HZH$0';

var _hzh_$ = '_hzh_$';
var _hzh_0 = '_hzh_0';
var _hzh$0 = '_hzh$0';

var $hzh_$ = '$hzh_$';
var $hzh_0 = '$hzh_0';
var $hzh$0 = '$hzh$0';

console.log("看看能不能把它们打印出来:");
console.log(hzh,  HZH, _hzh, $hzh, hzh_, hzh$, hzh6);
console.log(HZH_, HZH$, HZH9, _hzh_, _hzh$, _hzh12);
console.log($hzh_, $hzh$, $hzh15, hzh_$, hzh_0);
console.log(hzh$0, HZH_$, HZH_0, HZH$0, _hzh_$);
console.log(_hzh_0, _hzh$0, $hzh_$, $hzh_0, $hzh$0);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
看看能不能把它们打印出来:
hzh HZH _hzh $hzh hzh_ hzh$ hzh6
HZH_ HZH$ HZH9 _hzh_ _hzh$ _hzh12
$hzh_ $hzh$ $hzh15 hzh_$ hzh_0
hzh$0 HZH_$ HZH_0 HZH$0 _hzh_$
_hzh_0 _hzh$0 $hzh_$ $hzh_0 $hzh$0

[Done] exited with code=0 in 0.188 seconds

习惯上,以下划线(_)开始的标识符会被作为“内部标识符”来使用。又因为在 prototype.js 中,getElementById 函数的别名被记为了$,所以一些常用名称的别名常常会使用以美元符号($)开始的标识符。

4.4 字面量

字面量(literal)指的是,在代码中写下这些值之后,将会在运行时直接使用这些值的字面含义。有读者也许会觉得,在代码中书写的值自然会在运行时按原样表达该值,不过事实上并非如此,请看下面的代码:

// 字符串字面量"bar" 的例子
var foo = "bar";

根据语法规则,代码中的 var 这个词的含义是变量的声明,因此,在运行中 var 并不会被识别为一个内容为 var 的单词。类似地,foo 这个词在运行时也不会被识别为一个内容为 foo 的单词,而仅被认为是变量 foo 所表示的值。而即使把代码中所有的 foo 都改写为 foo2 也不会改变运行结果,通过这一事实也能进一步理解该规则。

另一方面,"bar" 是一个字符串字面量,所以 bar 这一单词在运行过程中的含义就是 bar 这一字符序列而已。

数值字面量的情况就更加容易理解了。在下面的代码中写有两个数值 0。val0 中的 0 是其变量名的一部分,并不具有数值0 的含义。这个 0 已经失去了可以进行算术运算的性质,仅仅是一个符号。

另一方面,右侧的字面量 0 则具有数值的含义。

// 数值字面量0 的例子
var val0 = 0;

【评】如果设置一个变量为var hzh = '黄子涵',那么变量hzh表示的‘黄子涵’这个字符串,改变标识符也改变不了字面量的意思。

字面量

表 4.4 字面量
名称 具体示例
数值 100
字符串值 "foobar"
布尔值 true
null 值 null
Object { x:1, y:2 }
数列 [3, 1, 2]
函数 function() { return 0; }
正则表达式 /foo/

4.5 语句

在程序设计语言中,语句(statement)的定义可以由该语言经过明确定义的语法(syntax)规则得到,并且可以在运行程序时执行(execute)语句。换一种角度来说的话,所谓运行一个程序,指的就是执行程序中一条条的语句。

虽然说,源代码中的语句并不一定是和运行中的每一步一一对应的,不过考虑到程序在运行时确实是在逐一执行语句,所以在概念上并不矛盾。

在 JavaScript 中,语句之间使用分号分隔。严格来说,分号只是一部分语句的结尾。例如对于表达式语句,其结尾必须使用分号;而对于复合语句,其结尾则是不需要分号。对于这方面的规则,JavaScript 和 Java 基本相同,不过 JavaScript 对于分号的使用限制更为宽松。例如在 JavaScript 中,换行时所省略的分号将被自动补全。

4.6 代码块(复合语句)

代码块是在大括号({})中所写的语句,以此将多条语句的集合视为一条语句来使用。这样一来,从语法上来说,代码中所有能够书写语句的地方都可以书写多条语句。

值得注意的是,JavaScript(准确地说是 ECMAScript)的代码块中的变量并不存在块级作用域这样的概念。

4.7 变量声明语句

// 变量声明语句的格式为,在关键字 var 之后跟上所需的变量名。
var hzh1;
// 在多个变量名之间使用逗号(,)分隔的话,就能够同时声明多个变量。
var hzh2, hzh3;
// 而使用 = 运算符,就可以在声明的同时对变量进行初始化。
var hzh4 = '黄子涵', hzh5 = '是帅哥!'

4.8 函数声明语句

JavaScript 中的函数声明语句,和 Java 中方法的定义语句在语法上是基本相同的,不同之处在于,函数声明语句并不是以返回值类型开始,而是使用了关键字 function,并且在JavaScript 中不用参数指定类型。

尽管在 ECMAScript 标准中,函数声明语句并没有被视为语句的一种。

// 函数声明语句的语法

function 函数名 (参数, 参数, ......) {
    语句
    语句
    ......
}

函数名和参数的位置上所书写的是标识符。参数的数量没有限制,所以即使一个参数也没有关系。大括号中的是函数体,里面可以书写多条语句。

4.9 表达式语句

Script 不同于 Java 那样,Java 只有一部分的表达式能够被作为语句使用,而在 JavaScript 中,所有的表达式都可以被视为一条表达式语句。不过很可惜,JavaScript 的这一特性并不是一个优点。

// 虽然没有意义,但是语法上并没有错误的代码
// 在相等运算符(==)的表达式从语法上来说属于是表达式语句
var hzh1;
hzh1 == 0; // 一条表达式语句(但是没有任何的实际效果)
// 执行上面的表达式语句并不会有任何效果。
// 又因为不会引起语法错误,所以即使不小心把 == 错写成了 = 也不容易被发现
// 而在 Java 中,这类没有意义的表达式语句将会引起编译错误。
// 能够很容易地发现问题,反而是一种优点
var hzh2;
hzh2 = '黄子涵';   // 赋值表达式的表达式语句
console.log(hzh2); // 函数调用表达式的表达式语句
[Running] node "e:\HMV\JavaScript\JavaScript.js"
黄子涵

[Done] exited with code=0 in 0.816 seconds

4.10 空语句

仅含有分号的语句就是空语句。仅在一部分场合下空语句才有其使用价值。

// 空的代码块
while (条件表达式) {

}

// 包含了空语句的代码块
while (条件表达式) {
    ;
}

// 仅有空语句
while (条件表达式)
    ;

4.11 控制语句

有一类语法规则被称为控制语句。控制语句包括条件分支、循环、跳转(包括异常处理)这 3 类。如果没有这样的控制语句,JavaScript 在理论上,是按照源代码上所写的代码顺序从上至下地执行。这种执行方式被称为“顺序执行”。

有了控制语句之后,就可以实现顺序执行以外的代码执行方式。

4.12 if-else语句

if 语句和 if-else 语句的语法结构如下。其中的条件表达式和语句不能省略。

// if 语句的语法

if ( 条件表达式 )
    语句

// if-else 语句的语法

if ( 条件表达式 )
     语句
else
     语句

与 if 对应的条件表达式及语句统称为 if 子句,而与 else 对应的条件表达式与语句则统称为 else 子句。可以把 if 表达式看作 if-else 表达式省略了 else 子句的特殊情况。

在条件表达式的位置所写的式子,将被求值并转换为布尔型。这一隐式的数据类型转换常常会带来各种各样的错误。

下面是一个 if-else 语句的具体示例。

// if-else语句的例子
var hzh1 = 0; 
if(hzh1 == 0) {
    console.log("if分句");
}
else {
    console.log("else分句");
}
[Running] node "e:\HMV\JavaScript\JavaScript.js"
if分句

[Done] exited with code=0 in 12.715 seconds

如果变量hzh1的值为0,则输出“if分句”,否则,输出“else分句”。

在 if 子句以及 else 子句中可以书写任意的语句。又因为 if-else 语句本身也是语句的一种,所以也能够在 if-else 语句的子句中再次使用 if-else 语句。下面是一个在 if 子句中使用 if-else 语句的例子。

// 嵌套if-else语句
var hzh2 = 0;
var hzh3 = 0;

if(hzh2 == 0) {
    if(hzh3 == 0) {
        console.log("hzh2==0 and hzh3==0");
    }
    else {
        console.log("hzh2==0 and hzh3!=0");
    } 
 }

 else
    console.log("hzh2!=0");
[Running] node "e:\HMV\JavaScript\JavaScript.js"
hzh2==0 and hzh3==0

[Done] exited with code=0 in 0.238 seconds

上面这段代码的执行结果和所预期的结果是一致的。接下来再来考虑一下没有外层 else 子句的嵌套 if-else 语句的情况。如果保持原有的缩进情况不变,将得到如下的代码。

var hzh4 = 0;
var hzh5 = 0;

if(hzh4 == 0) {
    if(hzh5 == 0) {
        console.log("hzh4==0 and hzh5==0");
    }
    else {
        console.log("hzh4==0 and hzh5!=0");
    } 
 }
[Running] node "e:\HMV\JavaScript\JavaScript.js"
hzh4==0 and hzh5==0

[Done] exited with code=0 in 0.274 seconds

也可以对其缩进进行修改,得到如下代码。

// 容易混淆的缩进
var hzh6 = 0;
var hzh7 = 0;
if (hzh6 == 0) {
    if (hzh7 == 0) {
        console.log("hzh6==0 and hzh7==0");
    }
    else
        console.log("hzh6==0 and hzh7!=0");
}
[Running] node "e:\HMV\JavaScript\JavaScript.js"
hzh6==0 and hzh7==0

[Done] exited with code=0 in 0.995 seconds

在上面的代码中,从缩进方式来看,似乎 else 子句是与外层 的 if 子句相对应的。但事实上,缩进对代码的实际意义不会产生影响。换句话说,在上述两段代码中,必然存在一组代码,其实际执行方式与代码缩进格式所暗示的方式有所不同。对于这个问题的回答是,由于 JavaScript 中有 else 子句必定与最邻近的 if 子句相结合的规则,因此在本例中,else 子句是和内层的 if 子句相对应的。这也就意味着,第二组
代码的缩进方式与其实际的执行方式是不相符的。

为了避免产生这样容易混淆的情况,可以使用支持自动缩进的文本编辑器。不过,其实只要始终使用代码块来书写 if 子句和 else 子句的话,就能够避免这一问题了,所以,在此更加推荐使用这种通用性更高的解决方式。

// 通过代码块来避免出现容易使人误解的缩进

var hzh1 = 0;
var hzh2 = 0;

if(hzh1 == 0) {
    if(hzh2 == 0) {
        console.log("hzh1 == 0 和 hzh2 == 0");
    } else {
        console.log("hzh1 == 0 和 hzh2 != 0");
    }
}
[Running] node "e:\HMV\Babel\hzh.js"
hzh1 == 0 和 hzh2 == 0

[Done] exited with code=0 in 0.361 seconds

按照自己的习惯就好,这部分省略。

4.13 switch-case语句

switch-case 语句是一种语法结构与 if-else 有所不同的条件分支判断语句。其语法结构如下。

// switch-case 语句的语法
switch ( 语句 ) {
    case 表达式 1:
        语句
        语句
        ......
    case 表达式 2:
        语句
        语句
        ......
    case 表达式 N:
        语句
        语句
        ......
    default:
        语句
        语句
        ......
}

根据习惯,“case 表达式:”部分被称为 case 标签,而 “default:” 部分被称为 default 标签。从语法规则的角度来看,它们与 if-else 语句中子句的功能不同,起到的是跳转目标的作用。在 switch 语句中可以书写多个 case 标签,而 default 标签则只能使用 1 次。此外,default 标签是可以省略的。

尽管 JavaScript 中的 switch 语句在语法结构上与 Java 的相同,但它们在实际的语法规则上却有着一些细微的差异。在 JavaScript 中,switch 的括号内可以写任意类型的表达式,case 标签中也可以写任意的表达式。与之相对应地,在 Java 的 case 标签中,则只能使用在编译时就能够获得结果的常量表达
式。

switch 语句会把其在 switch 之后的括号内的表达式,与 case 标签中所写的各个表达式,依次通过相等运算符(===)进行比较。为了避免用词混淆,这里将前者称为 switch 表达式,而将后者称为 case 表达式。=== 运算符是不会进行数据类型转换的相等运算符。switch 语句首先对 switch 表达式进行求值,之后依次对 case 表达式从上往下求值,并将其结果与 switch 表达式的求值结果进行等值比较(===)。如果值相等,则跳转至该 case 标签处。如果与所有的 case 表达式都不等值,则跳转至 default 标签处。

下面的例子主要展示了一些在 Java 中不被允许的类型。

代码清单 4.1 switch语句的例子

var hzh = '黄子涵';

switch (hzh) { // 可以在switch表达式中使用字符串值
    // 可以在case表达式中使用和switch表达式类型不同的值
    // hzh === 0 的值,所以将继续进行比较
    case 0:
        console.log("不在这里");
        break;

    // 可以在case表达式中使用含有变量的表达式
    // hzh === hzh.length 的值为假,所以继续进行比较
    case hzh.length: 
        console.log("不在这里");
        break;
        
    // 可以在 case 表达式中使用方法调用表达式
    // s === (0).toString()的值为假,所以将继续进行比较
    case (0).toString():
        console.log("不在这里");

    // 还可以在case表达式中书写这样的表达式
    // hzh === '黄' + '子' + '涵'为真,所以执行以下的代码
    case '黄' + '子' + '涵':
        console.log('在这里');
        break;
    
    // 如果所有的case表达式在等值运算(===)后得到的结果都为假,则执行以下的代码
    default: 
        console.log('不在这里');
        break;
}
[Running] node "e:\HMV\Babel\hzh.js"
在这里

[Done] exited with code=0 in 1.498 seconds

一看,case 标签之间的部分是作为一个整体来执行的,不过实际上,case 标签并没有对代码按块进行分割的功能。因此在一个 case 标签结束执行之后,并不会跳出 switch 语句。

在代码清单 4.2 的 switch 语句中,虽然第一个 case 标签的比较结果就为真,但之后所有的 case 标签也都会被执行。

代码清单 4.2 没有 break 语句的 switch 语句,将不会在执行完其中某一段 case 之后就结束整个 switch 语句

var hzh = 0;
switch (hzh) {
    case 0:
        console.log('黄子涵是帅哥!');
    case 1:
        console.log('黄子涵是靓仔!');
    case 2:
        console.log('黄子涵真聪明!');
    case 3:
        console.log('黄子涵真厉害!');
    default:
        console.log('黄子涵');
        break;
}
[Running] node "e:\HMV\JavaScript\JavaScript.js"
黄子涵是帅哥!
黄子涵是靓仔!
黄子涵真聪明!
黄子涵真厉害!
黄子涵

[Done] exited with code=0 in 2.271 seconds

由这一结果可知,应该将 case 标签与 default 标签看作跳转的目标地址。也就是应该这样来理解 case 标签的作用:当 switch 表达式与 case 表达式的值相一致时,就跳至该 case 标签所在位置,执行完该段代码之后继续从上往下逐次执行后续语句。

在很多时候,上面这样的代码都无法得到预期的结果。像代码清单 4.3 这样使用 break 语句就可以强制跳出当前 switch 语句。

代码清单 4.3 通过 break 语句跳出 switch 语句

var hzh = Math.round(Math.random()*10);
switch (hzh) {
    case 0:
        console.log('黄子涵是帅哥!');
        break;
    case 1:
        console.log('黄子涵是靓仔!');
        break;
    case 2:
        console.log('黄子涵真聪明!');
        break;
    case 3:
        console.log('黄子涵真厉害!');
        break;
    case 4:
        console.log('我是你爹!');
        break;
    case 5: 
        console.log('我是你妈!');
        break;
    case 6: 
        console.log('我是你爷!');
        break;
    case 7:
        console.log('黄子涵比彭于晏帅!');
        break;
    case 8:
        console.log('黄子涵比尤雨溪聪明!');    
        break;
    default:
        console.log('黄子涵');
        break;
}

4.14 循环语句

// while 语句
while (条件表达式)
    语句

// do-while语句
do
    语句
while (条件表达式);

// for语句
for (初始化表达式; 条件表达式; 更新表达式)
    语句

// for each in 语句(非ECMAScript标准功能)
for each(表达式 in 对象表达式)
语句

4.15 while语句

// while 语句的语法规则

while (条件表达式)
    语句

4.16 do-while语句

// do-while 语句的语法结构

do {
    语句
} while (条件表达式);

4.17 for语句

// for语句的语法

for (初始化表达式; 条件表达式; 更新表达式)
    语句

4.18 for in语句

// for in 语句的语法结构
for (变量 in 对象表达式)
    语句

4.19 for each in语句

// for each in 语句的语法

for each(变量 in 对象表达式)
    语句

4.20 break语句

4.21 continue语句

4.22 通过标签跳转

// 标签的语法规则

标签字符串: 语句

4.23 return语句

// return 语句的语法结构

return 表达式;

4.24 异常

// throw 语句的语法规则

throw 表达式;
// try-catch-finally 结构的语法

try {
    语句
    语句
    ......
} catch (变量名) { // 该变量是一个引用了所捕捉到的异常对象的局部变量
    语句
    语句
    ......
} finally {
    语句
    语句
    ......
}

4.25 其他

// with 语句的语法结构

with (表达式)
      语句

4.26 注释

注释分为以下两种类型。

// 单行注释
/* 注释 */

4.27 表达式

4.28 运算符

4.29 表达式求值

4.30 运算符的优先级以及结合律

4.31 算术运算符

4.32 字符串连接运算符

4.33 相等运算符

4.34 比较运算符

4.35 in运算符

4.36 instanceof运算符

4.37 逻辑运算符

4.38 位运算符

4.39 赋值运算符

4.40 算术赋值运算符

4.41 条件运算符(三目运算符)

// 条件运算符表达式的语法

条件表达式 ? 表达式1 : 表达式2

4.42 typeof运算符

4.43 new运算符

4.44 delete运算符

4.45 void运算符

4.46 逗号(,)运算符

4.47 点运算符和中括号运算符

字符 .(点)称为点运算符,中括号 [] 称为中括号运算符,它们都是用于访问属性的运算符。虽然这两个运算符不太显眼,却有着很重要的作用。

其左操作数为对象引用,右操作数为属性名。如果左操作数不是对象引用的话,则会被转换为 Object 类型。点运算符的右操作数是一个用于表示属性名的标识符,而中括号运算符的右操作数为字符串型或是可以被转换为字符串型的值。

4.48 函数调用运算符

4.49 运算符使用以及数据类型转换中需要注意的地方

相关