旧游无处不堪寻
无寻处,惟有少年心
JavaScript(三)

这一篇,我们来学习一下 JavaScript 基本概念中的操作符、语句以及函数。

操作符


ECMA 描述了一组用于操作数据值的操作符。包括: 算数操作符、位操作符、关系操作符以及相等操作符等。

ECMAScript 操作符与众不同之处在于,他们能操作很多值,如字符串、数字值、布尔值甚至对象。在应用于对象时,相应操作符都会调用对象的 valueOf() 或 toString() 方法,以便取得可操作的值。

一元操作符

只有一个操作数的操作符称为一元操作符。

自增和自减操作符

借鉴于 C 语言。有两个版本: 前置以及后置。

var a = 10;
++a;

var b = 20;
b++;

注意: 自增自减操作符不仅可以用于整数,还可以用于字符串、布尔值、浮点数以及对象。在应用于不同值时,遵循下面规则:

  • 在应用于一个包含有效数字字符的字符串时,先将其转换为数字,在执行自增自减操作
  • 在应用于一个不包含有效数字字符的字符串时,将变量的值设为 NaN
  • 在应用于一个布尔值 false 时,先将其转换为 0,在执行自增自减操作
  • 在应用于一个布尔值 true 时,先将其转换为 1,在执行自增自减操作
  • 在应用于一个浮点数时,执行自增自减操作
  • 在应用于一个对象,先调用对象的 valueOf() 方法,在按上述规则执行,若返回 NaN,则再调用 toString() 方法后,再按照上述规则执行

一元加减操作符

我们只需注意,在对非数值应用一元加减操作符时,与调用 Number() 转型函数的规则一样。

位操作符

按照内存中表示数值的位来操作数值。

按位非(NOT)

按位非操作符由一个 ~ 表示。按位非的结果就是返回数值的反码。

var num1 = 25;  // 二进制 00000000000000000000000000011001
var num2 = ~num1; // 二进制 11111111111111111111111111100110
alert(num2); // -26

按位与(AND)

按位与操作符由一个 & 表示。有两个操作数,将两个操作数每一位对齐,进行位操作。

var num1 = 25;  //00000000000000000000000000011001
var num2 = 3; //00000000000000000000000000000011
var num3 = num1 & num2; //00000000000000000000000000000001

按位或(OR)

按位或操作符由一个 | 表示。也有两个操作数,将两个操作数每一位对齐,进行位操作。

var num1 = 25;  //00000000000000000000000000011001
var num2 = 3; //00000000000000000000000000000011
var num3 = num1 | num2; //00000000000000000000000000011011

按位异或(XOR)

按位异或操作符由一个 ^ 表示。也有两个操作数,将两个操作数每一位对齐,进行位操作。

var num1 = 25;  //00000000000000000000000000011001
var num2 = 3; //00000000000000000000000000000011
var num3 = num1 ^ num2; //00000000000000000000000000011010

左移

左移操作符由 << 表示。这个操作符会将数值的位向左移动指定位数。

var oldValue = 2;  // 二进制 10
var newValue = oldValue << 5; // 二进制 1000000,十进制 64

注意: 左移不会影响符号位。即将 -2 左移 5 位,结果为 -64。

有符号右移

有符号右移操作符由 >> 表示,操作符会将数值向右移动并保留符号位。

var oldValue = 64;  // 二进制 1000000 
var newValue = oldValue >> 5; // 二进制 10,十进制 2

无符号右移

无符号右移操作符由 >>> 表示,无符号右移以 0 填充空位。

var oldValue = -64;  // 二进制 11111111111111111111111111000000 
var newValue = oldValue >>> 5; // 十进制 134217726

逻辑操作符

逻辑操作符一共有三个:

  • 逻辑非(NOT)
  • 逻辑与(AND)
  • 逻辑或(OR)

逻辑非

逻辑非操作符由!表示,他可以应用于 ECMAScript 中的任何值。无论这个值的类型是什么,这个操作符都会返回一个布尔值,逻辑非操作符首先会将他的操作数转换为一个布尔值,然后对其取反。规则如下:

  • 如果操作数是一个对象,返回 false
  • 如果操作数是一个空字符串,返回 true
  • 如果操作数是一个非空字符串,返回 false
  • 如果操作数是数值 0,返回 true
  • 如果操作数是非空数值,返回 false
  • 如果操作数是 null,返回 true
  • 如果操作数是 NaN,返回 true
  • 如果操作数是 undefined,返回 true

逻辑非操作符也可用于将一个值转换为与其对应的布尔值,即同时使用两个逻辑非操作符,会模拟转型函数 Boolean() 的行为,结果也与该函数结果相同。

逻辑与

逻辑与操作符由 && 表示,有两个操作数,可以是任意类型。在有一个操作数不是布尔值的情况下,操作结果不一定返回布尔值,遵循如下规则:

  • 如果第一个操作数是对象,则返回第二个操作数
  • 如果第二个操作数是对象,则只有在第一个操作数的求值结果为 true 时,才返回该对象。
  • 如果两个操作数都是对象,则返回第二个操作数
  • 如果有一个操作数是 null,则返回 null
  • 如果有一个操作数是 NaN,则返回 NaN
  • 如果有一个操作数是 undefined,则返回 undefined

并且要注意,逻辑与是短路操作,如果第一个操作数能决定结果,那么就不会对第二个操作数求值。

逻辑或

逻辑或操作符由 || 表示,有两个操作数,可以是任意类型。在有一个操作数不是布尔值的情况下,操作结果也不一定返回布尔值,遵循如下规则:

  • 如果第一个操作数是对象,则返回第一个操作数
  • 如果第一个操作数的求值结果为 false,则返回第二个操作数
  • 如果两个操作数都是对象,则返回第一个操作数
  • 如果两个操作数都是 null,则返回 null
  • 如果两个操作数都是 NaN,则返回 NaN
  • 如果两个操作数都是 undefined,则返回 undefined

逻辑或也是短路操作,如果第一个操作数能决定结果,那么就不会对第二个操作数求值。

乘性操作符

ECMAScript 定义了3个乘性操作符: 乘法、除法和求模。这些操作符与其他语言的相应操作符用途类似,只不过在操作数为非数值的情况下会执行自动的类型转换。如果参与乘法计算的某个操作数不是数值,后台会先使用 Number() 转换为数值。

乘法

乘法操作符由一个星号 * 表示,用于计算两个数值的乘积。
在处理特殊值的情况下,乘法操作符遵循下列特殊的规则:

  • 如果操作数都是数值,执行常规的乘法计算,即两个正数或两个负数相乘的结果还是正数,而如果只有一个操作数有符号,那么结果就是负数。如果乘积超过了 ECMAScript 数值的表示范围,则返回 Infinity 或 -Infinity
  • 如果有一个操作数是 NaN,则结果是 NaN
  • 如果是 Infinity 与 0 相乘,则结果是 NaN
  • 如果是 Infinity 与非 0 数值相乘,则结果是 Infinity 或 -Infinity,取决于有符号操作数的符号
  • 如果是 Infinity 与 Infinity 相乘,则结果是 Infinity
  • 如果有一个操作数不是数值,则在后台调用 Number() 将其转换为数值,然后再应用上面的规则

除法

除法操作符由一个斜线符号 / 表示,执行第二个操作数除第一个操作数的计算。
与乘法操作符类似,除法操作符对特殊的值也有特殊的处理规则。这些规则如下:

  • 如果操作数都是数值,执行常规的除法计算,即两个正数或两个负数相除的结果还是正数,而如果只有一个操作数有符号,那么结果就是负数。如果商超过了 ECMAScript 数值的表示范围,则返回 Infinity 或 -Infinity
  • 如果有一个操作数是 NaN,则结果是 NaN
  • 如果是 Infinity 被 Infinity 除,则结果是 NaN
  • 如果是零被零除,则结果是 NaN
  • 如果是非零的有限数被零除,则结果是 Infinity 或 -Infinity,取决于有符号操作数的符号
  • 如果是 Infinity 被任何非零数值除,则结果是 Infinity 或 -Infinity,取决于有符号操作数的符号
  • 如果有一个操作数不是数值,则在后台调用 Number() 将其转换为数值,然后再应用上面的规则

求模

求模(余数)操作符由一个百分号 % 表示。
与另外两个乘性操作符类似,求模操作符会遵循下列特殊规则来处理特殊的值:

  • 如果操作数都是数值,执行常规的除法计算,返回除得的余数
  • 如果被除数是无穷大值而除数是有限大的数值,则结果是 NaN
  • 如果被除数是有限大的数值而除数是零,则结果是 NaN
  • 如果是 Infinity 被 Infinity 除,则结果是 NaN
  • 如果被除数是有限大的数值而除数是无穷大的数值,则结果是被除数
  • 如果被除数是零,则结果是零
  • 如果有一个操作数不是数值,则在后台调用 Number() 将其转换为数值,然后再应用上面的规则

加性操作符

加法和减法这两个加性操作符应该说是编程语言中最简单的算术操作符了。但是在ECMAScript中,这两个操作符却都有一系列的特殊行为。

加法

如果两个操作符都是数值,执行常规的加法计算,然后根据下列规则返回结果:

  • 如果有一个操作数是 NaN,则结果是 NaN
  • 如果是 Infinity 加 Infinity,则结果是 Infinity
  • 如果是 -Infinity 加 -Infinity,则结果是 -Infinity
  • 如果是 Infinity 加 -Infinity,则结果是 NaN
  • 如果是 +0 加 +0,则结果是 +0
  • 如果是 -0 加 -0,则结果是 -0
  • 如果是 +0 加 -0,则结果是 +0

不过,如果有一个操作数是字符串,那么就要应用如下规则:

  • 如果两个操作数都是字符串,则将第二个操作数与第一个操作数拼接起来
  • 如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来

减法

与加法操作符类似,ECMAScript 中的减法操作符在处理各种数据类型转换时,同样需要遵循一些特殊规则,如下所示:

  • 如果两个操作符都是数值,则执行常规的算术减法操作并返回结果
  • 如果有一个操作数是 NaN,则结果是 NaN
  • 如果是 Infinity 减 Infinity,则结果是 NaN
  • 如果是 -Infinity 减 -Infinity,则结果是 NaN
  • 如果是 Infinity 减 -Infinity,则结果是 Infinity
  • 如果是 -Infinity 减 Infinity,则结果是 -Infinity
  • 如果是 +0 减 +0,则结果是 +0
  • 如果是 +0 减 -0,则结果是 -0
  • 如果是 -0 减 -0,则结果是 +0
  • 如果有一个操作数是字符串、布尔值、null 或 undefined,则先在后台调用 Number() 函数将其转换为数值,然后再根据前面的规则执行减法计算。如果转换的结果是 NaN,则减法的结果就是 NaN
  • 如果有一个操作数是对象,则调用对象的 valueOf() 方法以取得表示该对象的数值。如果得到的值是 NaN,则减法的结果就是 NaN。如果对象没有 valueOf() 方法,则调用其 toString() 方法并将得到的字符串转换为数值

关系操作符

小于(<)、大于(>)、小于等于(<=)和大于等于(>=)这几个关系操作符用于对两个值进行比较。
与 ECMAScript 中的其他操作符一样,当关系操作符的操作数使用了非数值时,也要进行数据转换或完成某些奇怪的操作。以下就是相应的规则:

  • 如果两个操作数都是数值,则执行数值比较
  • 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值
  • 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较
  • 如果一个操作数是对象,则调用这个对象的 valueOf() 方法,用得到的结果按照前面的规则执行比较。如果对象没有 valueOf() 方法,则调用 toString() 方法,并用得到的结果根据前面的规则执行比较
  • 如果一个操作数是布尔值,则先将其转换为数值,然后再执行比较

在使用关系操作符比较两个字符串时,会执行一种奇怪的操作。很多人都会认为,在比较字符串值时,小于的意思是”在字母表中的位置靠前”,而大于则意味着”在字母表中的位置靠后”,但实际上完全不是那么回事。在比较字符串时,实际比较的是两个字符串中对应位置的每个字符的字符编码值。经过这么一番比较之后,再返回一个布尔值。由于大写字母的字符编码全部小于小写字母的字符编码,因此我们就会看到如下所示的奇怪现象:

var result = "Brick" < "alphabet";    //true

另一种奇怪的现象发生在比较两个数字字符串的情况下,比如下面这个例子:

var result = "23" < "3";    //true

当比较字符串”23”是否小于”3”时,结果为 true。这是因为两个操作数都是字符串,而字符串比较的是字符编码(“2”的字符编码是 50,而”3”的字符编码是 51)

相等操作符

确定两个变量是否相等是编程中的一个非常重要的操作。在比较字符串、数值和布尔值的相等性时,问题还比较简单。但在涉及到对象的比较时,问题就变得复杂了。最早的 ECMAScript 中的相等和不等操作符会在执行比较之前,先将对象转换成相似的类型。后来,有人提出了这种转换到底是否合理的质疑。最后,ECMAScript 的解决方案就是提供两组操作符: 相等和不相等——先转换再比较,全等和不全等——仅比较而不转换。

相等

ECMAScript 中的相等操作符由两个等于号 == 表示,不相等操作符由叹号后跟等于号 != 表示。
这两个操作符都会先转换操作数(通常称为强制转型),然后再比较它们的相等性。
在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:

  • 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值 —— false 转换为0,而 true 转换为1
  • 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值
  • 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较

这两个操作符在进行比较时则要遵循下列规则:

  • null和undefined是相等的
  • 要比较相等性之前,不能将 null 和 undefined 转换成其他任何值
  • 如果有一个操作数是 NaN,则相等操作符返回 false,而不相等操作符返回 true。重要提示: 即使两个操作数都是 NaN,相等操作符也返回 false; 因为按照规则,NaN 不等于 NaN
  • 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true; 否则,返回 false

下表列出了一些特殊情况及比较结果:

null == undefined // true 
undefined == 0 // false
null == 0 // false
true == 1 // true
true == 2 // false
false == 0 // true
"NaN" == NaN // false
NaN == NaN // false
NaN != NaN // true
5 == NaN // false
"5"==5 // true

全等

全等操作符由3个等于号 === 表示,它只在两个操作数未经转换就相等的情况下返回 true。
如下:

var result1 = ("55" == 55);     //true,因为转换后相等
var result2 = ("55" === 55); //false,因为不同的数据类型不相等

记住: null == undefined 会返回 true,因为它们是类似的值,但 null === undefined 会返回 false,因为它们是不同类型的值。

三目操作符

variable = boolean_expression ? true_value : false_value;

本质上,这行代码的含义就是基于对 boolean_expression 求值的结果,决定给变量 variable 赋什么值。如果求值结果为 true,则给变量 variable 赋 true_value 值; 如果求值结果为 false,则给变量 variable 赋 false_value 值。

赋值操作符

简单的赋值操作符由等于号(=)表示,其作用就是把右侧的值赋给左侧的变量。

逗号操作符

使用逗号操作符可以在一条语句中执行多个操作,如下面的例子所示:

var num1 = 1, num2 = 2, num3 = 3;

逗号操作符多用于声明多个变量,但除此之外,逗号操作符还可以用于赋值。在用于赋值时,逗号操作符总会返回表达式中的最后一项。

语句


ECMA-262 规定了一组语句(也称为流控制语句)。从本质上看,语句定义了 ECMAScript 中的主要语法。

if 语句

以下是if语句的语法:

if (condition) {
statement1;
} else if {
statement2;
} else {
statement3;
}

其中的 condition(条件)可以是任意表达式,而且对这个表达式求值的结果不一定是布尔值。ECMAScript 会自动调用 Boolean() 转换函数将这个表达式的结果转换为一个布尔值。

do-while 语句

do-while 语句是一种后测试循环语句,即只有在循环体中的代码执行之后,才会测试出口条件。

do {
statement;
} while (expression);

while 语句

while 语句属于前测试循环语句,也就是说,在循环体内的代码被执行之前,就会对出口条件求值。

while(expression) {
statement
}

for 语句

for 语句也是一种前测试循环语句,但它具有在执行循环之前初始化变量和定义循环后要执行的代码的能力。

for (initialization; expression; post-loop-expression) {
statement
}

有必要指出的是,在 for 循环的变量初始化表达式中,也可以不使用 var 关键字,该变量的初始化可以在外部执行。

var count = 10;
var i;
for (i = 0; i < count; i++){
alert(i);
}

以上代码与在循环初始化表达式中声明变量的效果是一样的。由于 ECMAScript 中不存在块级作用域,因此在循环内部定义的变量也可以在外部访问到。

var count = 10;
for (var i = 0; i < count; i++){
alert(i);
}
alert(i); //10

for-in 语句

for-in 语句是一种精准的迭代语句,可以用来枚举对象的属性。

for (property in expression) {
statement
}

ECMAScript 对象的属性没有顺序。因此,通过 for-in 循环输出的属性名的顺序是不可预测的。具体来讲,所有属性都会被返回一次,但返回的先后次序可能会因浏览器而异。
但是,如果表示要迭代的对象的变量值为 null 或 undefined,for-in 语句会抛出错误。ECMAScript 5 更正了这一行为; 对这种情况不再抛出错误,而只是不执行循环体。

label 语句

使用 label 语句可以在代码中添加标签,以便将来使用。

label: statement

加标签的语句一般都要与 for 语句等循环语句配合使用。

break 和 continue 语句

break 和 continue 语句用于在循环中精确地控制代码的执行。其中:

  • break 语句会立即退出循环,强制继续执行循环后面的语句
  • continue 语句虽然也是立即退出循环,但退出循环后会从循环的顶部继续执行

with 语句

with 语句的作用是将代码的作用域设置到一个特定的对象中。

with (expression) {
statement;
}
with(location){
var qs = search.substring(1);
var hostName = hostname;
var url = href;
}

上述例子中,使用 with 语句关联了 location 对象。这意味着在 with 语句的代码块内部,每个变量首先被认为是一个局部变量,而如果在局部环境中找不到该变量的定义,就会查询 location 对象中是否有同名的属性。如果发现了同名属性,则以 location 对象属性的值作为变量的值。
严格模式下不允许使用 with 语句,否则将视为语法错误。
由于大量使用 with 语句会导致性能下降,同时也会给调试代码造成困难,因此在开发大型应用程序时,不建议使用with语句。

switch 语句

switch 语句与 if 语句的关系最为密切,而且也是在其他语言中普遍使用的一种流控制语句。

switch (expression) {
case value: statement
break;
case value: statement
break;
case value: statement
break;
case value: statement
break;
default: statement
}

break 关键字会导致代码执行流跳出 switch 语句。如果省略 break 关键字,就会导致执行完当前 case 后,继续执行下一个 case。
注意: 可以在 switch 语句中使用任何数据类型(在很多其他语言中只能使用数值),无论是字符串,还是对象都没有问题。其次,每个 case 的值不一定是常量,可以是变量,甚至是表达式。
switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换。

函数


函数对任何语言来说都是一个核心的概念。通过函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行。ECMAScript 中的函数使用 function 关键字来声明,后跟一组参数以及函数体。

function functionName(arg0, arg1,...,argN) {
statements
}

ECMAScript 中的函数在定义时不必指定是否返回值。实际上,任何函数在任何时候都可以通过 return 语句后跟要返回的值来实现返回值。
函数会在执行完 return 语句之后停止并立即退出。因此,位于 return 语句之后的任何代码都永远不会执行。另外,return 语句也可以不带有任何返回值。在这种情况下,函数在停止执行后将返回 undefined 值。

严格模式对函数有一些限制:

  • 不能把函数命名为 eval 或 arguments
  • 不能把参数命名为 eval 或 arguments
  • 不能出现两个命名参数同名的情况

理解参数

ECMAScript 函数的参数与大多数其他语言中函数的参数有所不同。ECMAScript 函数不介意传递进来多少个参数,也不在乎传进来参数是什么数据类型。也就是说,即便你定义的函数只接收两个参数,在调用这个函数时也未必一定要传递两个参数。可以传递一个、三个甚至不传递参数,而解析器永远不会有什么怨言。之所以会这样,原因是 ECMAScript 中的参数在内部是用一个数组来表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数(如果有参数的话)。如果这个数组中不包含任何元素,无所谓; 如果包含多个元素,也没有问题。实际上,在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数。
其实,arguments 对象只是与数组类似(它并不是 Array 的实例),因为可以使用方括号语法访问它的每一个元素(即第一个元素是 arguments[0],第二个元素是argumetns[1]等等,使用 length 属性来确定传递进来多少个参数。这个事实说明了 ECMAScript 函数的一个重要特点: 命名的参数只提供便利,但不是必需的。

关于 arguments 的行为,还有一点比较有意思。那就是它的值永远与对应命名参数的值保持同步。

function doAdd(num1, num2) {
arguments[1] = 10;
alert(arguments[0] + num2);
}

每次执行这个 doAdd() 函数都会重写第二个参数,将第二个参数的值修改为 10。因为 arguments 对象中的值会自动反映到对应的命名参数,所以修改 arguments[1],也就修改了 num2,结果它们的值都会变成 10。不过,这并不是说读取这两个值会访问相同的内存空间; 它们的内存空间是独立的,但它们的值会同步。但这种影响是单向的: 修改命名参数不会改变 arguments 中对应的值。另外还要记住,如果只传入了一个参数,那么为 arguments[1] 设置的值不会反应到命名参数中。这是因为 arguments 对象的长度是由传入的参数个数决定的,不是由定义函数时的命名参数的个数决定的。

关于参数还要记住最后一点: 没有传递值的命名参数将自动被赋予 undefined 值。这就跟定义了变量但又没有初始化一样。
严格模式对如何使用 argumetns 对象做出了一些限制。

  • 像前面例子中那样的赋值会变得无效。也就是说,即使把 arguments[1] 设置为 10,num2 的值仍然还是 undefined。
  • 重写 arguments 的值会导致语法错误(代码将不会执行)。

没有重载

ECMAScript 函数不能像传统意义上那样实现重载。
ECMAScirpt 函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。