<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`結構,用對象結構代替。