<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                <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代碼的注釋,所以&lt;!--和--&gt;也被視為單行注釋。 ```javascript x = 1; <!-- x = 2; --> x = 3; ``` 上面代碼中,只有`x = 1`會執行,其他的部分都被注釋掉了。 需要注意的是,--&gt;只有在行首,才會被當成單行注釋,否則就是一個運算符。 ```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):符號為&#710;,表示如果兩個二進制位中有且僅有一個為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&#710;=b, b&#710;=a, a&#710;=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個運算符,到底誰的優先級最高,實在不容易記住。 根據語言規格,這五個運算符的優先級從高到低依次為:小于等于(<=)、嚴格相等(===)、或(&#124;&#124;)、三元(?:)、等號(=)。因此上面的表達式,實際的運算順序如下。 ```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`結構,用對象結構代替。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看