<h2 id="2.1">2.1 概述</h2>
### 基本句法和变量
#### 语句
JavaScript程序的执行单位为行(line),也就是一行一行地执行。一般情况下,每一行就是一个语句。
语句(statement)是为了完成某种任务而进行的操作,比如下面就是一行赋值语句:
```javascript
var a = 1 + 3;
```
这条语句先用`var`命令,声明了变量`a`,然后将`1 + 3`的运算结果赋值给变量`a`。
`1 + 3`叫做表达式(expression),指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,一般情况下不需要返回值;后者则是为了得到返回值,一定会返回一个值。
凡是JavaScript语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。一条语句可以包含多个表达式。
语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。
```javascript
var a = 1 + 3 ; var b = 'abc';
```
分号前面可以没有任何内容,JavaScript引擎将其视为空语句。
```javascript
;;;
```
上面的代码就表示3个空语句。(关于分号的更多介绍,请看后文《代码风格》一节。)
表达式不需要分号结尾。一旦在表达式后面添加分号,则JavaScript引擎就将表达式视为语句,这样会产生一些没有任何意义的语句。
```javascript
1 + 3;
'abc';
```
上面两行语句有返回值,但是没有任何意义,因为只是返回一个单纯的值,没有任何其他操作。
#### 变量
变量是对“值”的引用,使用变量等同于引用一个值。每一个变量都有一个变量名。
```javascript
var a = 1;
```
上面的代码先声明变量`a`,然后在变量`a`与数值1之间建立引用关系,也称为将数值1“赋值”给变量`a`。以后,引用变量`a`就会得到数值1。最前面的`var`,是变量声明命令。它表示通知解释引擎,要创建一个变量`a`。
变量的声明和赋值,是分开的两个步骤,上面的代码将它们合在了一起,实际的步骤是下面这样。
```javascript
var a;
a = 1;
```
如果只是声明变量而没有赋值,则该变量的值是不存在的,JavaScript使用`undefined`表示这种情况。
```javascript
var a;
a // undefined
```
JavaScript允许在变量赋值的同时,省略`var`命令声明变量。也就是说,`var a = 1`与`a = 1`,这两条语句的效果相同。但是由于这样的做法很容易不知不觉地创建全局变量(尤其是在函数内部),所以建议总是使用`var`命令声明变量。
> 严格地说,`var a = 1` 与 `a = 1`,这两条语句的效果不完全一样,主要体现在`delete`命令无法删除前者。不过,绝大多数情况下,这种差异是可以忽略的。
如果一个变量没有声明就直接使用,JavaScript会报错,告诉你变量未定义。
```javascript
x
// ReferenceError: x is not defined
```
上面代码直接使用变量`x`,系统就报错,告诉你变量`x`没有声明。
可以在同一条`var`命令中声明多个变量。
```javascript
var a, b;
```
JavaScirpt是一种动态类型语言,也就是说,变量的类型没有限制,可以赋予各种类型的值。
```javascript
var a = 1;
a = 'hello';
```
上面代码中,变量`a`起先被赋值为一个数值,后来又被重新赋值为一个字符串。第二次赋值的时候,因为变量`a`已经存在,所以不需要使用`var`命令。
如果使用`var`重新声明一个已经存在的变量,是无效的。
```javascript
var x = 1;
var x;
x // 1
```
上面代码中,变量`x`声明了两次,第二次声明是无效的。
但是,如果第二次声明的同时还赋值了,则会覆盖掉前面的值。
```javascript
var x = 1;
var x = 2;
// 等同于
var x = 1;
var x;
x = 2;
```
#### 变量提升
JavaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
```javascript
console.log(a);
var a = 1;
```
上面代码首先使用`console.log`方法,在控制台(console)显示变量a的值。这时变量`a`还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。
```javascript
var a;
console.log(a);
a = 1;
```
最后的结果是显示`undefined`,表示变量`a`已声明,但还未赋值。
请注意,变量提升只对`var`命令声明的变量有效,如果一个变量不是用`var`命令声明的,就不会发生变量提升。
```javascript
console.log(b);
b = 1;
```
上面的语句将会报错,提示“ReferenceError: b is not defined”,即变量`b`未声明,这是因为`b`不是用`var`命令声明的,JavaScript引擎不会将其提升,而只是视为对顶层对象的`b`属性的赋值。
#### 标识符
标识符(identifier)是用来识别具体对象的一个名称。最常见的标识符就是变量名,以及后面要提到的函数名。JavaScript语言的标识符对大小写敏感,所以`a`和`A`是两个不同的标识符。
标识符有一套命名规则,不符合规则的就是非法标识符。JavaScript引擎遇到非法标识符,就会报错。
简单说,标识符命名规则如下:
- 第一个字符,可以是任意Unicode字母(包括英文字母和其他语言的字母),以及美元符号(`$`)和下划线(`_`)。
- 第二个字符及后面的字符,除了Unicode字母、美元符号和下划线,还可以用数字`0-9`。
下面这些都是合法的标识符。
```javascript
arg0
_tmp
$elem
π
```
下面这些则是不合法的标识符。
```javascript
1a // 第一个字符不能是数字
23 // 同上
*** // 标识符不能包含星号
a+b // 标识符不能包含加号
-d // 标识符不能包含减号或连词线
```
中文是合法的标识符,可以用作变量名。
```javascript
var 临时变量 = 1;
```
> JavaScript有一些保留字,不能用作标识符:arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。
另外,还有三个词虽然不是保留字,但是因为具有特别含义,也不应该用作标识符:`Infinity`、`NaN`、`undefined`。
#### 注释
源码中被JavaScript引擎忽略的部分就叫做注释,它的作用是对代码进行解释。Javascript提供两种注释:一种是单行注释,用//起头;另一种是多行注释,放在/\* 和 \*/之间。
```javascript
// 这是单行注释
/*
这是
多行
注释
*/
```
此外,由于历史上JavaScript兼容HTML代码的注释,所以<!--和-->也被视为单行注释。
```javascript
x = 1; <!-- x = 2;
--> x = 3;
```
上面代码中,只有`x = 1`会执行,其他的部分都被注释掉了。
需要注意的是,-->只有在行首,才会被当成单行注释,否则就是一个运算符。
```javascript
function countdown(n) {
while (n --> 0) console.log(n);
}
countdown(3)
// 2
// 1
// 0
```
上面代码中,`n --> 0`实际上会当作`n-- > 0`,因此输出2、1、0。
#### 区块
JavaScript使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。
与大多数编程语言不一样,JavaScript的区块不构成单独的作用域(scope)。也就是说,区块中的变量与区块外的变量,属于同一个作用域。
```javascript
{
var a = 1;
}
a // 1
```
上面代码在区块内部,声明并赋值了变量`a`,然后在区块外部,变量`a`依然有效,这说明区块不构成单独的作用域,与不使用区块的情况没有任何区别。所以,单独使用的区块在JavaScript中意义不大,很少出现。区块往往用来构成其他更复杂的语法结构,比如`for`、`if`、`while`、`function`等。
### 条件语句
条件语句提供一种语法构造,只有满足某个条件,才会执行相应的语句。JavaScript提供`if`结构和`switch`结构,完成条件判断。
#### if 结构
`if`结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。
```javascript
if (expression)
statement;
// 或者
if (expression) statement;
```
上面是`if`结构的基本形式。需要注意的是,expression(表达式)必须放在圆括号中,表示对表达式求值。如果结果为`true`,就执行紧跟在后面的语句(statement);如果结果为`false`,则跳过statement的部分。
```javascript
if (m === 3)
m += 1;
```
上面代码表示,只有在`m`等于3时,才会将其值加上1。
这种写法要求条件表达式后面只能有一个语句。如果想执行多个语句,必须在`if`的条件判断之后,加上大括号,表示代码块。
```javascript
if (m === 3) {
m += 1;
}
```
建议总是在`if`语句中使用大括号,因为这样方便插入语句。
注意,`if`后面的表达式,不要混淆“赋值表达式”(`=`)与“严格相等运算符”(`===`)或“相等运算符”(`==`)。因为,“赋值表达式”不具有比较作用。
```javascript
var x = 1;
var y = 2;
if (x = y) {
console.log(x);
}
// "2"
```
上面代码的原意是,当`x`等于`y`的时候,才执行相关语句。但是,不小心将“严格相等运算符”写成“赋值表达式”,结果变成了将`y`赋值给`x`,然后条件就变成了,变量`x`的值(等于2)自动转为布尔值以后,判断其是否为`true`。
这种错误可以正常生成一个布尔值,因而不会报错。为了避免这种情况,有些开发者习惯将常量写在运算符的左边,这样的话,一旦不小心将相等运算符写成赋值运算符,就会报错,因为常量不能被赋值。
```javascript
if (x = 2) { // 不报错
if (2 = x) { // 报错
```
至于为什么优先采用“严格相等运算符”(`===`),而不是“相等运算符”(`==`),请参考《运算符》一节。
### if...else结构
`if`代码块后面,还可以跟一个`else`代码块,表示不满足条件时,所要执行的代码。
```javascript
if (m === 3) {
// then
} else {
// else
}
```
上面代码判断变量`m`是否等于3,如果等于就执行`if`代码块,否则执行`else`代码块。
对同一个变量进行多次判断时,多个`if...else`语句可以连写在一起。
```javascript
if (m === 0) {
// ...
} else if (m === 1) {
// ...
} else if (m === 2) {
// ...
} else {
// ...
}
```
`else`代码块总是跟随离自己最近的那个`if`语句。
```javascript
var m = 1;
var n = 2;
if (m !== 1)
if (n === 2) console.log('hello');
else console.log('world');
```
上面代码不会有任何输出,`else`代码块不会得到执行,因为它跟着的是最近的那个`if`语句,相当于下面这样。
```javascript
if (m !== 1) {
if (n === 2) {
console.log('hello');
} else {
console.log('world');
}
}
```
如果想让`else`代码块跟随最上面的那个`if`语句,就要改变大括号的位置。
```javascript
if (m !== 1) {
if (n === 2) {
console.log('hello');
}
} else {
console.log('world');
}
// world
```
### switch结构
多个`if...else`连在一起使用的时候,可以转为使用更方便的`switch`结构。
```javascript
switch (fruit) {
case "banana":
// ...
break;
case "apple":
// ...
break;
default:
// ...
}
```
上面代码根据变量`fruit`的值,选择执行相应的`case`。如果所有`case`都不符合,则执行最后的`default`部分。需要注意的是,每个`case`代码块内部的`break`语句不能少,否则会接下去执行下一个`case`代码块,而不是跳出`switch`结构。
```javascript
var x = 1;
switch (x) {
case 1:
console.log('x 于1');
case 2:
console.log('x 等于2');
default:
console.log('x 等于其他值');
}
// x等于1
// x等于2
// x等于其他值
```
上面代码中,`case`代码块之中没有`break`语句,导致不会跳出`switch`结构,而会一直执行下去。
`switch`语句部分和`case`语句部分,都可以使用表达式。
```javascript
switch(1 + 3) {
case 2 + 2:
f();
break;
default:
neverhappens();
}
```
上面代码的`default`部分,是永远不会执行到的。
需要注意的是,`switch`语句后面的表达式与`case`语句后面的表示式,在比较运行结果时,采用的是严格相等运算符(`===`),而不是相等运算符(`==`),这意味着比较时不会发生类型转换。
```javascript
var x = 1;
switch (x) {
case true:
console.log('x发生类型转换');
default:
console.log('x没有发生类型转换');
}
// x没有发生类型转换
```
上面代码中,由于变量`x`没有发生类型转换,所以不会执行`case true`的情况。这表明,`switch`语句内部采用的是“严格相等运算符”,详细解释请参考《运算符》一节。
`switch`结构不利于代码重用,往往可以用对象形式重写。
```javascript
function getItemPricing(customer, item) {
switch(customer.type) {
case 'VIP':
return item.price * item.quantity * 0.50;
case 'Preferred':
return item.price * item.quantity * 0.75;
case 'Regular':
case default:
return item.price * item.quantity;
}
}
```
上面代码根据不同用户,返回不同的价格。你可以发现,`switch`语句包含的三种情况,内部逻辑都是相同的,不同只是折扣率。这启发我们可以用对象属性,重写这个判断。
```javascript
var pricing = {
'VIP': 0.50,
'Preferred': 0.75,
'Regular': 1.0
};
function getItemPricing(customer, item) {
if (pricing[customer.type])
return item.price * item.quantity * pricing[customer.type];
else
return item.price * item.quantity * pricing.Regular;
}
```
如果价格档次再多一些,对象属性写法的简洁优势就更明显了。
### 三元运算符 ?:
JavaScript还有一个三元运算符(即该运算符需要三个运算子)`?:`,也可以用于逻辑判断。
```javascript
(contidion) ? expression1 : expression2
```
上面代码中,如果`contidion`为`true`,则返回`expression1`的值,否则返回`expression2`的值。
```javascript
var even = (n % 2 === 0) ? true : false;
```
上面代码中,如果`n`可以被2整除,则`even`等于`true`,否则等于`false`。它等同于下面的形式。
```javascript
var even;
if (n % 2 === 0) {
even = true;
} else {
even = false;
}
```
这个三元运算符可以被视为`if...else...`的简写形式,因此可以用于多种场合。
```javascript
var myVar;
console.log( myVar
? 'myVar has a value'
: 'myVar do not has a value'
)
// myVar do not has a value
```
上面代码利用三元运算符,输出相应的提示。
```javascript
var msg = 'The number '
+ n
+ ' is '
+ ((n % 2 === 0) ? 'even' : 'odd');
```
上面代码利用三元运算符,在字符串之中插入不同的值。
## 循环语句
循环语句用于重复执行某个操作,它有多种形式。
### while循环
`While`语句包括一个循环条件和一段代码块,只要条件为真,就不断循环执行代码块。
```javascript
while (expression)
statement;
// 或者
while (expression) statement;
```
`while`语句的循环条件是一个表达式(express),必须放在圆括号中。代码块部分,如果只有一条语句(statement),可以省略大括号,否则就必须加上大括号。
```javascript
while (expression) {
statement;
}
```
下面是`while`语句的一个例子。
```javascript
var i = 0;
while (i < 100) {
console.log('i当前为:' + i);
i += 1;
}
```
上面的代码将循环100次,直到`i`等于100为止。
下面的例子是一个无限循环,因为条件总是为真。
```javascript
while (true) {
console.log("Hello, world");
}
```
### for循环
`for`语句是循环命令的另一种形式。
```javascript
for(initialize; test; increment)
statement
// 或者
for(initialize; test; increment) {
statement
}
```
`for`语句后面的括号里面,有三个表达式。
- 初始化表达式(initialize):确定循环的初始值,只在循环开始时执行一次。
- 测试表达式(test):检查循环条件,只要为真就进行后续操作。
- 递增表达式(increment):完成后续操作,然后返回上一步,再一次检查循环条件。
下面是一个例子。
```javascript
var x = 3;
for (var i = 0; i < x; i++) {
console.log(i);
}
// 0
// 1
// 2
```
上面代码中,初始化表达式是`var i = 0`,即初始化一个变量`i`;测试表达式是`i < x`,即只要`i`小于`x`,就会执行循环;递增表达式是`i++`,即每次循环结束后,`i`增大1。
所有`for`循环,都可以改写成`while`循环。上面的例子改为`while`循环,代码如下。
```javascript
var x = 3;
var i = 0;
while (i < x) {
console.log(i);
i++;
}
```
`for`语句的三个部分(initialize,test,increment),可以省略任何一个,也可以全部省略。
```javascript
for ( ; ; ){
console.log('Hello World');
}
```
上面代码省略了`for`语句表达式的三个部分,结果就导致了一个无限循环。
### do...while循环
`do...while`循环与`while`循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件。
```javascript
do
statement
while(expression);
// 或者
do {
statement
} while(expression);
```
不管条件是否为真,`do..while`循环至少运行一次,这是这种结构最大的特点。另外,`while`语句后面的分号不能省略。
下面是一个例子。
```javascript
var x = 3;
var i = 0;
do {
console.log(i);
i++;
} while(i < x);
```
### break语句和continue语句
`break`语句和`continue`语句都具有跳转作用,可以让代码不按既有的顺序执行。
`break`语句用于跳出代码块或循环。
```javascript
var i = 0;
while(i < 100) {
console.log('i当前为:' + i);
i++;
if (i === 10) break;
}
```
上面代码只会执行10次循环,一旦`i`等于10,就会跳出循环。
`for`循环也可以使用`break`语句跳出循环。
```javascript
for (var i = 0; i < 5; i++) {
console.log(i);
if (i === 3)
break;
}
// 0
// 1
// 2
// 3
```
上面代码执行到`i`等于3,就会跳出循环。
`continue`语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环。
```javascript
var i = 0;
while (i < 100){
i++;
if (i%2 === 0) continue;
console.log('i当前为:' + i);
}
```
上面代码只有在`i`为奇数时,才会输出`i`的值。如果`i`为偶数,则直接进入下一轮循环。
如果存在多重循环,不带参数的`break`语句和`continue`语句都只针对最内层循环。
### 标签(label)
JavaScript语言允许,语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下。
```javascript
label:
statement
```
标签可以是任意的标识符,但是不能是保留字,语句部分可以是任意语句。
标签通常与`break`语句和`continue`语句配合使用,跳出特定的循环。
```javascript
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) break top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
```
上面代码为一个双重循环区块,`break`命令后面加上了`top`标签(注意,`top`不用加引号),满足条件时,直接跳出双层循环。如果`break`语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。
`continue`语句也可以与标签配合使用。
```javascript
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) continue top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2
```
上面代码中,`continue`命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果`continue`语句后面不使用标签,则只能进入下一轮的内层循环。
## 数据类型
### 概述
JavaScript语言的每一个值,都属于某一种数据类型。JavaScript的数据类型,共有六种。(ES6又新增了第七种Symbol类型的值,本教程不涉及。)
- 数值(number):整数和小数(比如1和3.14)
- 字符串(string):字符组成的文本(比如"Hello World")
- 布尔值(boolean):`true`(真)和`false`(假)两个特定值
- `undefined`:表示“未定义”或不存在,即此处目前没有任何值
- `null`:表示空缺,即此处应该有一个值,但目前为空
- 对象(object):各种值组成的集合
通常,我们将数值、字符串、布尔值称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。而将对象称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于`undefined`和`null`,一般将它们看成两个特殊值。
对象又可以分成三个子类型。
- 狭义的对象(object)
- 数组(array)
- 函数(function)
狭义的对象和数组是两种不同的数据组合方式,而函数其实是处理数据的方法。JavaScript把函数当成一种数据类型,可以像其他类型的数据一样,进行赋值和传递,这为编程带来了很大的灵活性,体现了JavaScript作为“函数式语言”的本质。
这里需要明确的是,JavaScript的所有数据,都可以视为广义的对象。不仅数组和函数属于对象,就连原始类型的数据(数值、字符串、布尔值)也可以用对象方式调用。为了避免混淆,此后除非特别声明,本教程的”对象“都特指狭义的对象。
本教程将详细介绍所有的数据类型。`undefined`和`null`两个特殊值和布尔类型Boolean比较简单,将在本节介绍,其他类型将各自有单独的一节。
### typeof运算符
JavaScript有三种方法,可以确定一个值到底是什么类型。
- `typeof`运算符
- `instanceof`运算符
- `Object.prototype.toString`方法
`instanceof`运算符和`Object.prototype.toString`方法,将在后文相关章节介绍。这里着重介绍`typeof`运算符。
`typeof`运算符可以返回一个值的数据类型,可能有以下结果。
**(1)原始类型**
数值、字符串、布尔值分别返回`number`、`string`、`boolean`。
```javascript
typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
```
**(2)函数**
函数返回`function`。
```javascript
function f() {}
typeof f
// "function"
```
**(3)undefined**
`undefined`返回`undefined`。
```javascript
typeof undefined
// "undefined"
```
利用这一点,typeof可以用来检查一个没有声明的变量,而不报错。
```javascript
v
// ReferenceError: v is not defined
typeof v
// "undefined"
```
上面代码中,变量`v`没有用`var`命令声明,直接使用就会报错。但是,放在`typeof`后面,就不报错了,而是返回`undefined`。
实际编程中,这个特点通常用在判断语句。
```javascript
// 错误的写法
if (v) {
// ...
}
// ReferenceError: v is not defined
// 正确的写法
if (typeof v === "undefined") {
// ...
}
```
**(4)其他**
除此以外,其他情况都返回`object`。
```javascript
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"
```
从上面代码可以看到,空数组(`[]`)的类型也是`object`,这表示在JavaScript内部,数组本质上只是一种特殊的对象。
另外,`null`的类型也是`object`,这是由于历史原因造成的。1995年JavaScript语言的第一版,所有值都设计成32位,其中最低的3位用来表述数据类型,`object`对应的值是`000`。当时,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),完全没考虑`null`,只把它当作`object`的一种特殊值,32位全部为0。这是`typeof null`返回`object`的根本原因。
为了兼容以前的代码,后来就没法修改了。这并不是说`null`就属于对象,本质上`null`是一个类似于`undefined`的特殊值。
既然`typeof`对数组(array)和对象(object)的显示结果都是`object`,那么怎么区分它们呢?instanceof运算符可以做到。
```javascript
var o = {};
var a = [];
o instanceof Array // false
a instanceof Array // true
```
`instanceof`运算符的详细解释,请见《面向对象编程》一章。
## null和undefined
### 概述
`null`与`undefined`都可以表示“没有”,含义非常相似。将一个变量赋值为`undefined`或`null`,老实说,语法效果几乎没区别。
```javascript
var a = undefined;
// 或者
var a = null;
```
上面代码中,`a`变量分别被赋值为`undefined`和`null`,这两种写法的效果几乎等价。
在`if`语句中,它们都会被自动转为`false`,相等运算符(`==`)甚至直接报告两者相等。
```javascript
if (!undefined) {
console.log('undefined is false');
}
// undefined is false
if (!null) {
console.log('null is false');
}
// null is false
undefined == null
// true
```
上面代码说明,两者的行为是何等相似!Google公司开发的JavaScript语言的替代品Dart语言,就明确规定只有`null`,没有`undefined`!
既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。
1995年JavaScript诞生时,最初像Java一样,只设置了`null`作为表示"无"的值。根据C语言的传统,`null`被设计成可以自动转为`0`。
```javascript
Number(null) // 0
5 + null // 5
```
但是,JavaScript的设计者Brendan Eich,觉得这样做还不够,有两个原因。首先,`null`像在Java里一样,被当成一个对象。但是,JavaScript的值分成原始类型和合成类型两大类,Brendan Eich觉得表示"无"的值最好不是对象。其次,JavaScript的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。Brendan Eich觉得,如果`null`自动转为0,很不容易发现错误。
因此,Brendan Eich又设计了一个`undefined`。他是这样区分的:`null`是一个表示"无"的对象,转为数值时为`0`;`undefined`是一个表示"无"的原始值,转为数值时为`NaN`。
```javascript
Number(undefined) // NaN
5 + undefined // NaN
```
但是,这样的区分在实践中很快就被证明不可行。目前`null`和`undefined`基本是同义的,只有一些细微的差别。
`null`的特殊之处在于,JavaScript把它包含在对象类型(object)之中。
```javascript
typeof null // "object"
```
上面代码表示,查询`null`的类型,JavaScript返回`object`(对象)。
这并不是说null的数据类型就是对象,而是JavaScript早期部署中的一个约定俗成,其实不完全正确,后来再想改已经太晚了,会破坏现存代码,所以一直保留至今。
注意,JavaScript的标识名区分大小写,所以`undefined`和`null`不同于`Undefined`和`Null`(或者其他仅仅大小写不同的词形),后者只是普通的变量名。
### 用法和含义
对于`null`和`undefined`,可以大致可以像下面这样理解。
`null`表示空值,即该处的值现在为空。比如,调用函数时,不需要传入某个参数,这时就可以传入`null`。
`undefined`表示“未定义”,下面是返回`undefined`的典型场景。
```javascript
// 变量声明了,但没有赋值
var i;
i // undefined
// 调用函数时,应该提供的参数没有提供,该参数等于undefined
function f(x) {
return x;
}
f() // undefined
// 对象没有赋值的属性
var o = new Object();
o.p // undefined
// 函数没有返回值时,默认返回undefined
function f() {}
f() // undefined
```
## 布尔值
布尔值代表“真”和“假”两个状态。“真”用关键字`true`表示,“假”用关键字`false`表示。布尔值只有这两个值。
下列运算符会返回布尔值:
- 两元逻辑运算符: `&&` (And),`||` (Or)
- 前置逻辑运算符: `!` (Not)
- 相等运算符:`===`,`!==`,`==`,`!=`
- 比较运算符:`>`,`>=`,`<`,`<=`
如果JavaScript预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为`false`,其他值都视为`true`。
- `undefined`
- `null`
- `false`
- `0`
- `NaN`
- `""`或`''`(空字符串)
布尔值往往用于程序流程的控制,请看一个例子。
```javascript
if ('') {
console.log(true);
}
// 没有任何输出
```
上面代码的`if`命令后面的判断条件,预期应该是一个布尔值,所以JavaScript自动将空字符串,转为布尔值`false`,导致程序不会进入代码块,所以没有任何输出。
需要特别注意的是,空数组(`[]`)和空对象(`{}`)对应的布尔值,都是`true`。
```javascript
if ([]) {
console.log(true);
}
// true
if ({}) {
console.log(true);
}
// true
```
更多关于数据类型转换的介绍,参见《数据类型转换》一节。
<h2 id="2.2">2.2 数值</h2>
## 概述
### 整数和浮点数
JavaScript内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,`1`与`1.0`是相同的,是同一个数。
```javascript
1 === 1.0 // true
```
这就是说,在JavaScript语言的底层,根本没有整数,所有数字都是小数(64位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时JavaScript会自动把64位浮点数,转成32位整数,然后再进行运算,参见《运算符》一节的”位运算“部分。
由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。
```javascript
0.1 + 0.2 === 0.3
// false
0.3 / 0.1
// 2.9999999999999996
(0.3 - 0.2) === (0.2 - 0.1)
// false
```
### 数值精度
根据国际标准IEEE 754,JavaScript浮点数的64个二进制位,从最左边开始,是这样组成的。
- 第1位:符号位,`0`表示正数,`1`表示负数
- 第2位到第12位:储存指数部分
- 第13位到第64位:储存小数部分(即有效数字)
符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。
IEEE 754规定,有效数字第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字总是`1.xx...xx`的形式,其中`xx..xx`的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript提供的有效数字最长为53个二进制位。
```
(-1)^符号位 * 1.xx...xx * 2^指数位
```
上面公式是一个数在JavaScript内部实际的表现形式。
精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-(2<sup>53</sup>-1)到2<sup>53</sup>-1,都可以精确表示。
```javascript
Math.pow(2, 53)
// 9007199254740992
Math.pow(2, 53) + 1
// 9007199254740992
Math.pow(2, 53) + 2
// 9007199254740994
Math.pow(2, 53) + 3
// 9007199254740996
Math.pow(2, 53) + 4
// 9007199254740996
```
从上面示例可以看到,大于2的53次方以后,整数运算的结果开始出现错误。所以,大于等于2的53次方的数值,都无法保持精度。
```javascript
Math.pow(2, 53)
// 9007199254740992
// 多出的三个有效数字,将无法保存
9007199254740992111
// 9007199254740992000
```
上面示例表明,大于2的53次方以后,多出来的有效数字(最后三位的`111`)都会无法保存,变成0。
### 数值范围
根据标准,64位浮点数的指数部分的长度是11个二进制位,意味着指数部分的最大值是2047(2的11次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则JavaScript能够表示的数值范围为2<sup>1024</sup>到2<sup>-1023</sup>(开区间),超出这个范围的数无法表示。
如果指数部分等于或超过最大正值1024,JavaScript会返回`Infinity`(关于Infinity的介绍参见下文),这称为“正向溢出”;如果等于或超过最小负值-1023(即非常接近0),JavaScript会直接把这个数转为0,这称为“负向溢出”。
```javascript
var x = 0.5;
for(var i = 0; i < 25; i++) {
x = x * x;
}
x // 0
```
上面代码对`0.5`连续做25次平方,由于最后结果太接近0,超出了可表示的范围,JavaScript就直接将其转为0。
至于具体的最大值和最小值,JavaScript提供Number对象的`MAX_VALUE`和`MIN_VALUE`属性表示(参见《Number对象》一节)。
```javascript
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324
```
## 数值的表示法
JavaScript的数值有多种表示方法,可以用字面形式直接表示,比如`35`(十进制)和`0xFF`(十六进制)。
数值也可以采用科学计数法表示,下面是几个科学计数法的例子。
```javascript
123e3 // 123000
123e-3 // 0.123
-3.1E+12
.1e-23
```
科学计数法允许字母`e`或`E`的后面,跟着一个整数,表示这个数值的指数部分。
以下两种情况,JavaScript会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。
**(1)小数点前的数字多于21位。**
```javascript
1234567890123456789012
// 1.2345678901234568e+21
123456789012345678901
// 123456789012345680000
```
**(2)小数点后的零多于5个。**
```javascript
// 小数点后紧跟5个以上的零,
// 就自动转为科学计数法
0.0000003 // 3e-7
// 否则,就保持原来的字面形式
0.000003 // 0.000003
```
## 数值的进制
使用字面量(literal)时,JavaScript对整数提供四种进制的表示方法:十进制、十六进制、八进制、2进制。
- 十进制:没有前导0的数值。
- 八进制:有前缀`0o`或`0O`的数值,或者有前导0、且只用到0-7的七个阿拉伯数字的数值。
- 十六进制:有前缀`0x`或`0X`的数值。
- 二进制:有前缀`0b`或`0B`的数值。
默认情况下,JavaScript内部会自动将八进制、十六进制、二进制转为十进制。下面是一些例子。
```javascript
0xff // 255
0o377 // 255
0b11 // 3
```
如果八进制、十六进制、二进制的数值里面,出现不属于该进制的数字,就会报错。
```javascript
0xzz // 报错
0o88 // 报错
0b22 // 报错
```
上面代码中,十六进制出现了字母`z`、八进制出现数字`8`、二进制出现数字`2`,因此报错。
通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字`8`和`9`,则该数值被视为十进制。
```javascript
0888 // 888
0777 // 511
```
用前导0表示八进制,处理时很容易造成混乱。ES5的严格模式和ES6,已经废除了这种表示法,但是浏览器目前还支持。
## 特殊数值
JavaScript提供几个特殊的数值。
### 正零和负零
前面说过,JavaScript的64位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有一个对应的负值,就连`0`也不例外。
在JavaScript内部,实际上存在2个`0`:一个是`+0`,一个是`-0`。它们是等价的。
```javascript
-0 === +0 // true
0 === -0 // true
0 === +0 // true
```
几乎所有场合,正零和负零都会被当作正常的`0`。
```javascript
+0 // 0
-0 // 0
(-0).toString() // '0'
(+0).toString() // '0'
```
唯一有区别的场合是,`+0`或`-0`当作分母,返回的值是不相等的。
```javascript
(1 / +0) === (1 / -0) // false
```
上面代码之所以出现这样结果,是因为除以正零得到`+Infinity`,除以负零得到`-Infinity`,这两者是不相等的(关于`Infinity`详见后文)。
### NaN
**(1)含义**
`NaN`是JavaScript的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。
```javascript
5 - 'x' // NaN
```
上面代码运行时,会自动将字符串`x`转为数值,但是由于`x`不是数值,所以最后得到结果为`NaN`,表示它是“非数字”(`NaN`)。
另外,一些数学函数的运算结果会出现`NaN`。
```javascript
Math.acos(2) // NaN
Math.log(-1) // NaN
Math.sqrt(-1) // NaN
```
`0`除以`0`也会得到`NaN`。
```javascript
0 / 0 // NaN
```
需要注意的是,`NaN`不是一种独立的数据类型,而是一种特殊数值,它的数据类型依然属于`Number`,使用`typeof`运算符可以看得很清楚。
```javascript
typeof NaN // 'number'
```
**(2)运算规则**
`NaN`不等于任何值,包括它本身。
```javascript
NaN === NaN // false
```
由于数组的`indexOf`方法,内部使用的是严格相等运算符,所以该方法对`NaN`不成立。
```javascript
[NaN].indexOf(NaN) // -1
```
`NaN`在布尔运算时被当作`false`。
```javascript
Boolean(NaN) // false
```
`NaN`与任何数(包括它自己)的运算,得到的都是`NaN`。
```javascript
NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN
```
**(3)判断NaN的方法**
`isNaN`方法可以用来判断一个值是否为`NaN`。
```javascript
isNaN(NaN) // true
isNaN(123) // false
```
但是,`isNaN`只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成`NaN`,所以最后返回`true`,这一点要特别引起注意。也就是说,`isNaN`为`true`的值,有可能不是`NaN`,而是一个字符串。
```javascript
isNaN('Hello') // true
// 相当于
isNaN(Number('Hello')) // true
```
出于同样的原因,对于对象和数组,`isNaN`也返回`true`。
```javascript
isNaN({}) // true
// 等同于
isNaN(Number({})) // true
isNaN(['xzy']) // true
// 等同于
isNaN(Number(['xzy'])) // true
```
但是,对于空数组和只有一个数值成员的数组,`isNaN`返回`false`。
```javascript
isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false
```
上面代码之所以返回`false`,原因是这些数组能被`Number`函数转成数值,请参见《数据类型转换》一节。
因此,使用`isNaN`之前,最好判断一下数据类型。
```javascript
function myIsNaN(value) {
return typeof value === 'number' && isNaN(value);
}
```
判断`NaN`更可靠的方法是,利用`NaN`是JavaScript之中唯一不等于自身的值这个特点,进行判断。
```javascript
function myIsNaN(value) {
return value !== value;
}
```
### Infinity
**(1)定义**
`Infinity`表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到`Infinity`。
```javascript
// 场景一
Math.pow(2, Math.pow(2, 100))
// Infinity
// 场景二
0 / 0 // NaN
1 / 0 // Infinity
```
上面代码中,第一个场景是一个表达式的计算结果太大,超出了JavaScript能够表示的范围,因此返回`Infinity`。第二个场景是`0`除以`0`会得到`NaN`,而非0数值除以`0`,会返回`Infinity`。
`Infinity`有正负之分,`Infinity`表示正的无穷,`-Infinity`表示负的无穷。
```javascript
Infinity === -Infinity // false
1 / -0 // -Infinity
-1 / -0 // Infinity
```
上面代码中,非零正数除以`-0`,会得到`-Infinity`,负数除以`-0`,会得到`Infinity`。
由于数值正向溢出(overflow)、负向溢出(underflow)和被`0`除,JavaScript都不报错,而是返回`Infinity`,所以单纯的数学运算几乎没有可能抛出错误。
`Infinity`大于一切数值(除了`NaN`),`-Infinity`小于一切数值(除了`NaN`)。
```javascript
Infinity > 1000 // true
-Infinity < -1000 // true
```
`Infinity`与`NaN`比较,总是返回`false`。
```javascript
Infinity > NaN // false
Infinity < NaN // false
```
**(2)运算规则**
`Infinity`的四则运算,符合无穷的数学计算规则。
```javascript
5 * Infinity // Infinity
5 - Infinity // -Infinity
Infinity / 5 // Infinity
5 / Infinity // 0
```
`Infinity`加上或乘以`Infinity`,返回的还是`Infinity`。
```javascript
Infinity + Infinity // Infinity
Infinity * Infinity // Infinity
```
`Infinity`减去或除以`Infinity`,得到`NaN`。
```javascript
Infinity - Infinity // NaN
Infinity / Infinity // NaN
```
**(3)isFinite函数**
`isFinite`函数返回一个布尔值,检查某个值是不是正常数值,而不是`Infinity`。
```javascript
isFinite(Infinity) // false
isFinite(-1) // true
isFinite(true) // true
isFinite(NaN) // false
```
上面代码表示,如果对`NaN`使用`isFinite`函数,也返回`false`,表示`NaN`不是一个正常值。
## 与数值相关的全局方法
### parseInt()
**(1)基本用法**
`parseInt`方法用于将字符串转为整数。
```javascript
parseInt('123') // 123
```
如果字符串头部有空格,空格会被自动去除。
```javascript
parseInt(' 81') // 81
```
如果`parseInt`的参数不是字符串,则会先转为字符串再转换。
```javascript
parseInt(1.23) // 1
// 等同于
parseInt('1.23') // 1
```
字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分。
```javascript
parseInt('8a') // 8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15
```
上面代码中,`parseInt`的参数都是字符串,结果只返回字符串头部可以转为数字的部分。
如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回`NaN`。
```javascript
parseInt('abc') // NaN
parseInt('.3') // NaN
parseInt('') // NaN
parseInt('+') // NaN
parseInt('+1') // 1
```
`parseInt`的返回值只有两种可能,不是一个十进制整数,就是`NaN`。
如果字符串以`0x`或`0X`开头,`parseInt`会将其按照十六进制数解析。
```javascript
parseInt('0x10') // 16
```
如果字符串以`0`开头,将其按照10进制解析。
```javascript
parseInt('011') // 11
```
对于那些会自动转为科学计数法的数字,`parseInt`会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果。
```javascript
parseInt(1000000000000000000000.5) // 1
// 等同于
parseInt('1e+21') // 1
parseInt(0.0000008) // 8
// 等同于
parseInt('8e-7') // 8
```
**(2)进制转换**
`parseInt`方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,`parseInt`的第二个参数为10,即默认是十进制转十进制。
```javascript
parseInt('1000') // 1000
// 等同于
parseInt('1000', 10) // 1000
```
下面是转换指定进制的数的例子。
```javascript
parseInt('1000', 2) // 8
parseInt('1000', 6) // 216
parseInt('1000', 8) // 512
```
上面代码中,二进制、六进制、八进制的`1000`,分别等于十进制的8、216和512。这意味着,可以用`parseInt`方法进行进制的转换。
如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回`NaN`。如果第二个参数是`0`、`undefined`和`null`,则直接忽略。
```javascript
parseInt('10', 37) // NaN
parseInt('10', 1) // NaN
parseInt('10', 0) // 10
parseInt('10', null) // 10
parseInt('10', undefined) // 10
```
如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回`NaN`。
```javascript
parseInt('1546', 2) // 1
parseInt('546', 2) // NaN
```
上面代码中,对于二进制来说,`1`是有意义的字符,`5`、`4`、`6`都是无意义的字符,所以第一行返回1,第二行返回`NaN`。
前面说过,如果`parseInt`的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果。
```javascript
parseInt(0x11, 36) // 43
// 等同于
parseInt(String(0x11), 36)
parseInt('17', 36)
```
上面代码中,十六进制的`0x11`会被先转为十进制的17,再转为字符串。然后,再用36进制解读字符串`17`,最后返回结果`43`。
这种处理方式,对于八进制的前缀0,尤其需要注意。
```javascript
parseInt(011, 2) // NaN
// 等同于
parseInt(String(011), 2)
parseInt('011', 2) // 3
```
上面代码中,第一行的`011`会被先转为字符串`9`,因为`9`不是二进制的有效字符,所以返回`NaN`。第二行的字符串`011`,会被当作二进制处理,返回3。
ES5不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个`0`。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定。
### parseFloat()
`parseFloat`方法用于将一个字符串转为浮点数。
```javascript
parseFloat('3.14') // 3.14
```
如果字符串符合科学计数法,则会进行相应的转换。
```javascript
parseFloat('314e-2') // 3.14
parseFloat('0.0314E+2') // 3.14
```
如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分。
```javascript
parseFloat('3.14more non-digit characters') // 3.14
```
`parseFloat`方法会自动过滤字符串前导的空格。
```javascript
parseFloat('\t\v\r12.34\n ') // 12.34
```
如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回`NaN`。
```javascript
parseFloat([]) // NaN
parseFloat('FF2') // NaN
parseFloat('') // NaN
```
上面代码中,尤其值得注意,`parseFloat`会将空字符串转为`NaN`。
这些特点使得`parseFloat`的转换结果不同于`Number`函数。
```javascript
parseFloat(true) // NaN
Number(true) // 1
parseFloat(null) // NaN
Number(null) // 0
parseFloat('') // NaN
Number('') // 0
parseFloat('123.45#') // 123.45
Number('123.45#') // NaN
```
<h2 id="2.3">2.3 字符串</h2>
## 概述
### 定义
字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。
```javascript
'abc'
"abc"
```
单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。
```javascript
'key = "value"'
"It's a long journey"
```
上面两个都是合法的字符串。
如果要在单引号字符串的内部,使用单引号(或者在双引号字符串的内部,使用双引号),就必须在内部的单引号(或者双引号)前面加上反斜杠,用来转义。
```javascript
'Did she say \'Hello\'?'
// "Did she say 'Hello'?"
"Did she say \"Hello\"?"
// "Did she say "Hello"?"
```
字符串默认只能写在一行内,分成多行将会报错。
```javascript
'a
b
c'
// SyntaxError: Unexpected token ILLEGAL
```
上面代码将一个字符串分成三行,JavaScript就会报错。
如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠。
```javascript
var longString = "Long \
long \
long \
string";
longString
// "Long long long string"
```
上面代码表示,加了反斜杠以后,原来写在一行的字符串,可以分成多行,效果与写在同一行完全一样。注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。
连接运算符(`+`)可以连接多个单行字符串,用来模拟多行字符串。
```javascript
var longString = 'Long '
+ 'long '
+ 'long '
+ 'string';
```
另外,有一种利用多行注释,生成多行字符串的变通方法。
```javascript
(function () { /*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')
// "line 1 line 2 line 3"
```
### 转义
反斜杠(`\\`)在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。
需要用反斜杠转义的特殊字符,主要有下面这些:
- `\0` 代表没有内容的字符(\u0000)
- `\b` 后退键(\u0008)
- `\f` 换页符(\u000C)
- `\n` 换行符(\u000A)
- `\r` 回车键(\u000D)
- `\t` 制表符(\u0009)
- `\v` 垂直制表符(\u000B)
- `\'` 单引号(\u0027)
- `\"` 双引号(\u0022)
- `\\\\` 反斜杠(\u005C)
- `\XXX` 用三个八进制数(000到377)表示字符,`XXX`对应该字符的Unicode,比如`\251`表示版权符号。
- `\xXX` 用两个十六进制数(00到FF)表示字符,`XX`对应该字符的Unicode,比如`\xA9`表示版权符号。
- `\uXXXX` 用四位十六进制的Unicode编号代表某个字符,比如`\u00A9`表示版权符号。
下面是最后三种字符的特殊写法的例子。
```javascript
'\251' // "©"
'\xA9' // "©"
'\u00A9' // "©"
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
```
如果非特殊字符前面使用反斜杠,则反斜杠会被省略。
```javascript
'\a'
// "a"
```
上面代码表示`a`是一个正常字符,前面加反斜杠没有特殊含义,则反斜杠会被自动省略。
如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前需要再加一个反斜杠,用来对自身转义。
```javascript
"Prev \\ Next"
// "Prev \ Next"
```
### 字符串与数组
字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(从0开始)。
```javascript
var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"
// 直接对字符串使用方括号运算符
'hello'[1] // "e"
```
如果方括号中的数字超过字符串的范围,或者方括号中根本不是数字,则返回`undefined`。
```javascript
'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined
```
但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。
```javascript
var s = 'hello';
delete s[0];
s // "hello"
s[1] = 'a';
s // "hello"
s[5] = '!';
s // "hello"
```
上面代码表示,字符串内部的单个字符无法改变和增删,这些操作会默默地失败。
字符串之所以类似于字符数组,实际是由于对字符串进行方括号运算时,字符串会自动转换为一个字符串对象(详见《标准库》一章的《包装对象》一节)。
### length属性
`length`属性返回字符串的长度,该属性也是无法改变的。
```javascript
var s = 'hello';
s.length // 5
s.length = 3;
s.length // 5
s.length = 7;
s.length // 5
```
上面代码表示字符串的`length`属性无法改变,但是不会报错。
## 字符集
JavaScript使用Unicode字符集,也就是说在JavaScript内部,所有字符都用Unicode表示。
不仅JavaScript内部使用Unicode储存字符,而且还可以直接在程序中使用Unicode,所有字符都可以写成"\uxxxx"的形式,其中xxxx代表该字符的Unicode编码。比如,`\u00A9`代表版权符号。
```javascript
var s = '\u00A9';
s // "©"
```
每个字符在JavaScript内部都是以16位(即2个字节)的UTF-16格式储存。也就是说,JavaScript的单位字符长度固定为16位长度,即2个字节。
但是,UTF-16有两种长度:对于`U+0000`到`U+FFFF`之间的字符,长度为16位(即2个字节);对于`U+10000`到`U+10FFFF`之间的字符,长度为32位(即4个字节),而且前两个字节在`0xD800`到`0xDBFF`之间,后两个字节在`0xDC00`到`0xDFFF`之间。举例来说,`U+1D306`对应的字符为𝌆,它写成UTF-16就是`0xD834 0xDF06`。浏览器会正确将这四个字节识别为一个字符,但是JavaScript内部的字符长度总是固定为16位,会把这四个字节视为两个字符。
```javascript
var s = '\uD834\uDF06';
s // "𝌆"
s.length // 2
/^.$/.test(s) // false
s.charAt(0) // ""
s.charAt(1) // ""
s.charCodeAt(0) // 55348
s.charCodeAt(1) // 57094
```
上面代码说明,对于于`U+10000`到`U+10FFFF`之间的字符,JavaScript总是视为两个字符(字符的`length`属性为2),用来匹配单个字符的正则表达式会失败(JavaScript认为这里不止一个字符),`charAt`方法无法返回单个字符,`charCodeAt`方法返回每个字节对应的十进制值。
所以处理的时候,必须把这一点考虑在内。对于4个字节的Unicode字符,假定`C`是字符的Unicode编号,`H`是前两个字节,`L`是后两个字节,则它们之间的换算关系如下。
```javascript
// 将大于U+FFFF的字符,从Unicode转为UTF-16
H = Math.floor((C - 0x10000) / 0x400) + 0xD800
L = (C - 0x10000) % 0x400 + 0xDC00
// 将大于U+FFFF的字符,从UTF-16转为Unicode
C = (H - 0xD800) * 0x400 + L - 0xDC00 + 0x10000
```
下面的正则表达式可以识别所有UTF-16字符。
```javascript
([\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])
```
由于JavaScript引擎(严格说是ES5规格)不能自动识别辅助平面(编号大于0xFFFF)的Unicode字符,导致所有字符串处理函数遇到这类字符,都会产生错误的结果(详见《标准库》一章的`String`对象章节)。如果要完成字符串相关操作,就必须判断字符是否落在`0xD800`到`0xDFFF`这个区间。
下面是能够正确处理字符串遍历的函数。
```javascript
function getSymbols(string) {
var length = string.length;
var index = -1;
var output = [];
var character;
var charCode;
while (++index < length) {
character = string.charAt(index);
charCode = character.charCodeAt(0);
if (charCode >= 0xD800 && charCode <= 0xDBFF) {
output.push(character + string.charAt(++index));
} else {
output.push(character);
}
}
return output;
}
var symbols = getSymbols('𝌆');
symbols.forEach(function(symbol) {
// ...
});
```
替换(`String.prototype.replace`)、截取子字符串(`String.prototype.substring`, `String.prototype.slice`)等其他字符串操作,都必须做类似的处理。
## Base64转码
Base64是一种编码方法,可以将任意字符转成可打印字符。使用这种编码方法,主要不是为了加密,而是为了不出现特殊字符,简化程序的处理。
JavaScript原生提供两个Base64相关方法。
- btoa():字符串或二进制值转为Base64编码
- atob():Base64编码转为原来的编码
```javascript
var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"
```
这两个方法不适合非ASCII码的字符,会报错。
```javascript
btoa('你好')
// Uncaught DOMException: The string to be encoded contains characters outside of the Latin1 range.
```
要将非ASCII码字符转为Base64编码,必须中间插入一个转码环节,再使用这两个方法。
```javascript
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
```
<h2 id="2.4">对象</h2>
## 概述
### 生成方法
对象(object)是JavaScript的核心概念,也是最重要的数据类型。JavaScript的所有数据都可以被视为对象。
简单说,所谓对象,就是一种无序的数据集合,由若干个“键值对”(key-value)构成。
```javascript
var o = {
p: 'Hello World'
};
```
上面代码中,大括号就定义了一个对象,它被赋值给变量`o`。这个对象内部包含一个键值对(又称为“成员”),`p`是“键名”(成员的名称),字符串`Hello World`是“键值”(成员的值)。键名与键值之间用冒号分隔。如果对象内部包含多个键值对,每个键值对之间用逗号分隔。
```javascript
var o = {
p1: 'Hello',
p2: 'World'
};
```
对象的生成方法,通常有三种方法。除了像上面那样直接使用大括号生成(`{}`),还可以用`new`命令生成一个Object对象的实例,或者使用`Object.create`方法生成。
```javascript
var o1 = {};
var o2 = new Object();
var o3 = Object.create(null);
```
上面三行语句是等价的。一般来说,第一种采用大括号的写法比较简洁,第二种采用构造函数的写法清晰地表示了意图,第三种写法一般用在需要对象继承的场合。关于第二种写法,详见《标准库》一章的Object对象一节,第三种写法详见《面向对象编程》一章。
### 键名
对象的所有键名都是字符串,所以加不加引号都可以。上面的代码也可以写成下面这样。
```javascript
var o = {
'p': 'Hello World'
};
```
如果键名是数值,会被自动转为字符串。
```javascript
var o ={
1: 'a',
3.2: 'b',
1e2: true,
1e-2: true,
.234: true,
0xFF: true,
};
o
// Object {
// 1: "a",
// 100: true,
// 255: true,
// 3.2: "b",
// 0.01: true,
// 0.234: true
// }
```
但是,如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),也不是数字,则必须加上引号,否则会报错。
```javascript
var o = {
'1p': "Hello World",
'h w': "Hello World",
'p+q': "Hello World"
};
```
上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。
注意,JavaScript的保留字可以不加引号当作键名。
```javascript
var obj = {
for: 1,
class: 2
};
```
### 属性
对象的每一个“键名”又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用。
```javascript
var o = {
p: function (x) {
return 2 * x;
}
};
o.p(1)
// 2
```
上面的对象就有一个方法`p`,它就是一个函数。
对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。
```javascript
var o = {
p: 123,
m: function () { ... },
}
```
上面的代码中`m`属性后面的那个逗号,有或没有都不算错。但是,ECMAScript 3不允许添加逗号,所以如果要兼容老式浏览器(比如IE 8),那就不能加这个逗号。
属性可以动态创建,不必在对象声明时就指定。
```javascript
var obj = {};
obj.foo = 123;
obj.foo // 123
```
上面代码中,直接对`obj`对象的`foo`属性赋值,结果就在运行时创建了`foo`属性。
由于对象的方法就是函数,因此也有`name`属性。
```javascript
var obj = {
m1: function m1() {},
m2: function () {}
};
obj.m1.name // m1
obj.m2.name // undefined
```
### 对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。
```javascript
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
```
上面代码中,`o1`和`o2`指向同一个对象,因此为其中任何一个变量添加属性,另一个变量都可以读写该属性。
此时,如果取消某一个变量对于原对象的引用,不会影响到另一个变量。
```javascript
var o1 = {};
var o2 = o1;
o1 = 1;
o2 // {}
```
上面代码中,`o1`和`o2`指向同一个对象,然后`o1`的值变为1,这时不会对`o2`产生影响,`o2`还是指向原来的那个对象。
但是,这种引用只局限于对象,对于原始类型的数据则是传值引用,也就是说,都是值的拷贝。
```javascript
var x = 1;
var y = x;
x = 2;
y // 1
```
上面的代码中,当`x`的值发生变化后,`y`的值并不变,这就表示`y`和`x`并不是指向同一个内存地址。
### 表达式还是语句?
对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?
```javascript
{ foo: 1 }
```
JavaScript引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含`foo`属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签`foo`,指向表达式`123`。
为了避免这种歧义性,JavaScript规定,如果行首是大括号,一律解释为语句(即代码块)。如果要解释为表达式(即对象),必须在大括号前加上圆括号。
```javascript
({ foo: 1})
```
这种差异在`eval`语句中反映得最明显。
```javascript
eval('{foo: 1}') // 123
eval('({foo: 1})') // {foo: 123}
```
上面代码中,如果没有圆括号,`eval`将其理解为一个代码块;加上圆括号以后,就理解成一个对象。
## 属性的操作
### 读取属性
读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符。
```javascript
var o = {
p: 'Hello World'
};
o.p // "Hello World"
o['p'] // "Hello World"
```
上面代码分别采用点运算符和方括号运算符,读取属性`p`。
请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理。但是,数字键可以不加引号,因为会被当作字符串处理。
```javascript
var o = {
0.7: 'Hello World'
};
o['0.7'] // "Hello World"
o[0.7] // "Hello World"
```
方括号运算符内部可以使用表达式。
```javascript
o['hello' + ' world']
o[3 + 3]
```
数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。
```javascript
obj.0xFF
// SyntaxError: Unexpected token
obj[0xFF]
// true
```
上面代码的第一个表达式,对数值键名`0xFF`使用点运算符,结果报错。第二个表达式使用方括号运算符,结果就是正确的。
### 检查变量是否声明
如果读取一个不存在的键,会返回`undefined`,而不是报错。可以利用这一点,来检查一个全局变量是否被声明。
```javascript
// 检查a变量是否被声明
if (a) {...} // 报错
if (window.a) {...} // 不报错
if (window['a']) {...} // 不报错
```
上面的后二种写法之所以不报错,是因为在浏览器环境,所有全局变量都是`window`对象的属性。`window.a`的含义就是读取`window`对象的`a`属性,如果该属性不存在,就返回`undefined`,并不会报错。
需要注意的是,后二种写法有漏洞,如果`a`属性是一个空字符串(或其他对应的布尔值为`false`的情况),则无法起到检查变量是否声明的作用。正确的做法是可以采用下面的写法。
```javascript
// 写法一
if (window.a === undefined) {
// ...
}
// 写法二
if ('a' in window) {
// ...
}
```
### 属性的赋值
点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。
```javascript
o.p = 'abc';
o['p'] = 'abc';
```
上面代码分别使用点运算符和方括号运算符,对属性p赋值。
JavaScript允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。
```javascript
var o = { p: 1 };
// 等价于
var o = {};
o.p = 1;
```
### 查看所有属性
查看一个对象本身的所有属性,可以使用`Object.keys`方法。
```javascript
var o = {
key1: 1,
key2: 2
};
Object.keys(o);
// ['key1', 'key2']
```
### 属性的删除
删除一个属性,需要使用`delete`命令。
```javascript
var o = {p: 1};
Object.keys(o) // ["p"]
delete o.p // true
o.p // undefined
Object.keys(o) // []
```
上面代码表示,一旦使用`delete`命令删除某个属性,再读取该属性就会返回`undefined`,而且`Object.keys`方法返回的该对象的所有属性中,也将不再包括该属性。
麻烦的是,如果删除一个不存在的属性,delete不报错,而且返回true。
```javascript
var o = {};
delete o.p // true
```
上面代码表示,delete命令只能用来保证某个属性的值为undefined,而无法保证该属性是否真的存在。
只有一种情况,`delete`命令会返回`false`,那就是该属性存在,且不得删除。
```javascript
var o = Object.defineProperty({}, "p", {
value: 123,
configurable: false
});
o.p // 123
delete o.p // false
```
上面代码之中,`o`对象的`p`属性是不能删除的,所以`delete`命令返回`false`(关于`Object.defineProperty`方法的介绍,请看《标准库》一章的Object对象章节)。
另外,需要注意的是,`delete`命令只能删除对象本身的属性,不能删除继承的属性(关于继承参见《面向对象编程》一节)。delete命令也不能删除var命令声明的变量,只能用来删除属性。
### in运算符
in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回`true`,否则返回`false`。
```javascript
var o = { p: 1 };
'p' in o // true
```
在JavaScript语言中,所有全局变量都是顶层对象(浏览器的顶层对象就是`window`对象)的属性,因此可以用`in`运算符判断,一个全局变量是否存在。
```javascript
// 假设变量x未定义
// 写法一:报错
if (x) { return 1; }
// 写法二:不正确
if (window.x) { return 1; }
// 写法三:正确
if ('x' in window) { return 1; }
```
上面三种写法之中,如果`x`不存在,第一种写法会报错;如果`x`的值对应布尔值`false`(比如`x`等于空字符串),第二种写法无法得到正确结果;只有第三种写法,才能正确判断变量`x`是否存在。
`in`运算符的一个问题是,它不能识别对象继承的属性。
```javascript
var o = new Object();
o.hasOwnProperty('toString') // false
'toString' in o // true
```
上面代码中,`toString`方法不是对象`o`自身的属性,而是继承的属性,`hasOwnProperty`方法可以说明这一点。但是,`in`运算符不能识别,对继承的属性也返回`true`。
### for...in循环
`for...in`循环用来遍历一个对象的全部属性。
```javascript
var o = {a: 1, b: 2, c: 3};
for (var i in o) {
console.log(o[i]);
}
// 1
// 2
// 3
```
下面是一个使用`for...in`循环,进行数组赋值的例子。
```javascript
var props = [], i = 0;
for (props[i++] in {x: 1, y: 2});
props // ['x', 'y']
```
注意,`for...in`循环遍历的是对象所有可enumberable的属性,其中不仅包括定义在对象本身的属性,还包括对象继承的属性。
```javascript
// name 是 Person 本身的属性
function Person(name) {
this.name = name;
}
// describe是Person.prototype的属性
Person.prototype.describe = function () {
return 'Name: '+this.name;
};
var person = new Person('Jane');
// for...in循环会遍历实例自身的属性(name),
// 以及继承的属性(describe)
for (var key in person) {
console.log(key);
}
// name
// describe
```
上面代码中,`name`是对象本身的属性,`describe`是对象继承的属性,`for...in`循环的遍历会包括这两者。
如果只想遍历对象本身的属性,可以使用hasOwnProperty方法,在循环内部做一个判断。
```javascript
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
```
为了避免这一点,可以新建一个继承`null`的对象。由于`null`没有任何属性,所以新对象也就不会有继承的属性了。
## with语句
`with`语句的格式如下:
```javascript
with (object) {
statements;
}
```
它的作用是操作同一个对象的多个属性时,提供一些书写的方便。
```javascript
// 例一
with (o) {
p1 = 1;
p2 = 2;
}
// 等同于
o.p1 = 1;
o.p2 = 2;
// 例二
with (document.links[0]){
console.log(href);
console.log(title);
console.log(style);
}
// 等同于
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);
```
注意,`with`区块内部的变量,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。这是因为`with`区块没有改变作用域,它的内部依然是当前作用域。
```javascript
var o = {};
with (o) {
x = "abc";
}
o.x // undefined
x // "abc"
```
上面代码中,对象`o`没有属性`x`,所以`with`区块内部对`x`的操作,等于创造了一个全局变量`x`。正确的写法应该是,先定义对象`o`的属性`x`,然后在`with`区块内操作它。
```javascript
var o = {};
o.x = 1;
with (o) {
x = 2;
}
o.x // 2
```
这是`with`语句的一个很大的弊病,就是绑定对象不明确。
```javascript
with (o) {
console.log(x);
}
```
单纯从上面的代码块,根本无法判断`x`到底是全局变量,还是`o`对象的一个属性。这非常不利于代码的除错和模块化,编译器也无法对这段代码进行优化,只能留到运行时判断,这就拖慢了运行速度。因此,建议不要使用`with`语句,可以考虑用一个临时变量代替`with`。
```javascript
with(o1.o2.o3) {
console.log(p1 + p2);
}
// 可以写成
var temp = o1.o2.o3;
console.log(temp.p1 + temp.p2);
```
`with`语句少数有用场合之一,就是替换模板变量。
```javascript
var str = 'Hello <%= name %>!';
```
上面代码是一个模板字符串。假定有一个`parser`函数,可以将这个字符串解析成下面的样子。
```javascript
parser(str)
// '"Hello ", name, "!"'
```
那么,就可以利用`with`语句,进行模板变量替换。
```javascript
var str = 'Hello <%= name %>!';
var o = {
name: 'Alice'
};
function tmpl(str, obj) {
str = 'var p = [];' +
'with (obj) {p.push(' + parser(str) + ')};' +
'return p;'
var r = (new Function('obj', str))(obj);
return r.join('');
}
tmpl(str, o)
// "Hello Alice!"
```
上面代码的核心逻辑是下面的部分。
```javascript
var o = {
name: 'Alice'
};
var p = [];
with (o) {
p.push('Hello ', name, '!');
};
p.join('') // "Hello Alice!"
```
上面代码中,`with`区块内部,模板变量`name`可以被对象`o`的属性替换,而`p`依然是全局变量。这就是很多模板引擎的实现原理。
<h2 id="2.5">数组</h2>
## 数组的定义
数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示。
```javascript
var arr = ['a', 'b', 'c'];
```
上面代码中的`a`、`b`、`c`就构成一个数组,两端的方括号是数组的标志。`a`是0号位置,`b`是1号位置,`c`是2号位置。
除了在定义时赋值,数组也可以先定义后赋值。
```javascript
var arr = [];
arr[0] = 'a';
arr[1] = 'b';
arr[2] = 'c';
```
任何类型的数据,都可以放入数组。
```javascript
var arr = [
{a: 1},
[1, 2, 3],
function() {return true;}
];
arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}
```
上面数组`arr`的3个成员依次是对象、数组、函数。
如果数组的元素还是数组,就形成了多维数组。
```javascript
var a = [[1, 2], [3, 4]];
a[0][1] // 2
a[1][1] // 4
```
## 数组的本质
本质上,数组属于一种特殊的对象。`typeof`运算符会返回数组的类型是`object`。
```javascript
typeof [1, 2, 3] // "object"
```
上面代码表明,`typeof`运算符认为数组的类型就是对象。
数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2...)。
```javascript
var arr = ['a', 'b', 'c'];
Object.keys(arr)
// ["0", "1", "2"]
```
上面代码中,`Object.keys`方法返回数组的所有键名。可以看到数组的键名就是整数0、1、2。
由于数组成员的键名是固定的,因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。
JavaScript语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。
```javascript
var arr = ['a', 'b', 'c'];
arr['0'] // 'a'
arr[0] // 'a'
```
上面代码分别用数值和字符串作为键名,结果都能读取数组。原因是数值键名被自动转为了字符串。
需要注意的是,这一条在赋值时也成立。如果一个值可以被转换为整数,则以该值为键名,等于以对应的整数为键名。
```javascript
var a = [];
a['1000'] = 'abc';
a[1000] // 'abc'
a[1.00] = 6;
a[1] // 6
```
上面代码表明,由于字符串“1000”和浮点数1.00都可以转换为整数,所以视同为整数键赋值。
上一节说过,对象有两种读取成员的方法:“点”结构(`object.key`)和方括号结构(`object[key]`)。但是,对于数值的键名,不能使用点结构。
```javascript
var arr = [1, 2, 3];
arr.0 // SyntaxError
```
上面代码中,`arr.0`的写法不合法,因为单独的数值不能作为标识符(identifier)。所以,数组成员只能用方括号`arr[0]`表示(方括号是运算符,可以接受数值)。
## length属性
数组的length属性,返回数组的成员数量。
```javascript
['a', 'b', 'c'].length // 3
```
JavaScript使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有4294967295个(2<sup>32</sup>-1)个,也就是说`length`属性的最大值就是4294967295。
数组的`length`属性与对象的`length`属性有区别,只要是数组,就一定有`length`属性,而对象不一定有。而且,数组的`length`属性是一个动态的值,等于键名中的最大整数加上1。
```javascript
var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001
```
上面代码表示,数组的数字键不需要连续,`length`属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
`length`属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到`length`设置的值。
```javascript
var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]
```
上面代码表示,当数组的`length`属性设为2(即最大的整数键只能是1)那么整数键2(值为`c`)就已经不在数组中了,被自动删除了。
将数组清空的一个有效方法,就是将`length`属性设为0。
```javascript
var arr = [ 'a', 'b', 'c' ];
arr.length = 0;
arr // []
```
如果人为设置`length`大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位。
```javascript
var a = ['a'];
a.length = 3;
a[1] // undefined
```
上面代码表示,当`length`属性设为大于数组个数时,读取新增的位置都会返回`undefined`。
如果人为设置`length`为不合法的值,JavaScript会报错。
```javascript
// 设置负值
[].length = -1
// RangeError: Invalid array length
// 数组元素个数大于等于2的32次方
[].length = Math.pow(2,32)
// RangeError: Invalid array length
// 设置字符串
[].length = 'abc'
// RangeError: Invalid array length
```
值得注意的是,由于数组本质上是对象的一种,所以我们可以为数组添加属性,但是这不影响`length`属性的值。
```javascript
var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0
```
上面代码将数组的键分别设为字符串和小数,结果都不影响`length`属性。因为,`length`属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以`length`属性保持为0。
## 类似数组的对象
在JavaScript中,有些对象被称为“类似数组的对象”(array-like object)。意思是,它们看上去很像数组,可以使用`length`属性,但是它们并不是数组,所以无法使用一些数组的方法。
下面就是一个类似数组的对象。
```javascript
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
obj[0] // 'a'
obj[2] // 'c'
obj.length // 3
```
上面代码的变量`obj`是一个对象,但是看上去跟数组很像。所以只要有数字键和`length`属性,就是一个类似数组的对象。当然,变量`obj`无法使用数组特有的一些方法,比如`pop`和`push`方法。而且,`length`属性不是动态值,不会随着成员的变化而变化。
```javascript
var obj = {
length: 0
};
obj[3] = 'd';
obj.length // 0
```
上面代码为对象`obj`添加了一个数字键,但是`length`属性没变。这就说明了`obj`不是数组。
典型的类似数组的对象是函数的`arguments`对象,以及大多数DOM元素集,还有字符串。
```javascript
// arguments对象
function args() { return arguments }
var arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false
// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false
// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false
```
数组的`slice`方法将类似数组的对象,变成真正的数组。
```javascript
var arr = Array.prototype.slice.call(arrayLike);
```
遍历类似数组的对象,可以采用`for`循环,也可以采用数组的`forEach`方法。
```javascript
// for循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
// forEach方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i+'. '+elem);
});
}
```
由于字符串也是类似数组的对象,所以也可以用`Array.prototype.forEach.call`遍历。
```javascript
Array.prototype.forEach.call('abc', function(chr) {
console.log(chr);
});
// a
// b
// c
```
## in运算符
检查某个键名是否存在的运算符`in`,适用于对象,也适用于数组。
```javascript
2 in [ 'a', 'b', 'c' ] // true
'2' in [ 'a', 'b', 'c' ] // true
```
上面代码表明,数组存在键名为`2`的键。由于键名都是字符串,所以数值`2`会自动转成字符串。
## for...in循环和数组的遍历
使用`for...in`循环,可以遍历数组的所有元素。
```javascript
var a = [1, 2, 3];
for (var i in a) {
console.log(a[i]);
}
// 1
// 2
// 3
```
需要注意的是,`for...in`会遍历数组所有的键,即使是非数字键。
```javascript
var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
console.log(key);
}
// 0
// 1
// 2
// foo
```
上面代码在遍历数组时,也遍历到了非整数键`foo`。所以,使用`for...in`遍历数组的时候,一定要小心。
其他的数组遍历方法,就是使用`length`属性,结合`for`循环或者`while`循环。
```javascript
// for循环
var a = [1, 2, 3];
for(var i = 0; i < a.length; i++) {
console.log(a[i]);
}
// while循环
var i = 0;
while (i < a.length) {
console.log(a[i]);
i++;
}
var l = a.length;
while (l--) {
console.log(a[l]);
}
```
上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。
数组的`forEach`方法,也可以用来遍历数组,详见《标准库》一章的Array对象部分。
```javascript
var colors = ['red', 'green', 'blue'];
colors.forEach(function(color) {
console.log(color);
});
```
## 数组的空位
当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。
```javascript
var a = [1, , 1];
a.length // 3
```
上面代码表明,数组的空位不影响`length`属性。
需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的。
```javascript
var a = [1, 2, 3,];
a.length // 3
a // [1, 2, 3]
```
上面代码中,数组最后一个成员后面有一个逗号,这不影响`length`属性的值,与没有这个逗号时效果一样。
数组的空位是可以读取的,返回`undefined`。
```javascript
var a = [, , ,];
a[1] // undefined
```
使用`delete`命令删除一个值,会形成空位。
```javascript
var a = [1, 2, 3];
delete a[1];
a[1] // undefined
```
`delete`命令不影响`length`属性。
```javascript
var a = [1, 2, 3];
delete a[1];
delete a[2];
a.length // 3
```
上面代码用`delete`命令删除了两个键,对`length`属性没有影响。也就是说,`length`属性不过滤空位。所以,使用`length`属性进行数组遍历,一定要非常小心。
数组的某个位置是空位,与某个位置是`undefined`,是不一样的。如果是空位,使用数组的`forEach`方法、`for...in`结构、以及`Object.keys`方法进行遍历,空位都会被跳过。
```javascript
var a = [, , ,];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
})
// 不产生任何输出
for (var i in a) {
console.log(i);
}
// 不产生任何输出
Object.keys(a)
// []
```
如果某个位置是`undefined`,遍历的时候就不会被跳过。
```javascript
var a = [undefined, undefined, undefined];
a.forEach(function (x, i) {
console.log(i + '. ' + x);
});
// 0. undefined
// 1. undefined
// 2. undefined
for (var i in a) {
console.log(i);
}
// 0
// 1
// 2
Object.keys(a)
// ['0', '1', '2']
```
这就是说,空位就是数组没有这个元素,所以不会被遍历到,而`undefined`则表示数组有这个元素,值是`undefined`,所以遍历不会跳过。
<h2 id="2.6">函数</h2>
## 概述
函数就是一段预先设置的代码块,可以反复调用,根据输入参数的不同,返回不同的值。
JavaScript有三种方法,可以声明一个函数。
### 函数的声明
**(1)function命令**
`function`命令声明的代码区块,就是一个函数。`function`命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面。
```javascript
function print(s) {
console.log(s);
}
```
上面的代码命名了一个`print`函数,以后使用`print()`这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration)。
**(2)函数表达式**
除了用`function`命令声明函数,还可以采用变量赋值的写法。
```javascript
var print = function(s) {
console.log(s);
};
```
这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。
采用函数表达式声明函数时,`function`命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。
```javascript
var print = function x(){
console.log(typeof x);
};
x
// ReferenceError: x is not defined
print()
// function
```
上面代码在函数表达式中,加入了函数名`x`。这个`x`只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数)。因此,下面的形式声明函数也非常常见。
```javascript
var f = function f() {};
```
需要注意的是,函数的表达式需要在语句的结尾加上分号,表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微(参阅后文《变量提升》一节),这里可以近似认为是等价的。
**(3)Function构造函数**
还有第三种声明函数的方式:`Function`构造函数。
```javascript
var add = new Function(
'x',
'y',
'return (x + y)'
);
// 等同于
function add(x, y) {
return (x + y);
}
```
在上面代码中,`Function`构造函数接受三个参数,除了最后一个参数是`add`函数的“函数体”,其他参数都是`add`函数的参数。如果只有一个参数,该参数就是函数体。
```javascript
var foo = new Function(
'return "hello world"'
);
// 等同于
function foo() {
return "hello world";
}
```
`Function`构造函数可以不使用`new`命令,返回结果完全一样。
总的来说,这种声明函数的方式非常不直观,几乎无人使用。
### 函数的重复声明
如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。
```javascript
function f() {
console.log(1);
}
f() // 2
function f() {
console.log(2);
}
f() // 2
```
上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的,这一点要特别注意。
### 圆括号运算符,return语句和递归
调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。
```javascript
function add(x, y) {
return x + y;
}
add(1, 1) // 2
```
上面代码中,函数名后面紧跟一对圆括号,就会调用这个函数。
函数体内部的`return`语句,表示返回。JavaScript引擎遇到`return`语句,就直接返回`return`后面的那个表达式的值,后面即使还有语句,也不会得到执行。也就是说,`return`语句所带的那个表达式,就是函数的返回值。`return`语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回`undefined`。
函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码。
```javascript
function fib(num) {
if (num > 2) {
return fib(num - 2) + fib(num - 1);
} else {
return 1;
}
}
fib(6) // 8
```
上面代码中,`fib`函数内部又调用了`fib`,计算得到斐波那契数列的第6个元素是8。
### 第一等公民
JavaScript的函数与其他数据类型(数值、字符串、布尔值等等)处于同等地位,可以使用其他数据类型的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。
这表明,函数与其他数据类型完全是平等的,所以又称函数为第一等公民。
```javascript
function add(x, y) {
return x + y;
}
// 将函数赋值给一个变量
var operator = add;
// 将函数作为参数和返回值
function a(op){
return op;
}
a(add)(1, 1)
// 2
```
### 函数名的提升
JavaScript引擎将函数名视同变量名,所以采用`function`命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。
```javascript
f();
function f() {}
```
表面上,上面代码好像在声明之前就调用了函数`f`。但是实际上,由于“变量提升”,函数`f`被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript就会报错。
```javascript
f();
var f = function (){};
// TypeError: undefined is not a function
```
上面的代码等同于下面的形式。
```javascript
var f;
f();
f = function () {};
```
上面代码第二行,调用`f`的时候,`f`只是被声明了,还没有被赋值,等于`undefined`,所以会报错。因此,如果同时采用`function`命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义。
```javascript
var f = function() {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
```
### 不能在条件语句中声明函数
根据ECMAScript的规范,不得在非函数的代码块中声明函数,最常见的情况就是if和try语句。
```javascript
if (foo) {
function x() {}
}
try {
function x() {}
} catch(e) {
console.log(e);
}
```
上面代码分别在`if`代码块和`try`代码块中声明了两个函数,按照语言规范,这是不合法的。但是,实际情况是各家浏览器往往并不报错,能够运行。
但是由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的,这是非常容易出错的地方。
```javascript
if (false){
function f() {}
}
f() // 不报错
```
上面代码的原始意图是不声明函数`f`,但是由于`f`的提升,导致`if`语句无效,所以上面的代码不会报错。要达到在条件语句中定义函数的目的,只有使用函数表达式。
```javascript
if (false) {
var f = function () {};
}
f() // undefined
```
## 函数的属性和方法
### name属性
`name`属性返回紧跟在`function`关键字之后的那个函数名。
```javascript
function f1() {}
f1.name // 'f1'
var f2 = function () {};
f2.name // ''
var f3 = function myName() {};
f3.name // 'myName'
```
上面代码中,函数的`name`属性总是返回紧跟在`function`关键字之后的那个函数名。对于`f2`来说,返回空字符串,匿名函数的`name`属性总是为空字符串;对于`f3`来说,返回函数表达式的名字(真正的函数名还是`f3`,`myName`这个名字只在函数体内部可用)。
### length属性
`length`属性返回函数预期传入的参数个数,即函数定义之中的参数个数。
```javascript
function f(a, b) {}
f.length // 2
```
上面代码定义了空函数`f`,它的`length`属性就是定义时的参数个数。不管调用时输入了多少个参数,`length`属性始终等于2。
`length`属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的”方法重载“(overload)。
### toString()
函数的`toString`方法返回函数的源码。
```javascript
function f() {
a();
b();
c();
}
f.toString()
// function f() {
// a();
// b();
// c();
// }
```
函数内部的注释也可以返回。
```javascript
function f() {/*
这是一个
多行注释
*/}
f.toString()
// "function f(){/*
// 这是一个
// 多行注释
// */}"
```
利用这一点,可以变相实现多行字符串。
```javascript
var multiline = function (fn) {
var arr = fn.toString().split('\n');
return arr.slice(1, arr.length - 1).join('\n');
};
function f() {/*
这是一个
多行注释
*/}
multiline(f.toString())
// " 这是一个
// 多行注释"
```
## 函数作用域
### 定义
作用域(scope)指的是变量存在的范围。Javascript只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。
在函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。
```javascript
var v = 1;
function f(){
console.log(v);
}
f()
// 1
```
上面的代码表明,函数`f`内部可以读取全局变量`v`。
在函数内部定义的变量,外部无法读取,称为“局部变量”(local variable)。
```javascript
function f(){
var v = 1;
}
v // ReferenceError: v is not defined
```
上面代码中,变量`v`在函数内部定义,所以是一个局部变量,函数之外就无法读取。
函数内部定义的变量,会在该作用域内覆盖同名全局变量。
```javascript
var v = 1;
function f(){
var v = 2;
console.log(v);
}
f() // 2
v // 1
```
上面代码中,变量`v`同时在函数的外部和内部有定义。结果,在函数内部定义,局部变量`v`覆盖了全局变量`v`。
注意,对于`var`命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。
```javascript
if (true) {
var x = 5;
}
console.log(x); // 5
```
上面代码中,变量`x`在条件判断区块之中声明,结果就是一个全局变量,可以在区块之外读取。
### 函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生“变量提升”现象。`var`命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
```javascript
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
```
上面的代码等同于
```javascript
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}
```
### 函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域绑定其声明时所在的作用域。
```javascript
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
```
上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。
很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量。
```javascript
var x = function (){
console.log(a);
};
function y(f){
var a = 2;
f();
}
y(x)
// ReferenceError: a is not defined
```
上面代码将函数x作为参数,传入函数y。但是,函数x是在函数y体外声明的,作用域绑定外层,因此找不到函数y的内部变量a,导致报错。
## 参数
### 概述
函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数。
```javascript
function square(x) {
return x * x;
}
square(2) // 4
square(3) // 9
```
上式的`x`就是`square`函数的参数。每次运行的时候,需要提供这个值,否则得不到结果。
### 参数的省略
函数参数不是必需的,Javascript允许省略参数。
```javascript
function f(a, b) {
return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2
```
上面代码的函数`f`定义了两个参数,但是运行时无论提供多少个参数(或者不提供参数),JavaScript都不会报错。
被省略的参数的值就变为`undefined`。需要注意的是,函数的`length`属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
但是,没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入`undefined`。
```javascript
function f(a, b) {
return a;
}
f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
```
上面代码中,如果省略第一个参数,就会报错。
### 默认值
通过下面的方法,可以为函数的参数设置默认值。
```javascript
function f(a){
a = a || 1;
return a;
}
f('') // 1
f(0) // 1
```
上面代码的`||`表示“或运算”,即如果`a`有值,则返回`a`,否则返回事先设定的默认值(上例为1)。
这种写法会对`a`进行一次布尔运算,只有为`true`时,才会返回`a`。可是,除了`undefined`以外,`0`、空字符、`null`等的布尔值也是`false`。也就是说,在上面的函数中,不能让`a`等于`0`或空字符串,否则在明明有参数的情况下,也会返回默认值。
为了避免这个问题,可以采用下面更精确的写法。
```javascript
function f(a) {
(a !== undefined && a !== null) ? a = a : a = 1;
return a;
}
f() // 1
f('') // ""
f(0) // 0
```
上面代码中,函数`f`的参数是空字符或`0`,都不会触发参数的默认值。
### 传递方式
函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
```javascript
var p = 2;
function f(p) {
p = 3;
}
f(p);
p // 2
```
上面代码中,变量`p`是一个原始类型的值,传入函数`f`的方式是传值传递。因此,在函数内部,`p`的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。
但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
```javascript
var obj = {p: 1};
function f(o) {
o.p = 2;
}
f(obj);
obj.p // 2
```
上面代码中,传入函数`f`的是参数对象`obj`的地址。因此,在函数内部修改`obj`的属性`p`,会影响到原始值。
注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
```javascript
var obj = [1, 2, 3];
function f(o){
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
```
上面代码中,在函数`f`内部,参数对象`obj`被整个替换成另一个值。这时不会影响到原始值。这是因为,形式参数(`o`)与实际参数`obj`存在一个赋值关系。
```javascript
// 函数f内部
o = obj;
```
上面代码中,对`o`的修改都会反映在`obj`身上。但是,如果对`o`赋予一个新的值,就等于切断了`o`与`obj`的联系,导致此后的修改都不会影响到`obj`了。
某些情况下,如果需要对某个原始类型的变量,获取传址传递的效果,可以将它写成全局对象的属性。
```javascript
var a = 1;
function f(p) {
window[p] = 2;
}
f('a');
a // 2
```
上面代码中,变量`a`本来是传值传递,但是写成`window`对象的属性,就达到了传址传递的效果。
### 同名参数
如果有同名的参数,则取最后出现的那个值。
```javascript
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
```
上面的函数`f`有两个参数,且参数名都是`a`。取值的时候,以后面的`a`为准。即使后面的`a`没有值或被省略,也是以其为准。
```javascript
function f(a, a){
console.log(a);
}
f(1) // undefined
```
调用函数`f`的时候,没有提供第二个参数,`a`的取值就变成了`undefined`。这时,如果要获得第一个`a`的值,可以使用`arguments`对象。
```javascript
function f(a, a){
console.log(arguments[0]);
}
f(1) // 1
```
### arguments对象
**(1)定义**
由于JavaScript允许函数有不定数目的参数,所以我们需要一种机制,可以在函数体内部读取所有参数。这就是`arguments`对象的由来。
`arguments`对象包含了函数运行时的所有参数,`arguments[0]`就是第一个参数,`arguments[1]`就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
```javascript
var f = function(one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3
```
`arguments`对象除了可以读取参数,还可以为参数赋值(严格模式不允许这种用法)。
```javascript
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1)
// 5
```
可以通过`arguments`对象的`length`属性,判断函数调用时到底带几个参数。
```javascript
function f() {
return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
```
**(2)与数组的关系**
需要注意的是,虽然`arguments`很像数组,但它是一个对象。数组专有的方法(比如`slice`和`forEach`),不能在`arguments`对象上直接使用。
但是,可以通过`apply`方法,把`arguments`作为参数传进去,这样就可以让`arguments`使用数组方法了。
```javascript
// 用于apply方法
myfunction.apply(obj, arguments).
// 使用与另一个数组合并
Array.prototype.concat.apply([1,2,3], arguments)
```
要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组。
```javascript
var args = Array.prototype.slice.call(arguments);
// or
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
```
**(3)callee属性**
`arguments`对象带有一个`callee`属性,返回它所对应的原函数。
```javascript
var f = function(one) {
console.log(arguments.callee === f);
}
f() // true
```
可以通过`arguments.callee`,达到调用函数自身的目的。
## 函数的其他知识点
### 闭包
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
要理解闭包,首先必须理解变量作用域。前面提到,JavaScript有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。
```javascript
var n = 999;
function f1() {
console.log(n);
}
f1() // 999
```
上面代码中,函数`f1`可以读取全局变量`n`。
但是,在函数外部无法读取函数内部声明的变量。
```javascript
function f1() {
var n = 999;
}
console.log(n)
// Uncaught ReferenceError: n is not defined(
```
上面代码中,函数`f1`内部声明的变量`n`,函数外是无法读取的。
如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
```javascript
function f1() {
var n = 999;
function f2() {
console.log(n); // 999
}
}
```
上面代码中,函数`f2`就在函数`f1`内部,这时`f1`内部的所有局部变量,对`f2`都是可见的。但是反过来就不行,`f2`内部的局部变量,对`f1`就是不可见的。这就是JavaScript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然`f2`可以读取`f1`的局部变量,那么只要把`f2`作为返回值,我们不就可以在`f1`外部读取它的内部变量了吗!
```javascript
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
```
上面代码中,函数`f1`的返回值就是函数`f2`,由于`f2`可以读取`f1`的内部变量,所以就可以在外部获得`f1`的内部变量了。
闭包就是函数`f2`,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如`f2`记住了它诞生的环境`f1`,所以从`f2`可以得到`f1`的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
```javascript
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
```
上面代码中,`start`是函数`createIncrementor`的内部变量。通过闭包,`start`的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包`inc`使得函数`createIncrementor`的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么会这样呢?原因就在于`inc`始终在内存中,而`inc`的存在依赖于`createIncrementor`,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
```javascript
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = person('张三');
p1.setAge(25);
p1.getAge() // 25
```
上面代码中,函数`Person`的内部变量`_age`,通过闭包`getAge`和`setAge`,变成了返回对象`p1`的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
### 立即调用的函数表达式(IIFE)
在Javascript中,一对圆括号`()`是一种运算符,跟在函数名之后,表示调用该函数。比如,`print()`就表示调用`print`函数。
有时,我们需要在定义函数之后,立即调用该函数。这时,你不能在函数的定义之后加上圆括号,这会产生语法错误。
```javascript
function(){ /* code */ }();
// SyntaxError: Unexpected token (
```
产生这个错误的原因是,`function`这个关键字即可以当作语句,也可以当作表达式。
```javascript
// 语句
function f() {}
// 表达式
var f = function f() {}
```
为了避免解析上的歧义,JavaScript引擎规定,如果`function`关键字出现在行首,一律解释成语句。因此,JavaScript引擎看到行首是`function`关键字之后,认为这一段都是函数的定义,不应该以圆括号结尾,所以就报错了。
解决方法就是不要让`function`出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
```javascript
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
```
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称IIFE。
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个IIFE,可能就会报错。
```javascript
// 报错
(function(){ /* code */ }())
(function(){ /* code */ }())
```
上面代码的两行之间没有分号,JavaScript会将它们连在一起解释,将第二行解释为第一行的参数。
推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。
```javascript
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
```
甚至像下面这样写,也是可以的。
```javascript
!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();
```
`new`关键字也能达到这个效果。
```javascript
new function(){ /* code */ }
new function(){ /* code */ }()
// 只有传递参数时,才需要最后那个圆括号
```
通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
```javascript
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 写法二
(function (){
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
```
上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。
## eval命令
`eval`命令的作用是,将字符串当作语句执行。
```javascript
eval('var a = 1;');
a // 1
```
上面代码将字符串当作语句运行,生成了变量`a`。
放在`eval`中的字符串,应该有独自存在的意义,不能用来与`eval`以外的命令配合使用。举例来说,下面的代码将会报错。
```javascript
eval('return;');
```
由于`eval`没有自己的作用域,都在当前作用域内执行,因此可能会修改其他外部变量的值,造成安全问题。
```javascript
var a = 1;
eval('a = 2');
a // 2
```
上面代码中,`eval`命令修改了外部变量a的值。由于这个原因,所以`eval`有安全风险,如果无法做到作用域隔离,最好不要使用。此外,`eval`的命令字符串不会得到JavaScript引擎的优化,运行速度较慢,也是另一个不应该使用它的理由。通常情况下,`eval`最常见的场合是解析JSON数据字符串,正确的做法是这时应该使用浏览器提供的`JSON.parse`方法。
ECMAScript 5将`eval`的使用分成两种情况,像上面这样的调用,就叫做“直接使用”,这种情况下`eval`的作用域就是当前作用域(即全局作用域或函数作用域)。另一种情况是,`eval`不是直接调用,而是“间接调用”,此时eval的作用域总是全局作用域。
```javascript
var a = 1;
function f(){
var a = 2;
var e = eval;
e('console.log(a)');
}
f() // 1
```
上面代码中,`eval`是间接调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的`a`为全局变量。
eval的间接调用的形式五花八门,只要不是直接调用,几乎都属于间接调用。
```javascript
eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')
(1 ? eval : 0)('...')
(__ = eval)('...')
var e = eval; e('...')
(function(e) { e('...') })(eval)
(function(e) { return e })(eval)('...')
(function() { arguments[0]('...') })(eval)
this.eval('...')
this['eval']('...')
[eval][0]('...')
eval.call(this, '...')
eval('eval')('...')
```
上面这些形式都是`eval`的间接调用,因此它们的作用域都是全局作用域。
与`eval`作用类似的还有`Function`构造函数。利用它生成一个函数,然后调用该函数,也能将字符串当作命令执行。
```javascript
var jsonp = 'foo({"id":42})';
var f = new Function( "foo", jsonp );
// 相当于定义了如下函数
// function f(foo) {
// foo({"id":42});
// }
f(function(json){
console.log( json.id ); // 42
})
```
上面代码中,`jsonp`是一个字符串,`Function`构造函数将这个字符串,变成了函数体。调用该函数的时候,`jsonp`就会执行。这种写法的实质是将代码放到函数作用域执行,避免对全局作用域造成影响。
<h2 id="2.7">运算符</h2>
运算符是处理数据的基本方法,用来从现有数据得到新的数据。JavaScript与其他编程语言一样,提供了多种运算符。本节逐一介绍这些运算符。
## 加法运算符
加法运算符(`+`)是最常见的运算符之一,但是使用规则却相对复杂。因为在JavaScript语言里面,这个运算符可以完成两种运算,既可以处理算术的加法,也可以用作字符串连接,它们都写成`+`。
```javascript
// 加法
1 + 1 // 2
true + true // 2
1 + true // 2
// 字符串连接
'1' + '1' // "11"
'1.1' + '1.1' // "1.11.1"
```
它的算法步骤如下。
1. 如果运算子是对象,先自动转成原始类型的值(即先执行该对象的`valueOf`方法,如果结果还不是原始类型的值,再执行`toString`方法;如果对象是`Date`实例,则先执行`toString`方法)。
2. 两个运算子都是原始类型的值以后,只要有一个运算子是字符串,则两个运算子都转为字符串,执行字符串连接运算。
3. 否则,两个运算子都转为数值,执行加法运算。
下面是一些例子。
```javascript
'1' + {foo: 'bar'} // "1[object Object]"
'1' + 1 // "11"
'1' + true // "1true"
'1' + [1] // "11"
```
上面代码中,由于运算符左边是一个字符串,导致右边的运算子都会先转为字符串,然后执行字符串连接运算。
这种由于参数不同,而改变自身行为的现象,叫做“重载”(overload)。由于加法运算符是运行时决定到底执行那种运算,使用的时候必须很小心。
```javascript
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"
```
上面代码中,运算结果由于字符串的位置不同而不同。
下面的写法,可以用来将一个值转为字符串。
```javascript
x + ''
```
上面代码中,一个值加上空字符串,会使得该值转为字符串形式。
加法运算符会将其他类型的值,自动转为字符串,然后再执行连接运算。
```javascript
[1, 2] + [3]
// "1,23"
// 等同于
String([1, 2]) + String([3])
// '1,2' + '3'
```
上面代码中,两个数组相加,会先转成字符串,然后再连接。这种数据类型的自动转换,参见《数据类型转换》一节。
加法运算符一定有左右两个运算子,如果只有右边一个运算子,就是另一个运算符,叫做“数值运算符”。
```javascript
+ - 3 // 等同于 +(-3)
+ 1 + 2 // 等同于 +(1 + 2)
+ '1' // 1
```
上面代码中,数值运算符用于返回右边运算子的数值形式,详细解释见下文。
你可能会问,如果只有左边一个运算子,会出现什么情况?答案是会报错。
```javascript
1 +
// SyntaxError: Unexpected end of input
```
加法运算符以外的其他算术运算符(比如减法、除法和乘法),都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。
```javascript
1 - '2' // -1
1 * '2' // 2
1 / '2' // 0.5
```
上面代码中,减法、除法和乘法运算符,都是将字符串自动转为数值,然后再运算。
由于加法运算符与其他算术运算符的这种差异,会导致一些意想不到的结果,计算时要小心。
```javascript
var now = new Date();
typeof (now + 1) // "string"
typeof (now - 1) // "number"
```
上面代码中,`now`是一个`Date`对象的实例。加法运算时,得到的是一个字符串;减法运算时,得到却是一个数值。
## 算术运算符
JavaScript提供9个算术运算符,用来完成基本的算术运算。
- **加法运算符**(Addition):`x + y`
- **减法运算符**(Subtraction): `x - y`
- **乘法运算符**(Multiplication): `x * y`
- **除法运算符**(Division):`x / y`
- **余数运算符**(Remainder):`x % y`
- **自增运算符**(Increment):`++x` 或者 `x++`
- **自减运算符**(Decrement):`--x` 或者 `x--`
- **数值运算符**(Convert to number): `+x`
- **负数值运算符**(Negate):`-x`
减法、乘法、除法运算法比较单纯,就是执行相应的数学运算。下面介绍其他几个算术运算符。
### 余数运算符
余数运算符(`%`)返回前一个运算子被后一个运算子除,所得的余数。
```javascript
12 % 5 // 2
```
需要注意的是,运算结果的正负号由第一个运算子的正负号决定。
```javascript
-1 % 2 // -1
1 % -2 // 1
```
为了得到正确的负数的余数值,需要先使用绝对值函数。
```javascript
// 错误的写法
function isOdd(n) {
return n % 2 === 1;
}
isOdd(-5) // false
isOdd(-4) // false
// 正确的写法
function isOdd(n) {
return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false
```
余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。
```javascript
6.5 % 2.1
// 0.19999999999999973
```
### 自增和自减运算符
自增和自减运算符,是一元运算符,只需要一个运算子。它们的作用是将运算子首先转为数值,然后加上1或者减去1。它们会修改原始变量。
```javascript
var x = 1;
++x // 2
x // 2
--x // 1
x // 1
```
上面代码的变量`x`自增后,返回`2`,再进行自减,返回`1`。这两种情况都会使得,原始变量`x`的值发生改变。
自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值。
```javascript
var x = 1;
var y = 1;
x++ // 1
++y // 2
```
上面代码中,`x`是先返回当前值,然后自增,所以得到`1`;`y`是先自增,然后返回新的值,所以得到`2`。
### 数值运算符,负数值运算符
数值运算符(`+`)同样使用加号,但是加法运算符是二元运算符(需要两个操作数),它是一元运算符(只需要一个操作数)。
数值运算符的作用在于可以将任何值转为数值(与`Number`函数的作用相同)。
```javascript
+true // 1
+[] // 0
+{} // NaN
```
上面代码表示,非数值类型的值经过数值运算符以后,都变成了数值(最后一行`NaN`也是数值)。具体的类型转换规则,参见《数据类型转换》一节。
负数值运算符(`-`),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符。
```javascript
var x = 1;
-x // -1
-(-x) // 1
```
上面代码最后一行的圆括号不可少,否则会变成递减运算符。
数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值。
## 赋值运算符
赋值运算符(Assignment Operators)用于给变量赋值。
最常见的赋值运算符,当然就是等号(`=`),表达式`x = y`表示将`y`的值赋给`x`。
除此之外,JavaScript还提供其他11个复合的赋值运算符。
```javascript
x += y // 等同于 x = x + y
x -= y // 等同于 x = x - y
x *= y // 等同于 x = x * y
x /= y // 等同于 x = x / y
x %= y // 等同于 x = x % y
x >>= y // 等同于 x = x >> y
x <<= y // 等同于 x = x << y
x >>>= y // 等同于 x = x >>> y
x &= y // 等同于 x = x & y
x |= y // 等同于 x = x | y
x ^= y // 等同于 x = x ^ y
```
这些复合的赋值运算符,都是先进行指定运算,然后将得到值返回给左边的变量。
## 比较运算符
比较运算符用于比较两个值,然后返回一个布尔值,表示是否满足比较条件。
```javascript
2 > 1 // true
```
上面代码计算`2`是否大于`1`,返回`true`。
JavaScript一共提供了8个比较运算符。
- `==` 相等
- `===` 严格相等
- `!=` 不相等
- `!==` 严格不相等
- `<` 小于
- `<=` 小于或等于
- `>` 大于
- `>=` 大于或等于
### 比较运算符的算法
比较运算符可以比较各种类型的值,不仅仅是数值。
它的算法步骤如下。
1. 如果运算子是对象,先自动转成原始类型的值(即先执行该对象的`valueOf`方法,如果结果还不是原始类型的值,再执行`toString`方法)。
2. 如果两个运算子都是字符串,则按照字典顺序比较(实际上是比较Unicode码点)。
3. 否则,将两个运算子都转成数值,再进行比较。
下面是一个例子。
```javascript
[2] > [1] // true
// 等同于 '[2]' > '[1]'
[2] > [11] // true
// 等同于 '[2]' > '[11]'
```
上面代码是两个数组的比较,它们会先转成原始类型的值(这个例子是字符串),再进行比较。
```javascript
5 > '4' // true
true > false // true
2 > true // true
```
上面代码中,字符串和布尔值都会先转成数值,再进行比较。
### 字符串的比较
字符串按照字典顺序进行比较。
```javascript
'cat' > 'dog' // false
'cat' > 'catalog' // false
```
JavaScript引擎内部首先比较首字符的Unicode编号,如果相等,再比较第二个字符的Unicode编号,以此类推。
```javascript
'cat' > 'Cat' // true'
```
上面代码中,小写的`c`的Unicode编号(99)大于大写的`C`的Unicode编号(67),所以返回`true`。
由于,JavaScript的所有字符都有Unicode编号,因此汉字也可以比较。
```javascript
'大' > '小' // false
```
上面代码中,“大”的Unicode编号是22823,“小”是23567,因此返回`true`。
### 严格相等运算符
JavaScript提供两个相等运算符:`==`和`===`。
简单说,它们的区别是相等运算符(`==`)比较两个值是否相等,严格相等运算符(`===`)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(`===`)直接返回`false`,而相等运算符(`==`)会将它们转化成同一个类型,再用严格相等运算符进行比较。
严格相等运算符的运算规则如下。
**(1)不同类型的值**
如果两个值的类型不同,直接返回`false`。
```javascript
1 === "1" // false
true === "true" // false
```
上面代码比较数值的`1`与字符串的“1”、布尔值的`true`与字符串“true”,因为类型不同,结果都是`false`。
**(2)同一类的原始类型值**
同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回`true`,值不同就返回`false`。
```javascript
1 === 0x1 // true
```
上面代码比较十进制的1与十六进制的1,因为类型和值都相同,返回`true`。
需要注意的是,`NaN`与任何值都不相等(包括自身)。另外,正0等于负0。
```javascript
NaN === NaN // false
+0 === -0 // true
```
**(3)同一类的复合类型值**
两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个对象。
```javascript
({} === {}) // false
[] === [] // false
(function (){} === function (){}) // false
```
上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是`false`。另外,空对象的比较和空函数的比较,都放在括号内,是为了避免JavaScript引擎把行首的空对象解释成代码块,把行首的空函数解释成函数的定义。
如果两个变量引用同一个对象,则它们相等。
```javascript
var v1 = {};
var v2 = v1;
v1 === v2 // true
```
**(4)undefined和null**
`undefined`和`null`与自身严格相等。
```javascript
undefined === undefined // true
null === null // true
```
由于变量声明后默认值是`undefined`,因此两个只声明未赋值的变量是相等的。
```javascript
var v1;
var v2;
v1 === v2 // true
```
**(5)严格不相等运算符**
严格相等运算符有一个对应的“严格不相等运算符”(`!==`),两者的运算结果正好相反。
```javascript
1 !== '1' // true
```
### 相等运算符
相等运算符比较相同类型的数据时,与严格相等运算符完全一样。
比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。类型转换规则如下。
**(1)原始类型的值**
原始类型的数据会转换成数值类型再进行比较。
```javascript
1 == true // true
// 等同于 1 === 1
0 == false // true
// 等同于 0 === 0
2 == true // false
// 等同于 2 === 1
2 == false // false
// 等同于 2 === 0
'true' == true // false
// 等同于 Number('true') === Number(true)
// 等同于 NaN === 1
'' == 0 // true
// 等同于 Number('') === 0
// 等同于 0 === 0
'' == false // true
// 等同于 Number('') === Number(false)
// 等同于 0 === 0
'1' == true // true
// 等同于 Number('1') === Number(true)
// 等同于 1 === 1
'\n 123 \t' == 123 // true
// 因为字符串转为数字时,省略前置和后置的空格
```
上面代码将字符串和布尔值都转为数值,然后再进行比较。字符串与布尔值的类型转换规则,参见《数据类型转换》一节。
**(2)对象与原始类型值比较**
对象(这里指广义的对象,包括数值和函数)与原始类型的值比较时,对象转化成原始类型的值,再进行比较。
```javascript
[1] == 1 // true
// 等同于 Number([1]) == 1
[1] == '1' // true
// 等同于 String([1]) == Number('1')
[1] == true // true
// 等同于 Boolean([1]) == true
```
上面代码中,数组`[1]`分别与数值、字符串和布尔值进行比较,会先转成该类型,再进行比较。比如,与数值`1`比较时,数组`[1]`会被自动转换成数值`1`,因此得到`true`。对象的类型转换规则,参见《数据类型转换》一节。
**(3)undefined和null**
`undefined`和`null`与其他类型的值比较时,结果都为`false`,它们互相比较时结果为`true`。
```javascript
false == null // false
false == undefined // false
0 == null // false
0 == undefined // false
undefined == null // true
```
**(4)相等运算符的缺点**
相等运算符隐藏的类型转换,会带来一些违反直觉的结果。
```javascript
'' == '0' // false
0 == '' // true
0 == '0' // true
2 == true // false
2 == false // false
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
' \t\r\n ' == 0 // true
```
上面这些表达式都很容易出错,因此不要使用相等运算符(`==`),最好只使用严格相等运算符(`===`)。
**(5)不相等运算符**
相等运算符有一个对应的“不相等运算符”(`!=`),两者的运算结果正好相反。
```javascript
1 != '1' // false
```
## 布尔运算符
布尔运算符用于将表达式转为布尔值,一共包含四个运算符。
- 取反运算符:`!`
- 且运算符:`&&`
- 或运算符:`||`
- 三元运算符:`?:`
### 取反运算符(!)
取反运算符形式上是一个感叹号,用于将布尔值变为相反值,即`true`变成`false`,`false`变成`true`。
```javascript
!true // false
!false // true
```
对于非布尔值的数据,取反运算符会自动将其转为布尔值。规则是,以下六个值取反后为`true`,其他值取反后都为`false`。
- `undefined`
- `null`
- `false`
- `0`(包括`+0`和`-0`)
- `NaN`
- 空字符串(`''`)
这意味着,取反运算符有转换数据类型的作用。
```javascript
!undefined // true
!null // true
!0 // true
!NaN // true
!"" // true
!54 // false
!'hello' // false
![] // false
!{} // false
```
上面代码中,不管什么类型的值,经过取反运算后,都变成了布尔值。
如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与`Boolean`函数的作用相同。这是一种常用的类型转换的写法。
```javascript
!!x
// 等同于
Boolean(x)
```
上面代码中,不管`x`是什么类型的值,经过两次取反运算后,变成了与`Boolean`函数结果相同的布尔值。所以,两次取反就是将一个值转为布尔值的简便写法。
取反运算符的这种将任意数据自动转为布尔值的功能,对下面三种布尔运算符(且运算符、或运算符、三元条件运算符)都成立。
### 且运算符(&&)
且运算符的运算规则是:如果第一个运算子的布尔值为`true`,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为`false`,则直接返回第一个运算子的值,且不再对第二个运算子求值。
```javascript
't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""
var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
```
上面代码的最后一部分表示,由于且运算符的第一个运算子的布尔值为`false`,则直接返回它的值`0`,而不再对第二个运算子求值,所以变量`x`的值没变。
这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代`if`结构,比如下面是一段`if`结构的代码,就可以用且运算符改写。
```javascript
if (i !== 0 ) {
doSomething();
}
// 等价于
i && doSomething();
```
上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。
且运算符可以多个连用,这时返回第一个布尔值为`false`的表达式的值。
```javascript
true && 'foo' && '' && 4 && 'foo' && true
// ''
```
上面代码中第一个布尔值为`false`的表达式为第三个表达式,所以得到一个空字符串。
### 或运算符(||)
或运算符(`||`)的运算规则是:如果第一个运算子的布尔值为`true`,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为`false`,则返回第二个运算子的值。
```javascript
't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""
```
短路规则对这个运算符也适用。
或运算符可以多个连用,这时返回第一个布尔值为true的表达式的值。
```javascript
false || 0 || '' || 4 || 'foo' || true
// 4
```
上面代码中第一个布尔值为`true`的表达式是第四个表达式,所以得到数值4。
或运算符常用于为一个变量设置默认值。
```javascript
function saveText(text) {
text = text || '';
// ...
}
// 或者写成
saveText(this.text || '')
```
上面代码表示,如果函数调用时,没有提供参数,则该参数默认设置为空字符串。
### 三元条件运算符(?:)
三元条件运算符用问号(?)和冒号(:),分隔三个表达式。如果第一个表达式的布尔值为`true`,则返回第二个表达式的值,否则返回第三个表达式的值。
```javascript
't' ? 'hello' : 'world' // "hello"
0 ? 'hello' : 'world' // "world"
```
上面代码的`t`和`0`的布尔值分别为`true`和`false`,所以分别返回第二个和第三个表达式的值。
通常来说,三元条件表达式与`if...else`语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,`if...else`是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用`if..else`。
```javascript
console.log(true ? 'T' : 'F');
```
上面代码中,`console.log`方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用`if...else`语句,就必须改变整个代码写法了。
## 位运算符
### 简介
位运算符用于直接对二进制位进行计算,一共有7个。
- **或运算**(or):符号为`|`;,表示两个二进制位中只要有一个为1,则结果为1,否则为0。
- **与运算**(and):符号为`&`,表示如果两个二进制位都为1,则结果为1,否则为0。
- **否运算**(not):符号为`~`,表示将一个二进制位变成相反值。
- **异或运算**(xor):符号为ˆ,表示如果两个二进制位中有且仅有一个为1时,结果为1,否则为0。
- **左移运算**(left shift):符号为`<<`,详见下文解释。
- **右移运算**(right shift):符号为`>>`,详见下文解释。
- **带符号位的右移运算**(zero filled right shift):符号为`>>>`,详见下文解释。
这些位运算符直接处理每一个比特位,所以是非常底层的运算,好处是速度极快,缺点是很不直观,许多场合不能使用它们,否则会带来过度的复杂性。
有一点需要特别注意,位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再运行。另外,虽然在JavaScript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数。
```javascript
i = i | 0;
```
上面这行代码的意思,就是将`i`(不管是整数或小数)转为32位整数。
利用这个特性,可以写出一个函数,将任意数值转为32位整数。
```javascript
function ToInt32(x) {
return x | 0;
}
ToInt32(1.001) // 1
ToInt32(1.999) // 1
ToInt32(1) // 1
ToInt32(-1) // -1
ToInt32(Math.pow(2, 32) + 1) // 1
ToInt32(Math.pow(2, 32) - 1) // -1
```
上面代码中,最后两行得到`1`和`-1`,是因为一个整数大于32位的数位都会被舍去。
### “或运算”与“与运算”
这两种运算比较容易理解,就是逐位比较两个运算子。“或运算”的规则是,两个二进制位之中只要有一个为1,就返回1,否则返回0。“与运算”的规则是,两个二进制位之中只要有一个位为0,就返回0,否则返回1。
```javascript
0 | 3 // 3
0 & 3 // 0
```
上面两个表达式,0和3的二进制形式分别是`00`和`11`,所以进行“或运算”会得到`11`(即3),进行”与运算“会得到`00`(即0)。
位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与0进行或运算,等同于对该数去除小数部分,即取整数位。
```javascript
2.9 | 0 // 2
-2.9 | 0 // -2
```
需要注意的是,这种取整方法不适用超过32位整数最大值2147483647的数。
```javascript
2147483649.4 | 0;
// -2147483647
```
### 否运算
“否运算”将每个二进制位都变为相反值(0变为1,1变为0)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制。
```javascript
~ 3 // -4
```
上面表达式对3进行“否运算”,得到-4。之所以会有这样的结果,是因为位运算时,JavaScirpt内部将所有的运算子都转为32位的二进制整数再进行运算。3在JavaScript内部是`00000000000000000000000000000011`,否运算以后得到`11111111111111111111111111111100`,由于第一位是1,所以这个数是一个负数。JavaScript内部采用2的补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。这个数减去1等于`11111111111111111111111111111011`,再取一次反得到`00000000000000000000000000000100`,再加上负号就是-4。考虑到这样的过程比较麻烦,可以简单记忆成,一个数与自身的取反值相加,等于-1。
```javascript
~ -3 // 2
```
上面表达式可以这样算,-3的取反值等于-1减去-3,结果为2。
对一个整数连续两次“否运算”,得到它自身。
```javascript
~~3 // 3
```
所有的位运算都只对整数有效。否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次否运算,能达到取整效果。
```javascript
~~2.9 // 2
~~47.11 // 47
~~1.9999 // 1
~~3 // 3
```
使用否运算取整,是所有取整方法中最快的一种。
对字符串进行否运算,JavaScript引擎会先调用Number函数,将字符串转为数值。
```javascript
// 以下例子相当于~Number('011')
~'011' // -12
~'42 cats' // -1
~'0xcafebabe' // 889275713
~'deadbeef' // -1
// 以下例子相当于~~Number('011')
~~'011'; // 11
~~'42 cats'; // 0
~~'0xcafebabe'; // -889275714
~~'deadbeef'; // 0
```
Number函数将字符串转为数值的规则,参见《数据的类型转换》一节。否运算对特殊数值的处理是:超出32位的整数将会被截去超出的位数,NaN和Infinity转为0。
对于其他类型的参数,否运算也是先用`Number`转为数值,然后再进行处理。
```javascript
~~[] // 0
~~NaN // 0
~~null // 0
```
### 异或运算
“异或运算”在两个二进制位不同时返回1,相同时返回0。
```javascript
0 ^ 3 // 3
```
上面表达式中,`0`的二进制形式是`00`,`3`的二进制形式是`11`,它们每一个二进制位都不同,所以得到11(即3)。
“异或运算”有一个特殊运用,连续对两个数a和b进行三次异或运算,aˆ=b, bˆ=a, aˆ=b,可以互换它们的值(详见[维基百科](http://en.wikipedia.org/wiki/XOR_swap_algorithm))。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值。
```javascript
var a = 10;
var b = 99;
a ^= b, b ^= a, a ^= b;
a // 99
b // 10
```
这是互换两个变量的值的最快方法。
异或运算也可以用来取整。
```javascript
12.9 ^ 0 // 12
```
### 左移运算符(<<)
左移运算符表示将一个数的二进制值,向前移动指定的位数,尾部补0,即乘以2的指定次方。
```javascript
// 4 的二进制形式为100,
// 左移一位为1000(即十进制的8)
// 相当于乘以2的1次方
4 << 1
// 8
-4 << 1
// -8
```
上面代码中,`-4`左移一位得到`-8`,是因为`-4`的二进制形式是`11111111111111111111111111111100`,左移一位后得到`11111111111111111111111111111000`,该数转为十进制(减去1后取反,再加上负号)即为`-8`。
如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效。
```javascript
13.5 << 0
// 13
-13.5 << 0
// -13
```
左移运算符用于二进制数值非常方便。
```javascript
var color = {r: 186, g: 218, b: 85};
// RGB to HEX
var rgb2hex = function(r, g, b) {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
.toString(16)
.substr(1);
}
rgb2hex(color.r,color.g,color.b)
// "#bada55"
```
上面代码使用左移运算符,将颜色的RGB值转为HEX值。
### 右移运算符(>>)
右移运算符表示将一个数的二进制形式向右移动,头部补上最左位的值,即整数补0,负数补1。
```javascript
4 >> 1
// 2
/*
// 因为4的二进制形式为00000000000000000000000000000100,
// 右移一位得到00000000000000000000000000000010,
// 即为十进制的2
*/
-4 >> 1
// -2
/*
// 因为-4的二进制形式为11111111111111111111111111111100,
// 右移一位,头部补1,得到11111111111111111111111111111110,
// 即为十进制的-2
*/
```
右移运算可以模拟2的整除运算。
```javascript
5 >> 1
// 相当于 5 / 2 = 2
21 >> 2
// 相当于 21 / 4 = 5
21 >> 3
// 相当于 21 / 8 = 2
21 >> 4
// 相当于 21 / 16 = 1
```
### 带符号位的右移运算符(>>>)
该运算符表示将一个数的二进制形式向右移动,不管正数或负数,头部一律补0。所以,该运算总是得到正值,这就是它的名称“带符号位的右移”的涵义。对于正数,该运算的结果与右移运算符(>>)完全一致,区别主要在于负数。
```javascript
4 >>> 1
// 2
-4 >>> 1
// 2147483646
/*
// 因为-4的二进制形式为11111111111111111111111111111100,
// 带符号位的右移一位,得到01111111111111111111111111111110,
// 即为十进制的2147483646。
*/
```
这个运算实际上将一个值转为32位无符号整数。
查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符。
```javascript
-1 >>> 0 // 4294967295
```
上面代码表示,`-1`作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即`2^32 -1`,等于32个`1`)。
### 开关作用
位运算符可以用作设置对象属性的开关。
假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关。
```javascript
var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000
```
上面代码设置A、B、C、D四个开关,每个开关分别占有一个二进制位。
然后,就可以用“与运算”检验,当前设置是否打开了指定开关。
```javascript
var flags = 3; // 二进制的0101
if (flags & FLAG_C) {
// ...
}
// 0101 & 0100 => 0100 => true
```
上面代码检验是否打开了开关C。如果打开,会返回`true`,否则返回`false`。
现在假设需要打开ABD三个开关,我们可以构造一个掩码变量。
```javascript
var mask = FLAG_A | FLAG_B | FLAG_D;
// 0001 | 0010 | 1000 => 1011
```
上面代码对ABD三个变量进行“或运算”,得到掩码值为二进制的1011。
有了掩码,“或运算”可以将当前设置改成指定设置。
```javascript
flags = flags | mask;
```
“与运算”可以将当前设置中凡是与开关设置不一样的项,全部关闭。
```javascript
flags = flags & mask;
```
“异或运算”可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得到原来的值。
```javascript
flags = flags ^ mask;
```
“否运算”可以翻转当前设置,即原设置为0,运算后变为1;原设置为1,运算后变为0。
```javascript
flags = ~flags;
```
## 其他运算符
### 圆括号运算符
在JavaScript中,圆括号是一种运算符,它有两种用法:如果把表达式放在圆括号之中,作用是求值;如果跟在函数的后面,作用是调用函数。
把表达式放在圆括号之中,将返回表达式的值。
{% highlight javascript %}
(1) // 1
('a') // a
(1+2) // 3
{% endhighlight %}
把对象放在圆括号之中,则会返回对象的值,即对象本身。
```javascript
var o = {p:1};
(o)
// Object {p: 1}
```
将函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数,即对函数求值。
```javascript
function f(){return 1;}
(f) // function f(){return 1;}
f() // 1
```
上面的代码先定义了一个函数,然后依次将函数放在圆括号之中、将圆括号跟在函数后面,得到的结果是不一样的。
由于圆括号的作用是求值,如果将语句放在圆括号之中,就会报错,因为语句没有返回值。
```javascript
(var a =1)
// SyntaxError: Unexpected token var
```
### void运算符
`void`运算符的作用是执行一个表达式,然后不返回任何值,或者说返回`undefined`。
```javascript
void 0 // undefined
void(0) // undefined
```
上面是`void`运算符的两种写法,都正确。建议采用后一种形式,即总是使用括号。因为`void`运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,`void 4 + 7`实际上等同于`(void 4) + 7`。
下面是`void`运算符的一个例子。
```javascript
var x = 3;
void (x = 5) //undefined
x // 5
```
这个运算符主要是用于书签工具(bookmarklet),以及用于在超级链接中插入代码,目的是返回`undefined`可以防止网页跳转。
```html
<a href="javascript:void window.open('http://example.com/')">
点击打开新窗口
</a>
```
上面代码用于在网页中创建一个链接,点击后会打开一个新窗口。如果没有`void`,点击后就会在当前窗口打开链接。
下面是常见的网页中触发鼠标点击事件的写法。
```html
<a href="http://example.com" onclick="f();">文字</a>
```
上面代码有一个问题,函数`f`必须返回`false`,或者说`onclick`事件必须返回`false`,否则会引起浏览器跳转到`example.com`。
```javascript
function f() {
// some code
return false;
}
```
或者写成
```html
<a href="http://example.com" onclick="f();return false;">文字</a>
```
`void`运算符可以取代上面两种写法。
```html
<a href="javascript: void(f())">文字</a>
```
下面的代码会提交表单,但是不会产生页面跳转。
```html
<a href="javascript: void(document.form.submit())">
文字</a>
```
### 逗号运算符
逗号运算符用于对两个表达式求值,并返回后一个表达式的值。
```javascript
'a', 'b' // "b"
var x = 0;
var y = (x++, 10);
x // 1
y // 10
```
上面代码中,逗号运算符返回后一个表达式的值。
## 运算顺序
**(1)运算符的优先级**
JavaScript各种运算符的优先级别(Operator Precedence)是不一样的。优先级高的运算符先执行,优先级低的运算符后执行。
```javascript
4 + 5 * 6 // 34
```
上面的代码中,乘法运算符(`*`)的优先性高于加法运算符(`+`),所以先执行乘法,再执行加法,相当于下面这样。
```javascript
4 + (5 * 6) // 34
```
如果多个运算符混写在一起,常常会导致令人困惑的代码。
```javascript
var x = 1;
var arr = [];
var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];
```
上面代码中,变量`y`的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实在不容易记住。
根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。因此上面的表达式,实际的运算顺序如下。
```javascript
var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];
```
记住所有运算符的优先级,几乎是不可能的,也是没有必要的。
**(2)圆括号的作用**
圆括号可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的运算符会第一个运算。
```javascript
(4 + 5) * 6 // 54
```
上面代码中,由于使用了圆括号,加法会先于乘法执行。
由于运算符的优先级别十分繁杂,且都是来自硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。
**(3)左结合与右结合**
对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合”(left-to-right associativity),即从左边开始计算。
```javascript
x + y + z
```
上面代码先计算最左边的`x`与`y`的和,然后再计算与`z`的和。
但是少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(`=`)和三元条件运算符(`?:`)。
```javascript
w = x = y = z;
q = a ? b : c ? d : e ? f : g;
```
上面代码的运算结果,相当于下面的样子。
```javascript
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));
```
<h2 id="2.8">数据类型转换</h2>
JavaScript是一种动态类型语言,变量没有类型限制,可以随时赋予任意值。
```javascript
var x = y ? 1 : 'a';
```
上面代码中,变量`x`到底是数值还是字符串,取决于另一个变量`y`的值。只有在代码运行时,才可能知道`x`的类型。
虽然变量没有类型,但是数据本身和各种运算符是有类型的。如果运算符发现,数据的类型与预期不符,就会自动转换类型。比如,减法运算符预期两侧的运算子应该是数值,如果不是,就会自动将它们转为数值。
```javascript
'4' - '3' // 1
```
上面代码中,虽然是两个字符串相减,但是依然会得到结果`1`,原因就在于JavaScript将它们自动转为了数值。
本节讲解数据类型自动转换的规则,在此之前,先讲解如何手动强制转换数据类型。
## 强制转换
强制转换主要指使用`Number`、`String`和`Boolean`三个构造函数,手动将各种类型的值,转换成数字、字符串或者布尔值。
### Number()
使用`Number`函数,可以将任意类型的值转化成数值。
下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。
**(1)原始类型值的转换规则**
原始类型的值主要是字符串、布尔值、`undefined`和`null`,它们都能被`Number`转成数值或`NaN`。
```javascript
// 数值:转换后还是原来的值
Number(324) // 324
// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324
// 字符串:如果不可以被解析为数值,返回NaN
Number('324abc') // NaN
// 空字符串转为0
Number('') // 0
// 布尔值:true 转成1,false 转成0
Number(true) // 1
Number(false) // 0
// undefined:转成 NaN
Number(undefined) // NaN
// null:转成0
Number(null) // 0
```
`Number`函数将字符串转为数值,要比`parseInt`函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为`NaN`。
```javascript
parseInt('42 cats') // 42
Number('42 cats') // NaN
```
上面代码中,`parseInt`逐个解析字符,而`Number`函数整体转换字符串的类型。
另外,`Number`函数会自动过滤一个字符串前导和后缀的空格。
```javascript
Number('\t\v\r12.34\n') // 12.34
```
**(2)对象的转换规则**
如果参数是对象,`Number`将其转为数值的规则比较复杂。JavaScript的内部处理步骤如下。
1. 调用对象自身的`valueOf`方法。如果返回原始类型的值,则直接对该值使用`Number`函数,不再进行后续步骤。
2. 如果`valueOf`方法返回的还是对象,则改为调用对象自身的`toString`方法。如果返回原始类型的值,则对该值使用`Number`函数,不再进行后续步骤。
3. 如果`toString`方法返回的是对象,就报错。
请看下面的例子。
```javascript
var obj = {a: 1};
Number(obj) // NaN
// 等同于
if (typeof obj.valueOf() === 'object') {
Number(obj.toString());
} else {
Number(obj.valueOf());
}
```
上面代码中,`Number`函数将`obj`对象转为数值。首先,调用`obj.valueOf`方法, 结果返回对象本身;于是,继续调用`obj.toString`方法,这时返回字符串`[object Object]`,对这个字符串使用`Number`函数,得到`NaN`。
默认情况下,对象的`valueOf`方法返回对象本身,所以一般总是会调用`toString`方法,而`toString`方法返回对象的类型字符串(比如`[object Object]`)。所以,会有下面的结果。
```javascript
Number({}) // NaN
```
如果`toString`方法返回的不是原始类型的值,结果就会报错。
```javascript
var obj = {
valueOf: function () {
return {};
},
toString: function () {
return {};
}
};
Number(obj)
// TypeError: Cannot convert object to primitive value
```
上面代码的`valueOf`和`toString`方法,返回的都是对象,所以转成数值时会报错。
从上面的例子可以看出,`valueOf`和`toString`方法,都是可以自定义的。
```javascript
Number({
valueOf: function () {
return 2;
}
})
// 2
Number({
toString: function () {
return 3;
}
})
// 3
Number({
valueOf: function () {
return 2;
},
toString: function () {
return 3;
}
})
// 2
```
上面代码对三个对象使用`Number`函数。第一个对象返回`valueOf`方法的值,第二个对象返回`toString`方法的值,第三个对象表示`valueOf`方法先于`toString`方法执行。
### String()
使用`String`函数,可以将任意类型的值转化成字符串。转换规则如下。
**(1)原始类型值的转换规则**
- **数值**:转为相应的字符串。
- **字符串**:转换后还是原来的值。
- **布尔值**:`true`转为`"true"`,`false`转为`"false"`。
- **undefined**:转为`"undefined"`。
- **null**:转为`"null"`。
```javascript
String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) // "null"
```
**(2)对象的转换规则**
`String`函数将对象转为字符串的步骤,与`Number`函数的处理步骤基本相同,只是互换了`valueOf`方法和`toString`方法的执行顺序。
1. 先调用对象自身的`toString`方法。如果返回原始类型的值,则对该值使用`String`函数,不再进行以下步骤。
2. 如果`toString`方法返回的是对象,再调用`valueOf`方法。如果返回原始类型的值,则对该值使用`String`函数,不再进行以下步骤。
3. 如果`valueOf`方法返回的是对象,就报错。
下面是一个例子。
```javascript
String({a: 1})
// "[object Object]"
// 等同于
String({a: 1}.toString())
// "[object Object]"
```
上面代码先调用对象的`toString`方法,发现返回的是字符串`[object Object]`,就不再调用`valueOf`方法了。
如果`toString`法和`valueOf`方法,返回的都是对象,就会报错。
```javascript
var obj = {
valueOf: function () {
console.log('valueOf');
return {};
},
toString: function () {
console.log('toString');
return {};
}
};
String(obj)
// TypeError: Cannot convert object to primitive value
```
下面是通过自定义`toString`方法,改变转换成字符串时的返回值的例子。
```javascript
String({toString: function () {
return 3;
}
})
// "3"
String({valueOf: function () {
return 2;
}
})
// "[object Object]"
String({
valueOf: function () {
return 2;
},
toString: function () {
return 3;
}
})
// "3"
```
上面代码对三个对象使用`String`函数。第一个对象返回`toString`方法的值(数值3),第二个对象返回的还是`toString`方法的值(`[object Object]`),第三个对象表示`toString`方法先于`valueOf`方法执行。
### Boolean()
使用`Boolean`函数,可以将任意类型的变量转为布尔值。
它的转换规则相对简单:除了以下六个值的转换结果为`false`,其他的值全部为`true`。
- `undefined`
- `null`
- `-0`
- `0`或`+0`
- `NaN`
- `''`(空字符串)
```javascript
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
```
注意,所有对象(包括空对象)的转换结果都是`true`,甚至连`false`对应的布尔对象`new Boolean(false)`也是`true`。
```javascript
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true
```
所有对象的布尔值都是`true`,这是因为JavaScript语言设计的时候,出于性能的考虑,如果对象需要计算才能得到布尔值,对于`obj1 && obj2`这样的场景,可能会需要较多的计算。为了保证性能,就统一规定,对象的布尔值为`true`。
## 自动转换
下面介绍自动转换,它是以强制转换为基础的。
遇到以下三种情况时,JavaScript会自动转换数据类型,即转换是自动完成的,对用户不可见。
```javascript
// 1. 不同类型的数据互相运算
123 + 'abc' // "123abc"
// 2. 对非布尔值类型的数据求布尔值
if ('abc') {
console.log('hello')
} // "hello"
// 3. 对非数值类型的数据使用一元运算符(即“+”和“-”)
+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN
```
自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用`String`函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。
由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用`Boolean`、`Number`和`String`函数进行显式转换。
### 自动转换为布尔值
当JavaScript遇到预期为布尔值的地方(比如`if`语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用`Boolean`函数。
因此除了以下六个值,其他都是自动转为`true`。
- `undefined`
- `null`
- `-0`
- `0`或`+0`
- `NaN`
- `''`(空字符串)
下面这个例子中,条件部分的每个值都相当于`false`,使用否定运算符后,就变成了`true`。
```javascript
if ( !undefined
&& !null
&& !0
&& !NaN
&& !''
) {
console.log('true');
} // true
```
下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是`Boolean`函数。
```javascript
// 写法一
expression ? true : false
// 写法二
!! expression
```
### 自动转换为字符串
当JavaScript遇到预期为字符串的地方,就会将非字符串的数据自动转为字符串。系统内部会自动调用`String`函数。
字符串的自动转换,主要发生在加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串。
```javascript
'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"
```
这种自动转换很容易出错。
```javascript
var obj = {
width: '100'
};
obj.width + 20 // "10020"
```
上面代码中,开发者可能期望返回`120`,但是由于自动转换,实际上返回了一个字符`10020`。
### 自动转换为数值
当JavaScript遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用`Number`函数。
除了加法运算符有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值。
```javascript
'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
```
上面代码中,运算符两侧的运算子,都被转成了数值。
一元运算符也会把运算子转成数值。
```javascript
+'abc' // NaN
-'abc' // NaN
+true // 1
-false // 0
```
<h2 id="2.9">错误处理机制</h2>
## Error对象
一旦代码解析或运行时发生错误,JavaScript引擎就会自动产生并抛出一个Error对象的实例,然后整个程序就中断在发生错误的地方。
Error对象的实例有三个最基本的属性:
- **name**:错误名称
- **message**:错误提示信息
- **stack**:错误的堆栈(非标准属性,但是大多数平台支持)
利用name和message这两个属性,可以对发生什么错误有一个大概的了解。
```javascript
if (error.name){
console.log(error.name + ": " + error.message);
}
```
上面代码表示,显示错误的名称以及出错提示信息。
stack属性用来查看错误发生时的堆栈。
```javascript
function throwit() {
throw new Error('');
}
function catchit() {
try {
throwit();
} catch(e) {
console.log(e.stack); // print stack trace
}
}
catchit()
// Error
// at throwit (~/examples/throwcatch.js:9:11)
// at catchit (~/examples/throwcatch.js:3:9)
// at repl:1:5
```
上面代码显示,抛出错误首先是在throwit函数,然后是在catchit函数,最后是在函数的运行环境中。
## JavaScript的原生错误类型
Error对象是最一般的错误类型,在它的基础上,JavaScript还定义了其他6种错误,也就是说,存在Error的6个派生对象。
**(1)SyntaxError**
SyntaxError是解析代码时发生的语法错误。
```javascript
// 变量名错误
var 1a;
// 缺少括号
console.log 'hello');
```
**(2)ReferenceError**
ReferenceError是引用一个不存在的变量时发生的错误。
```javascript
unknownVariable
// ReferenceError: unknownVariable is not defined
```
另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值。
```javascript
console.log() = 1
// ReferenceError: Invalid left-hand side in assignment
this = 1
// ReferenceError: Invalid left-hand side in assignment
```
上面代码对函数console.log的运行结果和this赋值,结果都引发了ReferenceError错误。
**(3)RangeError**
RangeError是当一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。
```javascript
new Array(-1)
// RangeError: Invalid array length
(1234).toExponential(21)
// RangeError: toExponential() argument must be between 0 and 20
```
**(4)TypeError**
TypeError是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数。
```javascript
new 123
//TypeError: number is not a func
var obj = {};
obj.unknownMethod()
// TypeError: undefined is not a function
```
上面代码的第二种情况,调用对象不存在的方法,会抛出TypeError错误。
**(5)URIError**
URIError是URI相关函数的参数不正确时抛出的错误,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数。
```javascript
decodeURI('%2')
// URIError: URI malformed
```
**(6)EvalError**
eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再在ES5中出现了,只是为了保证与以前代码兼容,才继续保留。
以上这6种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,人为生成错误对象的实例。
```javascript
new Error("出错了!");
new RangeError("出错了,变量超出有效范围!");
new TypeError("出错了,变量类型无效!");
```
上面代码表示新建错误对象的实例,实质就是手动抛出错误。可以看到,错误对象的构造函数接受一个参数,代表错误提示信息(message)。
## 自定义错误
除了JavaScript内建的7种错误对象,还可以定义自己的错误对象。
```javascript
function UserError(message) {
this.message = message || "默认信息";
this.name = "UserError";
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
```
上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义的错误了。
```javascript
new UserError("这是自定义的错误!");
```
## throw语句
`throw`语句的作用是中断程序执行,抛出一个意外或错误。它接受一个表达式作为参数,可以抛出各种值。
```javascript
// 抛出一个字符串
throw "Error!";
// 抛出一个数值
throw 42;
// 抛出一个布尔值
throw true;
// 抛出一个对象
throw {toString: function() { return "Error!"; } };
```
上面代码表示,`throw`可以接受各种值作为参数。JavaScript引擎一旦遇到`throw`语句,就会停止执行后面的语句,并将`throw`语句的参数值,返回给用户。
如果只是简单的错误,返回一条出错信息就可以了,但是如果遇到复杂的情况,就需要在出错以后进一步处理。这时最好的做法是使用`throw`语句手动抛出一个`Error`对象。
```javascript
throw new Error('出错了!');
```
上面语句新建一个`Error`对象,然后将这个对象抛出,整个程序就会中断在这个地方。
`throw`语句还可以抛出用户自定义的错误。
```javascript
function UserError(message) {
this.message = message || "默认信息";
this.name = "UserError";
}
UserError.prototype.toString = function (){
return this.name + ': "' + this.message + '"';
}
throw new UserError("出错了!");
```
可以通过自定义一个`assert`函数,规范化`throw`抛出的信息。
```javascript
function assert(expression, message) {
if (!expression)
throw {name: 'Assertion Exception', message: message};
}
```
上面代码定义了一个`assert`函数,它接受一个表达式和一个字符串作为参数。一旦表达式不为真,就抛出指定的字符串。它的用法如下。
```javascript
assert(typeof myVar != 'undefined', 'myVar is undefined!');
```
`console`对象的`assert`方法,与上面函数的工作机制一模一样,所以可以直接使用。
```javascript
console.assert(typeof myVar != 'undefined', 'myVar is undefined!');
```
## try...catch结构
为了对错误进行处理,需要使用`try...catch`结构。
```javascript
try {
throw new Error('出错了!');
} catch (e) {
console.log(e.name + ": " + e.message);
console.log(e.stack);
}
// Error: 出错了!
// at <anonymous>:3:9
// ...
```
上面代码中,`try`代码块一抛出错误(上例用的是`throw`语句),JavaScript引擎就立即把代码的执行,转到`catch`代码块。可以看作,错误可以被`catch`代码块捕获。`catch`接受一个参数,表示`try`代码块抛出的值。
```javascript
function throwIt(exception) {
try {
throw exception;
} catch (e) {
console.log('Caught: '+ e);
}
}
throwIt(3);
// Caught: 3
throwIt('hello');
// Caught: hello
throwIt(new Error('An error happened'));
// Caught: Error: An error happened
```
上面代码中,`throw`语句先后抛出数值、字符串和错误对象。
`catch`代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去。
```javascript
try {
throw "出错了";
} catch (e) {
console.log(111);
}
console.log(222);
// 111
// 222
```
上面代码中,`try`代码块抛出的错误,被`catch`代码块捕获后,程序会继续向下执行。
`catch`代码块之中,还可以再抛出错误,甚至使用嵌套的`try...catch`结构。
```javascript
var n = 100;
try {
throw n;
} catch (e) {
if (e <= 50) {
// ...
} else {
throw e;
}
}
```
上面代码中,`catch`代码之中又抛出了一个错误。
为了捕捉不同类型的错误,`catch`代码块之中可以加入判断语句。
```javascript
try {
foo.bar();
} catch (e) {
if (e instanceof EvalError) {
console.log(e.name + ": " + e.message);
} else if (e instanceof RangeError) {
console.log(e.name + ": " + e.message);
}
// ...
}
```
上面代码中,`catch`捕获错误之后,会判断错误类型(`EvalError`还是`RangeError`),进行不同的处理。
`try...catch`结构是JavaScript语言受到Java语言影响的一个明显的例子。这种结构多多少少是对结构化编程原则一种破坏,处理不当就会变成类似`goto`语句的效果,应该谨慎使用。
## finally代码块
`try...catch`结构允许在最后添加一个`finally`代码块,表示不管是否出现错误,都必需在最后运行的语句。
```javascript
function cleansUp() {
try {
throw new Error('Sorry...');
} finally {
console.log('Performing clean-up');
}
}
cleansUp()
// Performing clean-up
// Error: Sorry...
```
上面代码说明,`throw`语句抛出错误以后,`finally`继续得到执行。
```javascript
function idle(x) {
try {
console.log(x);
return 'result';
} finally {
console.log("FINALLY");
}
}
idle('hello')
// hello
// FINALLY
// "result"
```
上面代码说明,即使有`return`语句在前,`finally`代码块依然会得到执行,且在其执行完毕后,才会显示`return`语句的值。
下面的例子说明,`return`语句的执行是排在`finally`代码之前,只是等`finally`代码执行完毕后才返回。
```javascript
var count = 0;
function countUp() {
try {
return count;
} finally {
count++;
}
}
countUp()
// 0
count
// 1
```
上面代码说明,`return`语句的`count`的值,是在`finally`代码块运行之前,就获取完成了。
下面是`finally`代码块用法的典型场景。
```javascript
openFile();
try {
writeFile(Data);
} catch(e) {
handleError(e);
} finally {
closeFile();
}
```
上面代码首先打开一个文件,然后在`try`代码块中写入文件,如果没有发生错误,则运行`finally`代码块关闭文件;一旦发生错误,则先使用`catch`代码块处理错误,再使用`finally`代码块关闭文件。
下面的例子充分反应了`try...catch...finally`这三者之间的执行顺序。
```javascript
function f() {
try {
console.log(0);
throw "bug";
} catch(e) {
console.log(1);
return true; // 这句原本会延迟到finally代码块结束再执行
console.log(2); // 不会运行
} finally {
console.log(3);
return false; // 这句会覆盖掉前面那句return
console.log(4); // 不会运行
}
console.log(5); // 不会运行
}
var result = f();
// 0
// 1
// 3
result
// false
```
上面代码中,`catch`代码块结束执行之前,会先执行`finally`代码块。从`catch`转入`finally`的标志,不仅有`return`语句,还有`throw`语句。
```javascript
function f() {
try {
throw '出错了!';
} catch(e) {
console.log('捕捉到内部错误');
throw e; // 这句原本会等到finally结束再执行
} finally {
return false; // 直接返回
}
}
try {
f();
} catch(e) {
// 此处不会执行
console.log('caught outer "bogus"');
}
// 捕捉到内部错误
```
上面代码中,进入`catch`代码块之后,一遇到`throw`语句,就会去执行`finally`代码块,其中有`return false`语句,因此就直接返回了,不再会回去执行`catch`代码块剩下的部分了。
某些情况下,甚至可以省略`catch`代码块,只使用`finally`代码块。
```javascript
openFile();
try {
writeFile(Data);
} finally {
closeFile();
}
```
<h2 id="2.10">编程风格</h2>
所谓"编程风格"(programming style),指的是编写代码的样式规则。不同的程序员,往往有不同的编程风格。
有人说,编译器的规范叫做"语法规则"(grammar),这是程序员必须遵守的;而编译器忽略的部分,就叫"编程风格"(programming style),这是程序员可以自由选择的。这种说法不完全正确,程序员固然可以自由选择编程风格,但是好的编程风格有助于写出质量更高、错误更少、更易于维护的程序。
所以,"编程风格"的选择不应该基于个人爱好、熟悉程度、打字量等因素,而要考虑如何尽量使代码清晰易读、减少出错。你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格。这一点,对于JavaScript这种语法自由度很高的语言尤其重要。
必须牢记的一点是,如果你选定了一种“编程风格”,就应该坚持遵守,切忌多种风格混用。如果你加入他人的项目,就应该遵守现有的风格。
## 缩进
空格和Tab键,都可以产生缩进效果(indent)。
Tab键可以节省击键次数,但不同的文本编辑器对Tab的显示不尽相同,有的显示四个空格,有的显示两个空格,所以有人觉得,空格键可以使得显示效果更统一。
无论你选择哪一种方法,都是可以接受的,要做的就是始终坚持这一种选择。不要一会使用Tab键,一会使用空格键。
## 区块
如果循环和判断的代码体只有一行,JavaScript允许该区块(block)省略大括号。
```javascript
if (a)
b();
c();
```
上面代码的原意可能是下面这样。
```javascript
if (a) {
b();
c();
}
```
但是,实际效果却是下面这样。
```javascript
if (a) {
b();
}
c();
```
因此,总是使用大括号表示区块。
另外,区块起首的大括号的位置,有许多不同的写法。
最流行的有两种。一种是起首的大括号另起一行:
```javascript
block
{
// ...
}
```
另一种是起首的大括号跟在关键字的后面。
```javascript
block {
// ...
}
```
一般来说,这两种写法都可以接受。但是,JavaScript要使用后一种,因为JavaScript会自动添加句末的分号,导致一些难以察觉的错误。
```javascript
return
{
key: value
};
// 相当于
return;
{
key: value
};
```
上面的代码的原意,是要返回一个对象,但实际上返回的是`undefined`,因为JavaScript自动在`return`语句后面添加了分号。为了避免这一类错误,需要写成下面这样。
```javascript
return {
key : value
};
```
因此,表示区块起首的大括号,不要另起一行。
## 圆括号
圆括号(parentheses)在JavaScript中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping)。
```javascript
// 圆括号表示函数的调用
console.log('abc');
// 圆括号表示表达式的组合
(1 + 2) * 3
```
我们可以用空格,区分这两种不同的括号。
> 1. 表示函数调用时,函数名与左括号之间没有空格。
>
> 2. 表示函数定义时,函数名与左括号之间没有空格。
>
> 3. 其他情况时,前面位置的语法元素与左括号之间,都有一个空格。
按照上面的规则,下面的写法都是不规范的。
```javascript
foo (bar)
return(a+b);
if(a === 0) {...}
function foo (b) {...}
function(x) {...}
```
上面代码的最后一行是一个匿名函数,function是语法关键字,不是函数名,所以与左括号之间应该要有一个空格。
## 行尾的分号
分号表示一条语句的结束。JavaScript规定,行尾的分号可以省略。事实上,确实有一些开发者行尾从来不写分号。但是,由于下面要讨论的原因,建议还是不要这个分号。
### 不使用分号的情况
有一些语法结构不需要在语句的结尾添加分号,主要是以下三种情况。
**(1)for和while循环**
```javascript
for ( ; ; ) {
} // 没有分号
while (true) {
} // 没有分号
```
需要注意的是`do...while`循环是有分号的。
```javascript
do {
a--;
} while(a > 0); // 分号不能省略
```
**(2)分支语句:if,switch,try**
```javascript
if (true) {
} // 没有分号
switch () {
} // 没有分号
try {
} catch {
} // 没有分号
```
**(3)函数的声明语句**
```javascript
function f() {
} // 没有分号
```
但是函数表达式仍然要使用分号。
```javascript
var f = function f() {
};
```
以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句。
### 分号的自动添加
除了上一节的三种情况,所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript会自动添加。
```javascript
var a = 1
// 等同于
var a = 1;
```
这种语法特性被称为“分号的自动添加”(Automatic Semicolon Insertion,简称ASI)。
因此,有人提倡省略句尾的分号。麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript就不会自动添加分号。
```javascript
// 等同于 var a = 3
var
a
=
3
// 等同于 'abc'.length
'abc'
.length
// 等同于 return a + b;
return a +
b;
// 等同于 obj.foo(arg1, arg2);
obj.foo(arg1,
arg2);
// 等同于 3 * 2 + 10 * (27 / 6)
3 * 2
+
10 * (27 / 6)
```
上面代码都会多行放在一起解释,不会每一行自动添加分号。这些例子还是比较容易看出来的,但是下面这个例子就不那么容易看出来了。
```javascript
x = y
(function () {
// ...
})();
// 等同于
x = y(function () {...})();
```
下面是更多不会自动添加分号的例子。
```javascript
// 解释为 c(d+e)
var a = b + c
(d+e).toString();
// 解释为 a = b/hi/g.exec(c).map(d)
// 正则表达式的斜杠,会当作除法运算符
a = b
/hi/g.exec(c).map(d);
// 解释为'b'['red', 'green'],
// 即把字符串当作一个数组,按索引取值
var a = 'b'
['red', 'green'].forEach(function (c) {
console.log(c);
})
// 解释为 function(x) { return x }(a++)
// 即调用匿名函数,结果f等于0
var a = 0;
var f = function(x) { return x }
(a++)
```
只有下一行的开始与本行的结尾,无法放在一起解释,JavaScript引擎才会自动添加分号。
```javascript
if (a < 0) a = 0
console.log(a)
// 等同于下面的代码,
// 因为0console没有意义
if (a < 0) a = 0;
console.log(a)
```
另外,如果一行的起首是“自增”(`++`)或“自减”(`--`)运算符,则它们的前面会自动添加分号。
```javascript
a = b = c = 1
a
++
b
--
c
console.log(a, b, c)
// 1 2 0
```
上面代码之所以会得到“1 2 0”的结果,原因是自增和自减运算符前,自动加上了分号。上面的代码实际上等同于下面的形式。
```javascript
a = b = c = 1;
a;
++b;
--c;
```
如果`continue`、`break`、`return`和`throw`这四个语句后面,直接跟换行符,则会自动添加分号。这意味着,如果`return`语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果。
```javascript
return
{ first: 'Jane' };
// 解释成
return;
{ first: 'Jane' };
```
由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。
不应该省略结尾的分号,还有一个原因。有些JavaScript代码压缩器不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。
另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号。
```javascript
;var a = 1;
// ...
```
上面这种写法就可以避免与其他脚本合并时,排在前面的脚本最后一行语句没有分号,导致运行出错的问题。
## 全局变量
JavaScript最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利。
因此,避免使用全局变量。如果不得不使用,用大写字母表示变量名,比如`UPPER_CASE`。
## 变量声明
JavaScript会自动将变量声明"提升"(hoist)到代码块(block)的头部。
```javascript
if (!o) {
var o = {};
}
// 等同于
var o;
if (!o) {
o = {};
}
```
为了避免可能出现的问题,最好把变量声明都放在代码块的头部。
```javascript
for (var i = 0; i < 10; i++) {
// ...
}
// 写成
var i;
for (i = 0; i < 10; i++) {
// ...
}
```
另外,所有函数都应该在使用之前定义,函数内部的变量声明,都应该放在函数的头部。
## new命令
JavaScript使用`new`命令,从构造函数生成一个新对象。
```javascript
var o = new myObject();
```
上面这种做法的问题是,一旦你忘了加上`new`,`myObject()`内部的`this`关键字就会指向全局对象,导致所有绑定在`this`上面的变量,都变成全局变量。
因此,建议使用`Object.create()`命令,替代`new`命令。如果不得不使用`new`,为了防止出错,最好在视觉上把构造函数与其他函数区分开来。比如,构造函数的函数名,采用首字母大写(InitialCap),其他函数名一律首字母小写。
## with语句
`with`可以减少代码的书写,但是会造成混淆。
```javascript
with (o) {
foo = bar;
}
```
上面的代码,可以有四种运行结果:
```javascript
o.foo = bar;
o.foo = o.bar;
foo = bar;
foo = o.bar;
```
这四种结果都可能发生,取决于不同的变量是否有定义。因此,不要使用`with`语句。
## 相等和严格相等
JavaScript有两个表示"相等"的运算符:"相等"(`==`)和"严格相等"(`===`)。
因为"相等"运算符会自动转换变量类型,造成很多意想不到的情况:
```javascript
0 == ''// true
1 == true // true
2 == true // false
0 == '0' // true
false == 'false' // false
false == '0' // true
’ \t\r\n ' == 0 // true
```
因此,不要使用“相等”(`==`)运算符,只使用“严格相等”(`===`)运算符。
## 语句的合并
有些程序员追求简洁,喜欢合并不同目的的语句。比如,原来的语句是
```javascript
a = b;
if (a) {
// ...
}
```
他喜欢写成下面这样。
```javascript
if (a = b) {
// ...
}
```
虽然语句少了一行,但是可读性大打折扣,而且会造成误读,让别人误解这行代码的意思是下面这样。
```javascript
if (a === b){
// ...
}
```
建议不要将不同目的的语句,合并成一行。
## 自增和自减运算符
自增(`++`)和自减(`--`)运算符,放在变量的前面或后面,返回的值不一样,很容易发生错误。事实上,所有的`++`运算符都可以用`+= 1`代替。
```javascript
++x
// 等同于
x += 1;
```
改用`+= 1`,代码变得更清晰了。有一个很可笑的例子,某个JavaScript函数库的源代码中出现了下面的片段:
```javascript
++x;
++x;
```
这个程序员忘了,还有更简单、更合理的写法。
```javascript
x += 2;
```
建议自增(`++`)和自减(`--`)运算符尽量使用`+=`和`-=`代替。
## switch...case结构
`switch...case`结构要求,在每一个`case`的最后一行必须是`break`语句,否则会接着运行下一个`case`。这样不仅容易忘记,还会造成代码的冗长。
而且,`switch...case`不使用大括号,不利于代码形式的统一。此外,这种结构类似于`goto`语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则。
```javascript
function doAction(action) {
switch (action) {
case 'hack':
return 'hack';
break;
case 'slash':
return 'slash';
break;
case 'run':
return 'run';
break;
default:
throw new Error('Invalid action.');
}
}
```
上面的代码建议改写成对象结构。
```javascript
function doAction(action) {
var actions = {
'hack': function () {
return 'hack';
},
'slash': function () {
return 'slash';
},
'run': function () {
return 'run';
}
};
if (typeof actions[action] !== 'function') {
throw new Error('Invalid action.');
}
return actions[action]();
}
```
建议避免使用`switch...case`结构,用对象结构代替。