国产AV88|国产乱妇无码在线观看|国产影院精品在线观看十分钟福利|免费看橹橹网站

javascript-gaojichengx有目錄u

發(fā)布時間:2024-12-25 | 雜志分類:其他
免費制作
更多內(nèi)容

javascript-gaojichengx有目錄u

3.4 數(shù)據(jù)類型 33 1 2 3 45 13 67 8 9 1011 12也必須以雙引號結尾,而以單引號開頭的字符串必須以單引號結尾。例如,下面這種字符串表示法會導致語法錯誤:var firstName = 'Nicholas\"; // 語法錯誤(左右引號必須匹配)1. 字符字面量String 數(shù)據(jù)類型包含一些特殊的字符字面量,也叫轉(zhuǎn)義序列,用于表示非打印字符,或者具有其他用途的字符。這些字符字面量如下表所示:字 面 量 含 義\ 換行\\t 制表\\b 空格\ 回車\\f 進紙\\\\ 斜杠\\' 單引號('),在用單引號表示的字符串中使用。例如:'He said, \\'hey.\\''\\\" 雙引號(\"),在用雙引號表示的字符串中使用。例如:\"He said, \\ ... [收起]
[展開]
javascript-gaojichengx有目錄u
粉絲: {{bookData.followerCount}}
文本內(nèi)容
第51頁

3.4 數(shù)據(jù)類型 33

1

2

3

4

5

13

6

7

8

9

10

11

12

也必須以雙引號結尾,而以單引號開頭的字符串必須以單引號結尾。例如,下面這種字符串表示法會導

致語法錯誤:

var firstName = 'Nicholas\"; // 語法錯誤(左右引號必須匹配)

1. 字符字面量

String 數(shù)據(jù)類型包含一些特殊的字符字面量,也叫轉(zhuǎn)義序列,用于表示非打印字符,或者具有其

他用途的字符。這些字符字面量如下表所示:

字 面 量 含 義

\

換行

\\t 制表

\\b 空格

\

回車

\\f 進紙

\\\\ 斜杠

\\' 單引號('),在用單引號表示的字符串中使用。例如:'He said, \\'hey.\\''

\\\" 雙引號(\"),在用雙引號表示的字符串中使用。例如:\"He said, \\\"hey.\\\"\"

\\xnn 以十六進制代碼nn表示的一個字符(其中n為0~F)。例如,\\x41表示\"A\"

\\unnnn 以十六進制代碼nnnn表示的一個Unicode字符(其中n為0~F)。例如,\Σ表示希臘字符Σ

這些字符字面量可以出現(xiàn)在字符串中的任意位置,而且也將被作為一個字符來解析,如下面的例子

所示:

var text = \"This is the letter sigma: \Σ.\";

這個例子中的變量 text 有 28 個字符,其中 6 個字符長的轉(zhuǎn)義序列表示 1 個字符。

任何字符串的長度都可以通過訪問其 length 屬性取得,例如:

alert(text.length); // 輸出 28

這個屬性返回的字符數(shù)包括 16 位字符的數(shù)目。如果字符串中包含雙字節(jié)字符,那么 length 屬性

可能不會精確地返回字符串中的字符數(shù)目。

2. 字符串的特點

ECMAScript 中的字符串是不可變的,也就是說,字符串一旦創(chuàng)建,它們的值就不能改變。要改變

某個變量保存的字符串,首先要銷毀原來的字符串,然后再用另一個包含新值的字符串填充該變量,

例如:

var lang = \"Java\";

lang = lang + \"Script\";

以上示例中的變量 lang 開始時包含字符串\"Java\"。而第二行代碼把 lang 的值重新定義為\"Java\"

與\"Script\"的組合,即\"JavaScript\"。實現(xiàn)這個操作的過程如下:首先創(chuàng)建一個能容納 10 個字符的

新字符串,然后在這個字符串中填充\"Java\"和\"Script\",最后一步是銷毀原來的字符串\"Java\"和字

符串\"Script\",因為這兩個字符串已經(jīng)沒用了。這個過程是在后臺發(fā)生的,而這也是在某些舊版本的

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第52頁

34 第 3 章 基本概念

瀏覽器(例如版本低于 1.0 的 Firefox、IE6 等)中拼接字符串時速度很慢的原因所在。但這些瀏覽器后

來的版本已經(jīng)解決了這個低效率問題。

3. 轉(zhuǎn)換為字符串

要把一個值轉(zhuǎn)換為一個字符串有兩種方式。第一種是使用幾乎每個值都有的 toString()方法(第

5 章將討論這個方法的特點)。這個方法唯一要做的就是返回相應值的字符串表現(xiàn)。來看下面的例子:

var age = 11;

var ageAsString = age.toString(); // 字符串\"11\"

var found = true;

var foundAsString = found.toString(); // 字符串\"true\"

StringExample01.htm

數(shù)值、布爾值、對象和字符串值(沒錯,每個字符串也都有一個 toString()方法,該方法返回字

符串的一個副本)都有 toString()方法。但 null 和 undefined 值沒有這個方法。

多數(shù)情況下,調(diào)用 toString()方法不必傳遞參數(shù)。但是,在調(diào)用數(shù)值的 toString()方法時,可

以傳遞一個參數(shù):輸出數(shù)值的基數(shù)。默認情況下,toString()方法以十進制格式返回數(shù)值的字符串表

示。而通過傳遞基數(shù),toString()可以輸出以二進制、八進制、十六進制,乃至其他任意有效進制格

式表示的字符串值。下面給出幾個例子:

var num = 10;

alert(num.toString()); // \"10\"

alert(num.toString(2)); // \"1010\"

alert(num.toString(8)); // \"12\"

alert(num.toString(10)); // \"10\"

alert(num.toString(16)); // \"a\"

StringExample02.htm

通過這個例子可以看出,通過指定基數(shù),toString()方法會改變輸出的值。而數(shù)值 10 根據(jù)基數(shù)的

不同,可以在輸出時被轉(zhuǎn)換為不同的數(shù)值格式。注意,默認的(沒有參數(shù)的)輸出值與指定基數(shù) 10 時

的輸出值相同。

在不知道要轉(zhuǎn)換的值是不是 null 或 undefined 的情況下,還可以使用轉(zhuǎn)型函數(shù) String(),這個

函數(shù)能夠?qū)⑷魏晤愋偷闹缔D(zhuǎn)換為字符串。String()函數(shù)遵循下列轉(zhuǎn)換規(guī)則:

? 如果值有 toString()方法,則調(diào)用該方法(沒有參數(shù))并返回相應的結果;

? 如果值是 null,則返回\"null\";

? 如果值是 undefined,則返回\"undefined\"。

下面再看幾個例子:

var value1 = 10;

var value2 = true;

var value3 = null;

var value4;

alert(String(value1)); // \"10\"

alert(String(value2)); // \"true\"

alert(String(value3)); // \"null\"

alert(String(value4)); // \"undefined\"

StringExample03.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第53頁

3.4 數(shù)據(jù)類型 35

1

2

3

4

5

13

6

7

8

9

10

11

12

這里先后轉(zhuǎn)換了 4 個值:數(shù)值、布爾值、null 和 undefined。數(shù)值和布爾值的轉(zhuǎn)換結果與調(diào)用

toString()方法得到的結果相同。因為 null 和 undefined 沒有 toString()方法,所以 String()

函數(shù)就返回了這兩個值的字面量。

要把某個值轉(zhuǎn)換為字符串,可以使用加號操作符(3.5 節(jié)討論)把它與一個字符

串(\"\")加在一起。

3.4.7 Object類型

ECMAScript 中的對象其實就是一組數(shù)據(jù)和功能的集合。對象可以通過執(zhí)行 new 操作符后跟要創(chuàng)建

的對象類型的名稱來創(chuàng)建。而創(chuàng)建 Object 類型的實例并為其添加屬性和(或)方法,就可以創(chuàng)建自定

義對象,如下所示:

var o = new Object();

這個語法與 Java 中創(chuàng)建對象的語法相似;但在 ECMAScript 中,如果不給構造函數(shù)傳遞參數(shù),則可

以省略后面的那一對圓括號。也就是說,在像前面這個示例一樣不傳遞參數(shù)的情況下,完全可以省略那

對圓括號(但這不是推薦的做法):

var o = new Object; // 有效,但不推薦省略圓括號

僅僅創(chuàng)建 Object 的實例并沒有什么用處,但關鍵是要理解一個重要的思想:即在 ECMAScript 中,

(就像 Java 中的 java.lang.Object 對象一樣)Object 類型是所有它的實例的基礎。換句話說,

Object 類型所具有的任何屬性和方法也同樣存在于更具體的對象中。

Object 的每個實例都具有下列屬性和方法。

? constructor:保存著用于創(chuàng)建當前對象的函數(shù)。對于前面的例子而言,構造函數(shù)(constructor)

就是 Object()。

? hasOwnProperty(propertyName):用于檢查給定的屬性在當前對象實例中(而不是在實例

的原型中)是否存在。其中,作為參數(shù)的屬性名(propertyName)必須以字符串形式指定(例

如:o.hasOwnProperty(\"name\"))。

? isPrototypeOf(object):用于檢查傳入的對象是否是傳入對象的原型(第 5 章將討論原

型)。

? propertyIsEnumerable(propertyName):用于檢查給定的屬性是否能夠使用 for-in 語句

(本章后面將會討論)來枚舉。與 hasOwnProperty()方法一樣,作為參數(shù)的屬性名必須以字符

串形式指定。

? toLocaleString():返回對象的字符串表示,該字符串與執(zhí)行環(huán)境的地區(qū)對應。

? toString():返回對象的字符串表示。

? valueOf():返回對象的字符串、數(shù)值或布爾值表示。通常與 toString()方法的返回值

相同。

由于在 ECMAScript 中 Object 是所有對象的基礎,因此所有對象都具有這些基本的屬性和方法。

第 5 章和第 6 章將詳細介紹 Object 與其他對象的關系。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第54頁

36 第 3 章 基本概念

從技術角度講,ECMA-262 中對象的行為不一定適用于 JavaScript 中的其他對象。

瀏覽器環(huán)境中的對象,比如 BOM 和 DOM 中的對象,都屬于宿主對象,因為它們是

由宿主實現(xiàn)提供和定義的。ECMA-262 不負責定義宿主對象,因此宿主對象可能會也

可能不會繼承 Object。

3.5 操作符

ECMA-262 描述了一組用于操作數(shù)據(jù)值的操作符,包括算術操作符(如加號和減號)、位操作符、

關系操作符和相等操作符。ECMAScript 操作符的與眾不同之處在于,它們能夠適用于很多值,例如字

符串、數(shù)字值、布爾值,甚至對象。不過,在應用于對象時,相應的操作符通常都會調(diào)用對象的 valueOf()

和(或)toString()方法,以便取得可以操作的值。

3.5.1 一元操作符

只能操作一個值的操作符叫做一元操作符。一元操作符是 ECMAScript 中最簡單的操作符。

1. 遞增和遞減操作符

遞增和遞減操作符直接借鑒自 C,而且各有兩個版本:前置型和后置型。顧名思義,前置型應該位

于要操作的變量之前,而后置型則應該位于要操作的變量之后。因此,在使用前置遞增操作符給一個數(shù)

值加 1 時,要把兩個加號(++)放在這個數(shù)值變量前面,如下所示:

var age = 29;

++age;

在這個例子中,前置遞增操作符把 age 的值變成了 30(為 29 加上了 1)。實際上,執(zhí)行這個前置遞

增操作與執(zhí)行以下操作的效果相同:

var age = 29;

age = age + 1;

執(zhí)行前置遞減操作的方法也類似,結果會從一個數(shù)值中減去 1。使用前置遞減操作符時,要把兩個

減號(--)放在相應變量的前面,如下所示:

var age = 29;

--age;

這樣,age 變量的值就減少為 28(從 29 中減去了 1)。

執(zhí)行前置遞增和遞減操作時,變量的值都是在語句被求值以前改變的。(在計算機科學領域,這種

情況通常被稱作副效應。)請看下面這個例子。

var age = 29;

var anotherAge = --age + 2;

alert(age); // 輸出 28

alert(anotherAge); // 輸出 30

IncrementDecrementExample01.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第55頁

3.5 操作符 37

1

2

3

4

5

13

6

7

8

9

10

11

12

這個例子中變量 anotherAge 的初始值等于變量 age 的值前置遞減之后加 2。由于先執(zhí)行了減法操

作,age 的值變成了 28,所以再加上 2 的結果就是 30。

由于前置遞增和遞減操作與執(zhí)行語句的優(yōu)先級相等,因此整個語句會從左至右被求值。再看一個例子:

var num1 = 2;

var num2 = 20;

var num3 = --num1 + num2; // 等于 21

var num4 = num1 + num2; // 等于 21

IncrementDecrementExample02.htm

在這里,num3 之所以等于 21 是因為 num1 先減去了 1 才與 num2 相加。而變量 num4 也等于 21 是

因為相應的加法操作使用了 num1 減去 1 之后的值。

后置型遞增和遞減操作符的語法不變(仍然分別是++和--),只不過要放在變量的后面而不是前面。

后置遞增和遞減與前置遞增和遞減有一個非常重要的區(qū)別,即遞增和遞減操作是在包含它們的語句被求

值之后才執(zhí)行的。這個區(qū)別在某些情況下不是什么問題,例如:

var age = 29;

age++;

把遞增操作符放在變量后面并不會改變語句的結果,因為遞增是這條語句的唯一操作。但是,當語

句中還包含其他操作時,上述區(qū)別就會非常明顯了。請看下面的例子:

var num1 = 2;

var num2 = 20;

var num3 = num1-- + num2; // 等于 22

var num4 = num1 + num2; // 等于 21

IncrementDecrementExample03.htm

這里僅僅將前置遞減改成了后置遞減,就立即可以看到差別。在前面使用前置遞減的例子中,num3

和 num4 最后都等于 21。而在這個例子中,num3 等于 22,num4 等于 21。差別的根源在于,這里在計

算 num3 時使用了 num1 的原始值(2)完成了加法計算,而 num4 則使用了遞減后的值(1)。

所有這 4 個操作符對任何值都適用,也就是它們不僅適用于整數(shù),還可以用于字符串、布爾值、浮

點數(shù)值和對象。在應用于不同的值時,遞增和遞減操作符遵循下列規(guī)則。

? 在應用于一個包含有效數(shù)字字符的字符串時,先將其轉(zhuǎn)換為數(shù)字值,再執(zhí)行加減 1 的操作。字

符串變量變成數(shù)值變量。

? 在應用于一個不包含有效數(shù)字字符的字符串時,將變量的值設置為 NaN(第 4 章將詳細討論)。

字符串變量變成數(shù)值變量。

? 在應用于布爾值 false 時,先將其轉(zhuǎn)換為 0 再執(zhí)行加減 1 的操作。布爾值變量變成數(shù)值變量。

? 在應用于布爾值 true 時,先將其轉(zhuǎn)換為 1 再執(zhí)行加減 1 的操作。布爾值變量變成數(shù)值變量。

? 在應用于浮點數(shù)值時,執(zhí)行加減 1 的操作。

? 在應用于對象時,先調(diào)用對象的 valueOf()方法(第 5 章將詳細討論)以取得一個可供操作的

值。然后對該值應用前述規(guī)則。如果結果是 NaN,則在調(diào)用 toString()方法后再應用前述規(guī)

則。對象變量變成數(shù)值變量。

以下示例展示了上面的一些規(guī)則:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第56頁

38 第 3 章 基本概念

var s1 = \"2\";

var s2 = \"z\";

var b = false;

var f = 1.1;

var o = {

valueOf: function() {

return -1;

}

};

s1++; // 值變成數(shù)值 3

s2++; // 值變成 NaN

b++; // 值變成數(shù)值 1

f--; // 值變成 0.10000000000000009(由于浮點舍入錯誤所致)

o--; // 值變成數(shù)值-2

IncrementDecrementExample04.htm

2. 一元加和減操作符

絕大多數(shù)開發(fā)人員對一元加和減操作符都不會陌生,而且這兩個 ECMAScript 操作符的作用與數(shù)學

書上講的完全一樣。一元加操作符以一個加號(+)表示,放在數(shù)值前面,對數(shù)值不會產(chǎn)生任何影響,

如下面的例子所示:

var num = 25;

num = +num; // 仍然是 25

不過,在對非數(shù)值應用一元加操作符時,該操作符會像 Number()轉(zhuǎn)型函數(shù)一樣對這個值執(zhí)行轉(zhuǎn)換。

換句話說,布爾值 false 和 true 將被轉(zhuǎn)換為 0 和 1,字符串值會被按照一組特殊的規(guī)則進行解析,而

對象是先調(diào)用它們的 valueOf()和(或)toString()方法,再轉(zhuǎn)換得到的值。

下面的例子展示了對不同數(shù)據(jù)類型應用一元加操作符的結果:

var s1 = \"01\";

var s2 = \"1.1\";

var s3 = \"z\";

var b = false;

var f = 1.1;

var o = {

valueOf: function() {

return -1;

}

};

s1 = +s1; // 值變成數(shù)值 1

s2 = +s2; // 值變成數(shù)值 1.1

s3 = +s3; // 值變成 NaN

b = +b; // 值變成數(shù)值 0

f = +f; // 值未變,仍然是 1.1

o = +o; // 值變成數(shù)值-1

UnaryPlusMinusExample01.htm

一元減操作符主要用于表示負數(shù),例如將 1 轉(zhuǎn)換成?1。下面的例子演示了這個簡單的轉(zhuǎn)換過程:

var num = 25;

num = -num; // 變成了-25

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第57頁

3.5 操作符 39

1

2

3

4

5

13

6

7

8

9

10

11

12

在將一元減操作符應用于數(shù)值時,該值會變成負數(shù)(如上面的例子所示)。而當應用于非數(shù)值時,

一元減操作符遵循與一元加操作符相同的規(guī)則,最后再將得到的數(shù)值轉(zhuǎn)換為負數(shù),如下面的例子所示:

var s1 = \"01\";

var s2 = \"1.1\";

var s3 = \"z\";

var b = false;

var f = 1.1;

var o = {

valueOf: function() {

return -1;

}

};

s1 = -s1; // 值變成了數(shù)值-1

s2 = -s2; // 值變成了數(shù)值-1.1

s3 = -s3; // 值變成了 NaN

b = -b; // 值變成了數(shù)值 0

f = -f; // 變成了-1.1

o = -o; // 值變成了數(shù)值 1

UnaryPlusMinusExample02.htm

一元加和減操作符主要用于基本的算術運算,也可以像前面示例所展示的一樣用于轉(zhuǎn)換數(shù)據(jù)類型。

3.5.2 位操作符

位操作符用于在最基本的層次上,即按內(nèi)存中表示數(shù)值的位來操作數(shù)值。ECMAScript 中的所有數(shù)

值都以 IEEE-754 64 位格式存儲,但位操作符并不直接操作 64 位的值。而是先將 64 位的值轉(zhuǎn)換成 32 位

的整數(shù),然后執(zhí)行操作,最后再將結果轉(zhuǎn)換回 64 位。對于開發(fā)人員來說,由于 64 位存儲格式是透明的,

因此整個過程就像是只存在 32 位的整數(shù)一樣。

對于有符號的整數(shù),32 位中的前 31 位用于表示整數(shù)的值。第 32 位用于表示數(shù)值的符號:0 表示正

數(shù),1 表示負數(shù)。這個表示符號的位叫做符號位,符號位的值決定了其他位數(shù)值的格式。其中,正數(shù)以

純二進制格式存儲,31 位中的每一位都表示 2 的冪。第一位(叫做位 0)表示 20

,第二位表示 21

,以此

類推。沒有用到的位以 0 填充,即忽略不計。例如,數(shù)值 18 的二進制表示是

00000000000000000000000000010010,或者更簡潔的 10010。這是 5 個有效位,這 5 位本身就決定了實

際的值(如圖 3-1 所示)。

圖 3-1

負數(shù)同樣以二進制碼存儲,但使用的格式是二進制補碼。計算一個數(shù)值的二進制補碼,需要經(jīng)過下

列 3 個步驟:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第58頁

40 第 3 章 基本概念

(1) 求這個數(shù)值絕對值的二進制碼(例如,要求?18 的二進制補碼,先求 18 的二進制碼);

(2) 求二進制反碼,即將 0 替換為 1,將 1 替換為 0;

(3) 得到的二進制反碼加 1。

要根據(jù)這 3 個步驟求得?18 的二進制碼,首先就要求得 18 的二進制碼,即:

0000 0000 0000 0000 0000 0000 0001 0010

然后,求其二進制反碼,即 0 和 1 互換:

1111 1111 1111 1111 1111 1111 1110 1101

最后,二進制反碼加 1:

1111 1111 1111 1111 1111 1111 1110 1101

1

---------------------------------------

1111 1111 1111 1111 1111 1111 1110 1110

這樣,就求得了?18 的二進制表示,即 11111111111111111111111111101110。要注意的是,在處理有

符號整數(shù)時,是不能訪問位 31 的。

ECMAScript 會盡力向我們隱藏所有這些信息。換句話說,在以二進制字符串形式輸出一個負數(shù)時,

我們看到的只是這個負數(shù)絕對值的二進制碼前面加上了一個負號。如下面的例子所示:

var num = -18;

alert(num.toString(2)); // \"-10010\"

要把數(shù)值?18 轉(zhuǎn)換成二進制字符串時,得到的結果是\"-10010\"。這說明轉(zhuǎn)換過程理解了二進制補

碼并將其以更合乎邏輯的形式展示了出來。

默認情況下,ECMAScript 中的所有整數(shù)都是有符號整數(shù)。不過,當然也存在無

符號整數(shù)。對于無符號整數(shù)來說,第 32 位不再表示符號,因為無符號整數(shù)只能是正

數(shù)。而且,無符號整數(shù)的值可以更大,因為多出的一位不再表示符號,可以用來表示

數(shù)值。

在 ECMAScript 中,當對數(shù)值應用位操作符時,后臺會發(fā)生如下轉(zhuǎn)換過程:64 位的數(shù)值被轉(zhuǎn)換成 32

位數(shù)值,然后執(zhí)行位操作,最后再將 32 位的結果轉(zhuǎn)換回 64 位數(shù)值。這樣,表面上看起來就好像是在操

作 32 位數(shù)值,就跟在其他語言中以類似方式執(zhí)行二進制操作一樣。但這個轉(zhuǎn)換過程也導致了一個嚴重

的副效應,即在對特殊的 NaN 和 Infinity 值應用位操作時,這兩個值都會被當成 0 來處理。

如果對非數(shù)值應用位操作符,會先使用 Number()函數(shù)將該值轉(zhuǎn)換為一個數(shù)值(自動完成),然后

再應用位操作。得到的結果將是一個數(shù)值。

1. 按位非(NOT)

按位非操作符由一個波浪線(~)表示,執(zhí)行按位非的結果就是返回數(shù)值的反碼。按位非是

ECMAScript 操作符中少數(shù)幾個與二進制計算有關的操作符之一。下面看一個例子:

var num1 = 25; // 二進制 00000000000000000000000000011001

var num2 = ~num1; // 二進制 11111111111111111111111111100110

alert(num2); // -26

BitwiseNotExample01.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第59頁

3.5 操作符 41

1

2

3

4

5

13

6

7

8

9

10

11

12

這里,對 25 執(zhí)行按位非操作,結果得到了?26。這也驗證了按位非操作的本質(zhì):操作數(shù)的負值減 1。

因此,下面的代碼也能得到相同的結果:

var num1 = 25;

var num2 = -num1 - 1;

alert(num2); // \"-26\"

雖然以上代碼也能返回同樣的結果,但由于按位非是在數(shù)值表示的最底層執(zhí)行操作,因此速度更快。

2. 按位與(AND)

按位與操作符由一個和號字符(&)表示,它有兩個操作符數(shù)。從本質(zhì)上講,按位與操作就是將兩

個數(shù)值的每一位對齊,然后根據(jù)下表中的規(guī)則,對相同位置上的兩個數(shù)執(zhí)行 AND 操作:

第一個數(shù)值的位 第二個數(shù)值的位 結 果

1 1 1

1 0 0

0 1 0

0 0 0

簡而言之,按位與操作只在兩個數(shù)值的對應位都是 1 時才返回 1,任何一位是 0,結果都是 0。

下面看一個對 25 和 3 執(zhí)行按位與操作的例子:

var result = 25 & 3;

alert(result); //1

BitwiseAndExample01.htm

可見,對 25 和 3 執(zhí)行按位與操作的結果是 1。為什么呢?請看其底層操作:

25 = 0000 0000 0000 0000 0000 0000 0001 1001

3 = 0000 0000 0000 0000 0000 0000 0000 0011

---------------------------------------------

AND = 0000 0000 0000 0000 0000 0000 0000 0001

原來,25 和 3 的二進制碼對應位上只有一位同時是 1,而其他位的結果自然都是 0,因此最終結果

等于 1。

3. 按位或(OR)

按位或操作符由一個豎線符號(|)表示,同樣也有兩個操作數(shù)。按位或操作遵循下面這個真值表。

第一個數(shù)值的位 第二個數(shù)值的位 結 果

1 1 1

1 0 1

0 1 1

0 0 0

由此可見,按位或操作在有一個位是 1 的情況下就返回 1,而只有在兩個位都是 0 的情況下才返回 0。

如果在前面按位與的例子中對 25 和 3 執(zhí)行按位或操作,則代碼如下所示:

var result = 25 | 3;

alert(result); //27

BitwiseOrExample01.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第60頁

42 第 3 章 基本概念

25 與 3 按位或的結果是 27:

25 = 0000 0000 0000 0000 0000 0000 0001 1001

3 = 0000 0000 0000 0000 0000 0000 0000 0011

--------------------------------------------

OR = 0000 0000 0000 0000 0000 0000 0001 1011

這兩個數(shù)值的都包含 4 個 1,因此可以把每個 1 直接放到結果中。二進制碼 11011 等于十進制值 27。

4. 按位異或(XOR)

按位異或操作符由一個插入符號(^)表示,也有兩個操作數(shù)。以下是按位異或的真值表。

第一個數(shù)值的位 第二個數(shù)值的位 結 果

1 1 0

1 0 1

0 1 1

0 0 0

按位異或與按位或的不同之處在于,這個操作在兩個數(shù)值對應位上只有一個 1 時才返回 1,如果對

應的兩位都是 1 或都是 0,則返回 0。

對 25 和 3 執(zhí)行按位異或操作的代碼如下所示:

var result = 25 ^ 3;

alert(result); //26

BitwiseXorExample01.htm

25 與 3 按位異或的結果是 26,其底層操作如下所示:

25 = 0000 0000 0000 0000 0000 0000 0001 1001

3 = 0000 0000 0000 0000 0000 0000 0000 0011

---------------------------------------------

XOR = 0000 0000 0000 0000 0000 0000 0001 1010

這兩個數(shù)值都包含 4 個 1,但第一位上則都是 1,因此結果的第一位變成了 0。而其他位上的 1 在另

一個數(shù)值中都沒有對應的 1,可以直接放到結果中。二進制碼 11010 等于十進制值 26(注意這個結果比

執(zhí)行按位或時小 1)。

5. 左移

左移操作符由兩個小于號(<<)表示,這個操作符會將數(shù)值的所有位向左移動指定的位數(shù)。例如,

如果將數(shù)值 2(二進制碼為 10)向左移動 5 位,結果就是 64(二進制碼為 1000000),代碼如下所示:

var oldValue = 2; // 等于二進制的 10

var newValue = oldValue << 5; // 等于二進制的 1000000,十進制的 64

LeftShiftExample01.htm

注意,在向左移位后,原數(shù)值的右側多出了 5 個空位。左移操作會以 0 來填充這些空位,以便得到

的結果是一個完整的 32 位二進制數(shù)(見圖 3-2)。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第61頁

3.5 操作符 43

1

2

3

4

5

13

6

7

8

9

10

11

12

圖 3-2

注意,左移不會影響操作數(shù)的符號位。換句話說,如果將?2 向左移動 5 位,結果將是?64,而非 64。

6. 有符號的右移

有符號的右移操作符由兩個大于號(>>)表示,這個操作符會將數(shù)值向右移動,但保留符號位(即

正負號標記)。有符號的右移操作與左移操作恰好相反,即如果將 64 向右移動 5 位,結果將變回 2:

var oldValue = 64; // 等于二進制的 1000000

var newValue = oldValue >> 5; // 等于二進制的 10 ,即十進制的 2

SignedRightShiftExample01.htm

同樣,在移位過程中,原數(shù)值中也會出現(xiàn)空位。只不過這次的空位出現(xiàn)在原數(shù)值的左側、符號位的

右側(見圖 3-3)。而此時 ECMAScript 會用符號位的值來填充所有空位,以便得到一個完整的值。

圖 3-3

7. 無符號右移

無符號右移操作符由 3 個大于號(>>>)表示,這個操作符會將數(shù)值的所有 32 位都向右移動。對正

數(shù)來說,無符號右移的結果與有符號右移相同。仍以前面有符號右移的代碼為例,如果將 64 無符號右

移 5 位,結果仍然還是 2:

var oldValue = 64; // 等于二進制的 1000000

var newValue = oldValue >>> 5; // 等于二進制的 10 ,即十進制的 2

UnsignedRightShiftExample01.htm

但是對負數(shù)來說,情況就不一樣了。首先,無符號右移是以 0 來填充空位,而不是像有符號右移那

樣以符號位的值來填充空位。所以,對正數(shù)的無符號右移與有符號右移結果相同,但對負數(shù)的結果就不

隱藏的符號位 數(shù)值 2

將數(shù)值 2 向左移動 5 位(得到 64)

以 0 填充

隱藏的符號位 數(shù)值 64

將數(shù)值 64 向右移動 5 位(得到 2)

以 0 填充(符號位的值)

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第62頁

44 第 3 章 基本概念

一樣了。其次,無符號右移操作符會把負數(shù)的二進制碼當成正數(shù)的二進制碼。而且,由于負數(shù)以其絕對

值的二進制補碼形式表示,因此就會導致無符號右移后的結果非常之大,如下面的例子所示:

var oldValue = -64; // 等于二進制的 11111111111111111111111111000000

var newValue = oldValue >>> 5; // 等于十進制的 134217726

UnsignedRightShiftExample02.htm

這里,當對?64 執(zhí)行無符號右移 5 位的操作后,得到的結果是 134217726。之所以結果如此之大,

是因為?64 的二進制碼為 11111111111111111111111111000000,而且無符號右移操作會把這個二進制碼當

成正數(shù)的二進制碼,換算成十進制就是 4294967232。如果把這個值右移 5 位,結果就變成了

00000111111111111111111111111110,即十進制的 134217726。

3.5.3 布爾操作符

在一門編程語言中,布爾操作符的重要性堪比相等操作符。如果沒有測試兩個值關系的能力,那么

諸如 if...else 和循環(huán)之類的語句就不會有用武之地了。布爾操作符一共有 3 個:非(NOT)、與(AND)

和或(OR)。

1. 邏輯非

邏輯非操作符由一個嘆號(?。┍硎?,可以應用于 ECMAScript 中的任何值。無論這個值是什么數(shù)據(jù)

類型,這個操作符都會返回一個布爾值。邏輯非操作符首先會將它的操作數(shù)轉(zhuǎn)換為一個布爾值,然后再

對其求反。也就是說,邏輯非操作符遵循下列規(guī)則:

? 如果操作數(shù)是一個對象,返回 false;

? 如果操作數(shù)是一個空字符串,返回 true;

? 如果操作數(shù)是一個非空字符串,返回 false;

? 如果操作數(shù)是數(shù)值 0,返回 true;

? 如果操作數(shù)是任意非 0 數(shù)值(包括 Infinity),返回 false;

? 如果操作數(shù)是 null,返回 true;

? 如果操作數(shù)是 NaN,返回 true;

? 如果操作數(shù)是 undefined,返回 true。

下面幾個例子展示了應用上述規(guī)則的結果:

alert(!false); // true

alert(!\"blue\"); // false

alert(!0); // true

alert(!NaN); // true

alert(!\"\"); // true

alert(!12345); // false

LogicalNotExample01.htm

邏輯非操作符也可以用于將一個值轉(zhuǎn)換為與其對應的布爾值。而同時使用兩個邏輯非操作符,實際

上就會模擬 Boolean()轉(zhuǎn)型函數(shù)的行為。其中,第一個邏輯非操作會基于無論什么操作數(shù)返回一個布

爾值,而第二個邏輯非操作則對該布爾值求反,于是就得到了這個值真正對應的布爾值。當然,最終結

果與對這個值使用 Boolean()函數(shù)相同,如下面的例子所示:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第63頁

3.5 操作符 45

1

2

3

4

5

13

6

7

8

9

10

11

12

alert(!!\"blue\"); //true

alert(!!0); //false

alert(!!NaN); //false

alert(!!\"\"); //false

alert(!!12345); //true

LogicalNotExample02.htm

2. 邏輯與

邏輯與操作符由兩個和號(&&)表示,有兩個操作數(shù),如下面的例子所示:

var result = true && false;

邏輯與的真值表如下:

第一個操作數(shù) 第二個操作數(shù) 結 果

true true true

true false false

false true false

false false false

邏輯與操作可以應用于任何類型的操作數(shù),而不僅僅是布爾值。在有一個操作數(shù)不是布爾值的情況

下,邏輯與操作就不一定返回布爾值;此時,它遵循下列規(guī)則:

? 如果第一個操作數(shù)是對象,則返回第二個操作數(shù);

? 如果第二個操作數(shù)是對象,則只有在第一個操作數(shù)的求值結果為 true 的情況下才會返回該

對象;

? 如果兩個操作數(shù)都是對象,則返回第二個操作數(shù);

? 如果有一個操作數(shù)是 null,則返回 null;

? 如果有一個操作數(shù)是 NaN,則返回 NaN;

? 如果有一個操作數(shù)是 undefined,則返回 undefined。

邏輯與操作屬于短路操作,即如果第一個操作數(shù)能夠決定結果,那么就不會再對第二個操作數(shù)求值。

對于邏輯與操作而言,如果第一個操作數(shù)是 false,則無論第二個操作數(shù)是什么值,結果都不再可能是

true 了。來看下面的例子:

var found = true;

var result = (found && someUndefinedVariable); // 這里會發(fā)生錯誤

alert(result); // 這一行不會執(zhí)行

LogicalAndExample01.htm

在上面的代碼中,當執(zhí)行邏輯與操作時會發(fā)生錯誤,因為變量 someUndefinedVariable 沒有聲

明。由于變量 found 的值是 true,所以邏輯與操作符會繼續(xù)對變量 someUndefinedVariable 求值。

但 someUndefinedVariable 尚未定義,因此就會導致錯誤。這說明不能在邏輯與操作中使用未定義

的值。如果像下面這個例中一樣,將 found 的值設置為 false,就不會發(fā)生錯誤了:

var found = false;

var result = (found && someUndefinedVariable); // 不會發(fā)生錯誤

alert(result); // 會執(zhí)行(\"false\")

LogicalAndExample02.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第64頁

46 第 3 章 基本概念

在這個例子中,警告框會顯示出來。無論變量 someUndefinedVariable 有沒有定義,也永遠不

會對它求值,因為第一個操作數(shù)的值是 false。而這也就意味著邏輯與操作的結果必定是 false,根本

用不著再對&&右側的操作數(shù)求值了。在使用邏輯與操作符時要始終銘記它是一個短路操作符。

3. 邏輯或

邏輯或操作符由兩個豎線符號(||)表示,有兩個操作數(shù),如下面的例子所示:

var result = true || false;

邏輯或的真值表如下:

第一個操作數(shù) 第二個操作數(shù) 結 果

True true true

True false true

false true true

false false false

與邏輯與操作相似,如果有一個操作數(shù)不是布爾值,邏輯或也不一定返回布爾值;此時,它遵循下

列規(guī)則:

? 如果第一個操作數(shù)是對象,則返回第一個操作數(shù);

? 如果第一個操作數(shù)的求值結果為 false,則返回第二個操作數(shù);

? 如果兩個操作數(shù)都是對象,則返回第一個操作數(shù);

? 如果兩個操作數(shù)都是 null,則返回 null;

? 如果兩個操作數(shù)都是 NaN,則返回 NaN;

? 如果兩個操作數(shù)都是 undefined,則返回 undefined。

與邏輯與操作符相似,邏輯或操作符也是短路操作符。也就是說,如果第一個操作數(shù)的求值結果為

true,就不會對第二個操作數(shù)求值了。下面看一個例子:

var found = true;

var result = (found || someUndefinedVariable); // 不會發(fā)生錯誤

alert(result); // 會執(zhí)行(\"true\")

LogicalOrExample01.htm

這個例子跟前面的例子一樣,變量 someUndefinedVariable 也沒有定義。但是,由于變量 found

的值是 true,而變量 someUndefinedVariable 永遠不會被求值,因此結果就會輸出\"true\"。如果

像下面這個例子一樣,把 found 的值改為 false,就會導致錯誤:

var found = false;

var result = (found || someUndefinedVariable); // 這里會發(fā)生錯誤

alert(result); // 這一行不會執(zhí)行

LogicalOrExample02.htm

我們可以利用邏輯或的這一行為來避免為變量賦 null 或 undefined 值。例如:

var myObject = preferredObject || backupObject;

在這個例子中,變量 myObject 將被賦予等號后面兩個值中的一個。變量 preferredObject 中包

含優(yōu)先賦給變量 myObject 的值,變量 backupObject 負責在 preferredObject 中不包含有效值的

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第65頁

3.5 操作符 47

1

2

3

4

5

13

6

7

8

9

10

11

12

情況下提供后備值。如果 preferredObject 的值不是 null,那么它的值將被賦給 myObject;如果

是 null,則將 backupObject 的值賦給 myObject。ECMAScript 程序的賦值語句經(jīng)常會使用這種模式,

本書也將采用這種模式。

3.5.4 乘性操作符

ECMAScript 定義了 3 個乘性操作符:乘法、除法和求模。這些操作符與 Java、C 或者 Perl 中的相

應操作符用途類似,只不過在操作數(shù)為非數(shù)值的情況下會執(zhí)行自動的類型轉(zhuǎn)換。如果參與乘性計算的某

個操作數(shù)不是數(shù)值,后臺會先使用 Number()轉(zhuǎn)型函數(shù)將其轉(zhuǎn)換為數(shù)值。也就是說,空字符串將被當作

0,布爾值 true 將被當作 1。

1. 乘法

乘法操作符由一個星號(*)表示,用于計算兩個數(shù)值的乘積。其語法類似于 C,如下面的例子

所示:

var result = 34 * 56;

在處理特殊值的情況下,乘法操作符遵循下列特殊的規(guī)則:

? 如果操作數(shù)都是數(shù)值,執(zhí)行常規(guī)的乘法計算,即兩個正數(shù)或兩個負數(shù)相乘的結果還是正數(shù),而

如果只有一個操作數(shù)有符號,那么結果就是負數(shù)。如果乘積超過了 ECMAScript 數(shù)值的表示范圍,

則返回 Infinity 或-Infinity;

? 如果有一個操作數(shù)是 NaN,則結果是 NaN;

? 如果是 Infinity 與 0 相乘,則結果是 NaN;

? 如果是 Infinity 與非 0 數(shù)值相乘,則結果是 Infinity 或-Infinity,取決于有符號操作數(shù)

的符號;

? 如果是 Infinity 與 Infinity 相乘,則結果是 Infinity;

? 如果有一個操作數(shù)不是數(shù)值,則在后臺調(diào)用 Number()將其轉(zhuǎn)換為數(shù)值,然后再應用上面的

規(guī)則。

2. 除法

除法操作符由一個斜線符號(/)表示,執(zhí)行第二個操作數(shù)除第一個操作數(shù)的計算,如下面的例子

所示:

var result = 66 / 11;

與乘法操作符類似,除法操作符對特殊的值也有特殊的處理規(guī)則。這些規(guī)則如下:

? 如果操作數(shù)都是數(shù)值,執(zhí)行常規(guī)的除法計算,即兩個正數(shù)或兩個負數(shù)相除的結果還是正數(shù),而

如果只有一個操作數(shù)有符號,那么結果就是負數(shù)。如果商超過了 ECMAScript 數(shù)值的表示范圍,

則返回 Infinity 或-Infinity;

? 如果有一個操作數(shù)是 NaN,則結果是 NaN;

? 如果是 Infinity 被 Infinity 除,則結果是 NaN;

? 如果是零被零除,則結果是 NaN;

? 如果是非零的有限數(shù)被零除,則結果是 Infinity 或-Infinity,取決于有符號操作數(shù)的符號;

? 如果是 Infinity 被任何非零數(shù)值除,則結果是 Infinity 或-Infinity,取決于有符號操作

數(shù)的符號;

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第66頁

48 第 3 章 基本概念

? 如果有一個操作數(shù)不是數(shù)值,則在后臺調(diào)用 Number()將其轉(zhuǎn)換為數(shù)值,然后再應用上面的規(guī)則。

3. 求模

求模(余數(shù))操作符由一個百分號(%)表示,用法如下:

var result = 26 % 5; // 等于 1

與另外兩個乘性操作符類似,求模操作符會遵循下列特殊規(guī)則來處理特殊的值:

? 如果操作數(shù)都是數(shù)值,執(zhí)行常規(guī)的除法計算,返回除得的余數(shù);

? 如果被除數(shù)是無窮大值而除數(shù)是有限大的數(shù)值,則結果是 NaN;

? 如果被除數(shù)是有限大的數(shù)值而除數(shù)是零,則結果是 NaN;

? 如果是 Infinity 被 Infinity 除,則結果是 NaN;

? 如果被除數(shù)是有限大的數(shù)值而除數(shù)是無窮大的數(shù)值,則結果是被除數(shù);

? 如果被除數(shù)是零,則結果是零;

? 如果有一個操作數(shù)不是數(shù)值,則在后臺調(diào)用 Number()將其轉(zhuǎn)換為數(shù)值,然后再應用上面的規(guī)則。

3.5.5 加性操作符

加法和減法這兩個加性操作符應該說是編程語言中最簡單的算術操作符了。但是在 ECMAScript 中,

這兩個操作符卻都有一系列的特殊行為。與乘性操作符類似,加性操作符也會在后臺轉(zhuǎn)換不同的數(shù)據(jù)類

型。然而,對于加性操作符而言,相應的轉(zhuǎn)換規(guī)則還稍微有點復雜。

1. 加法

加法操作符(+)的用法如下所示:

var result = 1 + 2;

如果兩個操作符都是數(shù)值,執(zhí)行常規(guī)的加法計算,然后根據(jù)下列規(guī)則返回結果:

? 如果有一個操作數(shù)是 NaN,則結果是 NaN;

? 如果是 Infinity 加 Infinity,則結果是 Infinity;

? 如果是-Infinity 加-Infinity,則結果是-Infinity;

? 如果是 Infinity 加-Infinity,則結果是 NaN;

? 如果是+0 加+0,則結果是+0;

? 如果是?0 加?0,則結果是?0;

? 如果是+0 加?0,則結果是+0。

不過,如果有一個操作數(shù)是字符串,那么就要應用如下規(guī)則:

? 如果兩個操作數(shù)都是字符串,則將第二個操作數(shù)與第一個操作數(shù)拼接起來;

? 如果只有一個操作數(shù)是字符串,則將另一個操作數(shù)轉(zhuǎn)換為字符串,然后再將兩個字符串拼接

起來。

如果有一個操作數(shù)是對象、數(shù)值或布爾值,則調(diào)用它們的 toString()方法取得相應的字符串值,

然后再應用前面關于字符串的規(guī)則。對于 undefined 和 null,則分別調(diào)用 String()函數(shù)并取得字符

串\"undefined\"和\"null\"。

下面來舉幾個例子:

var result1 = 5 + 5; // 兩個數(shù)值相加

alert(result1); // 10

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第67頁

3.5 操作符 49

1

2

3

4

5

13

6

7

8

9

10

11

12

var result2 = 5 + \"5\"; // 一個數(shù)值和一個字符串相加

alert(result2); // \"55\"

AddExample01.htm

以上代碼演示了加法操作符在兩種模式下的差別。第一行代碼演示了正常的情況,即 5+5 等于 10

(數(shù)值)。但是,如果將一個操作數(shù)改為字符串\"5\",結果就變成了\"55\"(字符串值),因為第一個操作

數(shù)也被轉(zhuǎn)換成了\"5\"。

忽視加法操作中的數(shù)據(jù)類型是 ECMAScript 編程中最常見的一個錯誤。再來看一個例子:

var num1 = 5;

var num2 = 10;

var message = \"The sum of 5 and 10 is \" + num1 + num2;

alert(message); // \"The sum of 5 and 10 is 510\"

AddExample02.htm

在這個例子中,變量 message 的值是執(zhí)行兩個加法操作之后的結果。有人可能以為最后得到的字

符串是\"The sum of 5 and 10 is 15\",但實際的結果卻是\"The sum of 5 and 10 is 510\"。

之所以會這樣,是因為每個加法操作是獨立執(zhí)行的。第一個加法操作將一個字符串和一個數(shù)值(5)拼

接了起來,結果是一個字符串。而第二個加法操作又用這個字符串去加另一個數(shù)值(10),當然也會得

到一個字符串。如果想先對數(shù)值執(zhí)行算術計算,然后再將結果與字符串拼接起來,應該像下面這樣使用

圓括號:

var num1 = 5;

var num2 = 10;

var message = \"The sum of 5 and 10 is \" + (num1 + num2);

alert(message); //\"The sum of 5 and 10 is 15\"

AddExample03.htm

在這個例子中,一對圓括號把兩個數(shù)值變量括在了一起,這樣就會告訴解析器先計算其結果,然后

再將結果與字符串拼接起來。因此,就得到了結果\"The sum of 5 and 10 is 15\"。

2. 減法

減法操作符(?)是另一個極為常用的操作符,其用法如下所示:

var result = 2 - 1;

與加法操作符類似,ECMAScript 中的減法操作符在處理各種數(shù)據(jù)類型轉(zhuǎn)換時,同樣需要遵循一些

特殊規(guī)則,如下所示:

? 如果兩個操作符都是數(shù)值,則執(zhí)行常規(guī)的算術減法操作并返回結果;

? 如果有一個操作數(shù)是 NaN,則結果是 NaN;

? 如果是 Infinity 減 Infinity,則結果是 NaN;

? 如果是-Infinity 減-Infinity,則結果是 NaN;

? 如果是 Infinity 減-Infinity,則結果是 Infinity;

? 如果是-Infinity 減 Infinity,則結果是-Infinity;

? 如果是+0 減+0,則結果是+0;

? 如果是+0 減?0,則結果是?0;

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第68頁

50 第 3 章 基本概念

? 如果是?0 減?0,則結果是+0;

? 如果有一個操作數(shù)是字符串、布爾值、null 或 undefined,則先在后臺調(diào)用 Number()函數(shù)將

其轉(zhuǎn)換為數(shù)值,然后再根據(jù)前面的規(guī)則執(zhí)行減法計算。如果轉(zhuǎn)換的結果是 NaN,則減法的結果

就是 NaN;

? 如果有一個操作數(shù)是對象,則調(diào)用對象的 valueOf()方法以取得表示該對象的數(shù)值。如果得到

的值是 NaN,則減法的結果就是 NaN。如果對象沒有 valueOf()方法,則調(diào)用其 toString()

方法并將得到的字符串轉(zhuǎn)換為數(shù)值。

下面幾個例子展示了上面的規(guī)則:

var result1 = 5 - true; // 4,因為 true 被轉(zhuǎn)換成了 1

var result2 = NaN - 1; // NaN

var result3 = 5 - 3; // 2

var result4 = 5 - \"\"; // 5,因為\"\" 被轉(zhuǎn)換成了 0

var result5 = 5 - \"2\"; // 3,因為\"2\"被轉(zhuǎn)換成了 2

var result6 = 5 - null; // 5,因為 null 被轉(zhuǎn)換成了 0

SubtractExample01.htm

3.5.6 關系操作符

小于(<)、大于(>)、小于等于(<=)和大于等于(>=)這幾個關系操作符用于對兩個值進行比

較,比較的規(guī)則與我們在數(shù)學課上所學的一樣。這幾個操作符都返回一個布爾值,如下面的例子所示:

var result1 = 5 > 3; //true

var result2 = 5 < 3; //false

RelationalOperatorsExample01.htm 中包含本節(jié)所有的代碼片段

與 ECMAScript 中的其他操作符一樣,當關系操作符的操作數(shù)使用了非數(shù)值時,也要進行數(shù)據(jù)轉(zhuǎn)換

或完成某些奇怪的操作。以下就是相應的規(guī)則。

? 如果兩個操作數(shù)都是數(shù)值,則執(zhí)行數(shù)值比較。

? 如果兩個操作數(shù)都是字符串,則比較兩個字符串對應的字符編碼值。

? 如果一個操作數(shù)是數(shù)值,則將另一個操作數(shù)轉(zhuǎn)換為一個數(shù)值,然后執(zhí)行數(shù)值比較。

? 如果一個操作數(shù)是對象,則調(diào)用這個對象的 valueOf()方法,用得到的結果按照前面的規(guī)則執(zhí)

行比較。如果對象沒有 valueOf()方法,則調(diào)用 toString()方法,并用得到的結果根據(jù)前面

的規(guī)則執(zhí)行比較。

? 如果一個操作數(shù)是布爾值,則先將其轉(zhuǎn)換為數(shù)值,然后再執(zhí)行比較。

在使用關系操作符比較兩個字符串時,會執(zhí)行一種奇怪的操作。很多人都會認為,在比較字符串值

時,小于的意思是“在字母表中的位置靠前”,而大于則意味著“在字母表中的位置靠后”,但實際上完

全不是那么回事。在比較字符串時,實際比較的是兩個字符串中對應位置的每個字符的字符編碼值。經(jīng)

過這么一番比較之后,再返回一個布爾值。由于大寫字母的字符編碼全部小于小寫字母的字符編碼,因

此我們就會看到如下所示的奇怪現(xiàn)象:

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

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第69頁

3.5 操作符 51

1

2

3

4

5

13

6

7

8

9

10

11

12

在這個例子中,字符串\"Brick\"被認為小于字符串\"alphabet\"。原因是字母 B 的字符編碼為 66,

而字母 a 的字符編碼是 97。如果要真正按字母表順序比較字符串,就必須把兩個操作數(shù)轉(zhuǎn)換為相同的大

小寫形式(全部大寫或全部小寫),然后再執(zhí)行比較,如下所示:

var result = \"Brick\".toLowerCase() < \"alphabet\".toLowerCase(); //false

通過將兩個操作數(shù)都轉(zhuǎn)換為小寫形式,就可以得出\"alphabet\"按字母表順序排在\"Brick\"之前的

正確判斷了。

另一種奇怪的現(xiàn)象發(fā)生在比較兩個數(shù)字字符串的情況下,比如下面這個例子:

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

確實,當比較字符串\"23\"是否小于\"3\"時,結果居然是 true。這是因為兩個操作數(shù)都是字符串,

而字符串比較的是字符編碼(\"2\"的字符編碼是 50,而\"3\"的字符編碼是 51)。不過,如果像下面例子

中一樣,將一個操作數(shù)改為數(shù)值,比較的結果就正常了:

var result = \"23\" < 3; //false

此時,字符串\"23\"會被轉(zhuǎn)換成數(shù)值 23,然后再與 3 進行比較,因此就會得到合理的結果。在比較

數(shù)值和字符串時,字符串都會被轉(zhuǎn)換成數(shù)值,然后再以數(shù)值方式與另一個數(shù)值比較。當然,這個規(guī)則對

前面的例子是適用的。可是,如果那個字符串不能被轉(zhuǎn)換成一個合理的數(shù)值呢?比如:

var result = \"a\" < 3; // false,因為\"a\"被轉(zhuǎn)換成了 NaN

由于字母\"a\"不能轉(zhuǎn)換成合理的數(shù)值,因此就被轉(zhuǎn)換成了 NaN。根據(jù)規(guī)則,任何操作數(shù)與 NaN 進行

關系比較,結果都是 false。于是,就出現(xiàn)了下面這個有意思的現(xiàn)象:

var result1 = NaN < 3; //false

var result2 = NaN >= 3; //false

按照常理,如果一個值不小于另一個值,則一定是大于或等于那個值。然而,在與 NaN 進行比較時,

這兩個比較操作的結果都返回了 false。

3.5.7 相等操作符

確定兩個變量是否相等是編程中的一個非常重要的操作。在比較字符串、數(shù)值和布爾值的相等性時,

問題還比較簡單。但在涉及到對象的比較時,問題就變得復雜了。最早的 ECMAScript 中的相等和不等

操作符會在執(zhí)行比較之前,先將對象轉(zhuǎn)換成相似的類型。后來,有人提出了這種轉(zhuǎn)換到底是否合理的質(zhì)

疑。最后,ECMAScript 的解決方案就是提供兩組操作符:相等和不相等——先轉(zhuǎn)換再比較,全等和不

全等——僅比較而不轉(zhuǎn)換。

1. 相等和不相等

ECMAScript 中的相等操作符由兩個等于號(==)表示,如果兩個操作數(shù)相等,則返回 true。而不

相等操作符由嘆號后跟等于號(!=)表示,如果兩個操作數(shù)不相等,則返回 true。這兩個操作符都會

先轉(zhuǎn)換操作數(shù)(通常稱為強制轉(zhuǎn)型),然后再比較它們的相等性。

在轉(zhuǎn)換不同的數(shù)據(jù)類型時,相等和不相等操作符遵循下列基本規(guī)則:

? 如果有一個操作數(shù)是布爾值,則在比較相等性之前先將其轉(zhuǎn)換為數(shù)值——false 轉(zhuǎn)換為 0,而

true 轉(zhuǎn)換為 1;

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第70頁

52 第 3 章 基本概念

? 如果一個操作數(shù)是字符串,另一個操作數(shù)是數(shù)值,在比較相等性之前先將字符串轉(zhuǎn)換為數(shù)值;

? 如果一個操作數(shù)是對象,另一個操作數(shù)不是,則調(diào)用對象的 valueOf()方法,用得到的基本類

型值按照前面的規(guī)則進行比較;

這兩個操作符在進行比較時則要遵循下列規(guī)則。

? null 和 undefined 是相等的。

? 要比較相等性之前,不能將 null 和 undefined 轉(zhuǎn)換成其他任何值。

? 如果有一個操作數(shù)是 NaN,則相等操作符返回 false,而不相等操作符返回 true。重要提示:

即使兩個操作數(shù)都是 NaN,相等操作符也返回 false;因為按照規(guī)則,NaN 不等于 NaN。

? 如果兩個操作數(shù)都是對象,則比較它們是不是同一個對象。如果兩個操作數(shù)都指向同一個對象,

則相等操作符返回 true;否則,返回 false。

下表列出了一些特殊情況及比較結果:

表 達 式 值 表 達 式 值

null == undefined true true == 1 true

\"NaN\" == NaN false true == 2 false

5 == NaN false undefined == 0 false

NaN == NaN false null == 0 false

NaN != NaN true \"5\"==5 true

false == 0 true

2. 全等和不全等

除了在比較之前不轉(zhuǎn)換操作數(shù)之外,全等和不全等操作符與相等和不相等操作符沒有什么區(qū)別。全

等操作符由 3 個等于號(===)表示,它只在兩個操作數(shù)未經(jīng)轉(zhuǎn)換就相等的情況下返回 true,如下面的

例子所示:

var result1 = (\"55\" == 55); //true,因為轉(zhuǎn)換后相等

var result2 = (\"55\" === 55); //false,因為不同的數(shù)據(jù)類型不相等

EqualityOperatorsExample02.htm

在這個例子中,第一個比較使用的是相等操作符比較字符串\"55\"和數(shù)值 55,結果返回了 true。如

前所述,這是因為字符串\"55\"先被轉(zhuǎn)換成了數(shù)值 55,然后再與另一個數(shù)值 55 進行比較。第二個比較使

用了全等操作符以不轉(zhuǎn)換數(shù)值的方式比較同樣的字符串和值。在不轉(zhuǎn)換的情況下,字符串當然不等于數(shù)

值,因此結果就是 false。

不全等操作符由一個嘆號后跟兩個等于號(!==)表示,它在兩個操作數(shù)未經(jīng)轉(zhuǎn)換就不相等的情況

下返回 true。例如:

var result1 = (\"55\" != 55); //false,因為轉(zhuǎn)換后相等

var result2 = (\"55\" !== 55); //true,因為不同的數(shù)據(jù)類型不相等

EqualityOperatorsExample03.htm

在這個例子中,第一個比較使用了不相等操作符,而該操作符會將字符串\"55\"轉(zhuǎn)換成 55,結果就

與第二個操作數(shù)(也是 55)相等了。而由于這兩個操作數(shù)被認為相等,因此就返回了 false。第二個

比較使用了不全等操作符。假如我們這樣想:字符串 55 與數(shù)值 55 不相同嗎?,那么答案一定是:是的

(true)。

記住:null == undefined 會返回 true,因為它們是類似的值;但 null === undefined 會返

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第71頁

3.5 操作符 53

1

2

3

4

5

13

6

7

8

9

10

11

12

回 false,因為它們是不同類型的值。

由于相等和不相等操作符存在類型轉(zhuǎn)換問題,而為了保持代碼中數(shù)據(jù)類型的完整

性,我們推薦使用全等和不全等操作符。

3.5.8 條件操作符

條件操作符應該算是 ECMAScript 中最靈活的一種操作符了,而且它遵循與 Java 中的條件操作符相

同的語法形式,如下面的例子所示:

variable = boolean_expression ? true_value : false_value;

本質(zhì)上,這行代碼的含義就是基于對 boolean_expression 求值的結果,決定給變量 variable

賦什么值。如果求值結果為 true,則給變量 variable 賦 true_value 值;如果求值結果為 false,

則給變量 variable 賦 false_value 值。再看一個例子:

var max = (num1 > num2) ? num1 : num2;

在這個例子中,max 中將會保存一個最大的值。這個表達式的意思是:如果 num1 大于 num2(關

系表達式返回 true),則將 num1 的值賦給 max;如果 num1 小于或等于 num2(關系表達式返回 false),

則將 num2 的值賦給 max。

3.5.9 賦值操作符

簡單的賦值操作符由等于號(=)表示,其作用就是把右側的值賦給左側的變量,如下面的例子所示:

var num = 10;

如果在等于號(=)前面再添加乘性操作符、加性操作符或位操作符,就可以完成復合賦值操作。

這種復合賦值操作相當于是對下面常規(guī)表達式的簡寫形式:

var num = 10;

num = num + 10;

其中的第二行代碼可以用一個復合賦值來代替:

var num = 10;

num += 10;

每個主要算術操作符(以及個別的其他操作符)都有對應的復合賦值操作符。這些操作符如下所示:

? 乘/賦值(*=);

? 除/賦值(/=);

? 模/賦值(%=);

? 加/賦值(+=);

? 減/賦值(?=);

? 左移/賦值(<<=);

? 有符號右移/賦值(>>=);

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第72頁

54 第 3 章 基本概念

? 無符號右移/賦值(>>>=)。

設計這些操作符的主要目的就是簡化賦值操作。使用它們不會帶來任何性能的提升。

3.5.10 逗號操作符

使用逗號操作符可以在一條語句中執(zhí)行多個操作,如下面的例子所示:

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

逗號操作符多用于聲明多個變量;但除此之外,逗號操作符還可以用于賦值。在用于賦值時,逗號

操作符總會返回表達式中的最后一項,如下面的例子所示:

var num = (5, 1, 4, 8, 0); // num 的值為 0

由于 0 是表達式中的最后一項,因此 num 的值就是 0。雖然逗號的這種使用方式并不常見,但這個

例子可以幫我們理解逗號的這種行為。

3.6 語句

ECMA-262 規(guī)定了一組語句(也稱為流控制語句)。從本質(zhì)上看,語句定義了 ECMAScript 中的主要

語法,語句通常使用一或多個關鍵字來完成給定任務。語句可以很簡單,例如通知函數(shù)退出;也可以比

較復雜,例如指定重復執(zhí)行某個命令的次數(shù)。

3.6.1 if語句

大多數(shù)編程語言中最為常用的一個語句就是 if 語句。以下是 if 語句的語法:

if (condition) statement1 else statement2

其中的 condition(條件)可以是任意表達式;而且對這個表達式求值的結果不一定是布爾值。

ECMAScript 會自動調(diào)用 Boolean()轉(zhuǎn)換函數(shù)將這個表達式的結果轉(zhuǎn)換為一個布爾值。如果對 condition

求值的結果是 true,則執(zhí)行 statement1(語句 1),如果對 condition求值的結果是 false,則執(zhí)行 statement2

(語句 2)。而且這兩個語句既可以是一行代碼,也可以是一個代碼塊(以一對花括號括起來的多行代碼)。

請看下面的例子。

if (i > 25)

alert(\"Greater than 25.\"); // 單行語句

else {

alert(\"Less than or equal to 25.\"); // 代碼塊中的語句

}

IfStatementExample01.htm

不過,業(yè)界普遍推崇的最佳實踐是始終使用代碼塊,即使要執(zhí)行的只有一行代碼。因為這樣可以消

除人們的誤解,否則可能讓人分不清在不同條件下要執(zhí)行哪些語句。

另外,也可以像下面這樣把整個 if 語句寫在一行代碼中:

if (condition1) statement1 else if (condition2) statement2 else statement3

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第73頁

3.6 語句 55

1

2

3

4

5

13

6

7

8

9

10

11

12

但我們推薦的做法則是像下面這樣:

if (i > 25) {

alert(\"Greater than 25.\");

} else if (i < 0) {

alert(\"Less than 0.\");

} else {

alert(\"Between 0 and 25, inclusive.\");

}

IfStatementExample02.htm

3.6.2 do-while語句

do-while 語句是一種后測試循環(huán)語句,即只有在循環(huán)體中的代碼執(zhí)行之后,才會測試出口條件。

換句話說,在對條件表達式求值之前,循環(huán)體內(nèi)的代碼至少會被執(zhí)行一次。以下是 do-while 語句的

語法:

do {

statement

} while (expression);

下面是一個示例:

var i = 0;

do {

i += 2;

} while (i < 10);

alert(i);

DoWhileStatementExample01.htm

在這個例子中,只要變量 i 的值小于 10,循環(huán)就會一直繼續(xù)下去。而且變量 i 的值最初為 0,每次

循環(huán)都會遞增 2。

像 do-while 這種后測試循環(huán)語句最常用于循環(huán)體中的代碼至少要被執(zhí)行一次的

情形。

3.6.3 while語句

while 語句屬于前測試循環(huán)語句,也就是說,在循環(huán)體內(nèi)的代碼被執(zhí)行之前,就會對出口條件求值。

因此,循環(huán)體內(nèi)的代碼有可能永遠不會被執(zhí)行。以下是 while 語句的語法:

while(expression) statement

下面是一個示例:

var i = 0;

while (i < 10) {

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第74頁

56 第 3 章 基本概念

i += 2;

}

WhileStatementExample01.htm

在這個例子中,變量 i 開始時的值為 0,每次循環(huán)都會遞增 2。而只要 i 的值小于 10,循環(huán)就會繼

續(xù)下去。

3.6.4 for語句

for 語句也是一種前測試循環(huán)語句,但它具有在執(zhí)行循環(huán)之前初始化變量和定義循環(huán)后要執(zhí)行的代

碼的能力。以下是 for 語句的語法:

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

下面是一個示例:

var count = 10;

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

alert(i);

}

ForStatementExample01.htm

以上代碼定義了變量 i 的初始值為 0。只有當條件表達式(i<count)返回 true 的情況下才會進

入 for 循環(huán),因此也有可能不會執(zhí)行循環(huán)體中的代碼。如果執(zhí)行了循環(huán)體中的代碼,則一定會對循環(huán)后

的表達式(i++)求值,即遞增 i 的值。這個 for 循環(huán)語句與下面的 while 語句的功能相同:

var count = 10;

var i = 0;

while (i < count){

alert(i);

i++;

}

使用 while 循環(huán)做不到的,使用 for 循環(huán)同樣也做不到。也就是說,for 循環(huán)只是把與循環(huán)有關

的代碼集中在了一個位置。

有必要指出的是,在 for 循環(huán)的變量初始化表達式中,也可以不使用 var 關鍵字。該變量的初始

化可以在外部執(zhí)行,例如:

var count = 10;

var i;

for (i = 0; i < count; i++){

alert(i);

}

ForStatementExample02.htm

以上代碼與在循環(huán)初始化表達式中聲明變量的效果是一樣的。由于 ECMAScript 中不存在塊級作用

域(第 4 章將進一步討論這一點),因此在循環(huán)內(nèi)部定義的變量也可以在外部訪問到。例如:

var count = 10;

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

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第75頁

3.6 語句 57

1

2

3

4

5

13

6

7

8

9

10

11

12

alert(i);

}

alert(i); //10

ForStatementExample03.htm

在這個例子中,會有一個警告框顯示循環(huán)完成后變量 i 的值,這個值是 10。這是因為,即使 i 是

在循環(huán)內(nèi)部定義的一個變量,但在循環(huán)外部仍然可以訪問到它。

此外,for 語句中的初始化表達式、控制表達式和循環(huán)后表達式都是可選的。將這三個表達式全部

省略,就會創(chuàng)建一個無限循環(huán),例如:

for (;;) { // 無限循環(huán)

doSomething();

}

而只給出控制表達式實際上就把 for 循環(huán)轉(zhuǎn)換成了 while 循環(huán),例如:

var count = 10;

var i = 0;

for (; i < count; ){

alert(i);

i++;

}

ForStatementExample04.htm

由于 for 語句存在極大的靈活性,因此它也是 ECMAScript 中最常用的一個語句。

3.6.5 for-in語句

for-in 語句是一種精準的迭代語句,可以用來枚舉對象的屬性。以下是 for-in 語句的語法:

for (property in expression) statement

下面是一個示例:

for (var propName in window) {

document.write(propName);

}

ForInStatementExample01.htm

在這個例子中,我們使用 for-in 循環(huán)來顯示了 BOM 中 window 對象的所有屬性。每次執(zhí)行循環(huán)

時,都會將 window 對象中存在的一個屬性名賦值給變量 propName。這個過程會一直持續(xù)到對象中的

所有屬性都被枚舉一遍為止。與 for 語句類似,這里控制語句中的 var 操作符也不是必需的。但是,

為了保證使用局部變量,我們推薦上面例子中的這種做法。

ECMAScript 對象的屬性沒有順序。因此,通過 for-in 循環(huán)輸出的屬性名的順序是不可預測的。

具體來講,所有屬性都會被返回一次,但返回的先后次序可能會因瀏覽器而異。

但是,如果表示要迭代的對象的變量值為 null 或 undefined,for-in 語句會拋出錯誤。

ECMAScript 5 更正了這一行為;對這種情況不再拋出錯誤,而只是不執(zhí)行循環(huán)體。為了保證最大限度的

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第76頁

58 第 3 章 基本概念

兼容性,建議在使用 for-in 循環(huán)之前,先檢測確認該對象的值不是 null 或 undefined。

Safari 3 以前版本的 for-in 語句中存在一個 bug,該 bug 會導致某些屬性被返回

兩次。

3.6.6 label語句

使用 label 語句可以在代碼中添加標簽,以便將來使用。以下是 label 語句的語法:

label: statement

下面是一個示例:

start: for (var i=0; i < count; i++) {

alert(i);

}

這個例子中定義的 start 標簽可以在將來由 break 或 continue 語句引用。加標簽的語句一般都

要與 for 語句等循環(huán)語句配合使用。

3.6.7 break和continue語句

break 和 continue 語句用于在循環(huán)中精確地控制代碼的執(zhí)行。其中,break 語句會立即退出循環(huán),

強制繼續(xù)執(zhí)行循環(huán)后面的語句。而 continue 語句雖然也是立即退出循環(huán),但退出循環(huán)后會從循環(huán)的頂

部繼續(xù)執(zhí)行。請看下面的例子:

var num = 0;

for (var i=1; i < 10; i++) {

if (i % 5 == 0) {

break;

}

num++;

}

alert(num); //4

BreakStatementExample01.htm

這個例子中的 for 循環(huán)會將變量 i 由 1 遞增至 10。在循環(huán)體內(nèi),有一個 if 語句檢查 i 的值是否

可以被 5 整除(使用求模操作符)。如果是,則執(zhí)行 break 語句退出循環(huán)。另一方面,變量 num 從 0 開

始,用于記錄循環(huán)執(zhí)行的次數(shù)。在執(zhí)行 break 語句之后,要執(zhí)行的下一行代碼是 alert()函數(shù),結果

顯示 4。也就是說,在變量 i 等于 5 時,循環(huán)總共執(zhí)行了 4 次;而 break 語句的執(zhí)行,導致了循環(huán)在

num 再次遞增之前就退出了。如果在這里把 break 替換為 continue 的話,則可以看到另一種結果:

var num = 0;

for (var i=1; i < 10; i++) {

if (i % 5 == 0) {

continue;

}

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第77頁

3.6 語句 59

1

2

3

4

5

13

6

7

8

9

10

11

12

num++;

}

alert(num); //8

ContinueStatementExample01.htm

例子的結果顯示 8,也就是循環(huán)總共執(zhí)行了 8 次。當變量 i 等于 5 時,循環(huán)會在 num 再次遞增之前

退出,但接下來執(zhí)行的是下一次循環(huán),即 i 的值等于 6 的循環(huán)。于是,循環(huán)又繼續(xù)執(zhí)行,直到 i 等于

10 時自然結束。而 num 的最終值之所以是 8,是因為 continue 語句導致它少遞增了一次。

break 和 continue 語句都可以與 label 語句聯(lián)合使用,從而返回代碼中特定的位置。這種聯(lián)合

使用的情況多發(fā)生在循環(huán)嵌套的情況下,如下面的例子所示:

var num = 0;

outermost:

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

for (var j=0; j < 10; j++) {

if (i == 5 && j == 5) {

break outermost;

}

num++;

}

}

alert(num); //55

BreakStatementExample02.htm

在這個例子中,outermost 標簽表示外部的 for 語句。如果每個循環(huán)正常執(zhí)行 10 次,則 num++

語句就會正常執(zhí)行 100 次。換句話說,如果兩個循環(huán)都自然結束,num 的值應該是 100。但內(nèi)部循環(huán)中

的 break 語句帶了一個參數(shù):要返回到的標簽。添加這個標簽的結果將導致 break 語句不僅會退出內(nèi)

部的 for 語句(即使用變量 j 的循環(huán)),而且也會退出外部的 for 語句(即使用變量 i 的循環(huán))。為此,

當變量 i 和 j 都等于 5 時,num 的值正好是 55。同樣,continue 語句也可以像這樣與 label 語句聯(lián)

用,如下面的例子所示:

var num = 0;

outermost:

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

for (var j=0; j < 10; j++) {

if (i == 5 && j == 5) {

continue outermost;

}

num++;

}

}

alert(num); //95

ContinueStatementExample02.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第78頁

60 第 3 章 基本概念

在這種情況下,continue 語句會強制繼續(xù)執(zhí)行循環(huán)——退出內(nèi)部循環(huán),執(zhí)行外部循環(huán)。當 j 是 5

時,continue 語句執(zhí)行,而這也就意味著內(nèi)部循環(huán)少執(zhí)行了 5 次,因此 num 的結果是 95。

雖然聯(lián)用 break、continue 和 label 語句能夠執(zhí)行復雜的操作,但如果使用過度,也會給調(diào)試

帶來麻煩。在此,我們建議如果使用 label 語句,一定要使用描述性的標簽,同時不要嵌套過多的循環(huán)。

3.6.8 with語句

with 語句的作用是將代碼的作用域設置到一個特定的對象中。with 語句的語法如下:

with (expression) statement;

定義 with 語句的目的主要是為了簡化多次編寫同一個對象的工作,如下面的例子所示:

var qs = location.search.substring(1);

var hostName = location.hostname;

var url = location.href;

上面幾行代碼都包含 location 對象。如果使用 with 語句,可以把上面的代碼改寫成如下所示:

with(location){

var qs = search.substring(1);

var hostName = hostname;

var url = href;

}

WithStatementExample01.htm

在這個重寫后的例子中,使用 with 語句關聯(lián)了 location 對象。這意味著在 with 語句的代碼塊

內(nèi)部,每個變量首先被認為是一個局部變量,而如果在局部環(huán)境中找不到該變量的定義,就會查詢

location 對象中是否有同名的屬性。如果發(fā)現(xiàn)了同名屬性,則以 location 對象屬性的值作為變量的值。

嚴格模式下不允許使用 with 語句,否則將視為語法錯誤。

由于大量使用 with 語句會導致性能下降,同時也會給調(diào)試代碼造成困難,因此

在開發(fā)大型應用程序時,不建議使用 with 語句。

3.6.9 switch語句

switch 語句與 if 語句的關系最為密切,而且也是在其他語言中普遍使用的一種流控制語句。

ECMAScript 中 switch 語句的語法與其他基于 C 的語言非常接近,如下所示:

switch (expression) {

case value: statement

break;

case value: statement

break;

case value: statement

break;

case value: statement

break;

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第79頁

3.6 語句 61

1

2

3

4

5

13

6

7

8

9

10

11

12

default: statement

}

switch 語句中的每一種情形(case)的含義是:“如果表達式等于這個值(value),則執(zhí)行后面的

語句(statement)”。而 break 關鍵字會導致代碼執(zhí)行流跳出 switch 語句。如果省略 break 關鍵字,

就會導致執(zhí)行完當前 case 后,繼續(xù)執(zhí)行下一個 case。最后的 default 關鍵字則用于在表達式不匹配前

面任何一種情形的時候,執(zhí)行機動代碼(因此,也相當于一個 else 語句)。

從根本上講,switch 語句就是為了讓開發(fā)人員免于編寫像下面這樣的代碼:

if (i == 25){

alert(\"25\");

} else if (i == 35) {

alert(\"35\");

} else if (i == 45) {

alert(\"45\");

} else {

alert(\"Other\");

}

而與此等價的 switch 語句如下所示:

switch (i) {

case 25:

alert(\"25\");

break;

case 35:

alert(\"35\");

break;

case 45:

alert(\"45\");

break;

default:

alert(\"Other\");

}

SwitchStatementExample01.htm

通過為每個 case 后面都添加一個 break 語句,就可以避免同時執(zhí)行多個 case 代碼的情況。假如確

實需要混合幾種情形,不要忘了在代碼中添加注釋,說明你是有意省略了 break 關鍵字,如下所示:

switch (i) {

case 25:

/* 合并兩種情形 */

case 35:

alert(\"25 or 35\");

break;

case 45:

alert(\"45\");

break;

default:

alert(\"Other\");

}

SwitchStatementExample02.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第80頁

62 第 3 章 基本概念

雖然 ECMAScript 中的 switch 語句借鑒自其他語言,但這個語句也有自己的特色。首先,可以在

switch 語句中使用任何數(shù)據(jù)類型(在很多其他語言中只能使用數(shù)值),無論是字符串,還是對象都沒有

問題。其次,每個 case 的值不一定是常量,可以是變量,甚至是表達式。請看下面這個例子:

switch (\"hello world\") {

case \"hello\" + \" world\":

alert(\"Greeting was found.\");

break;

case \"goodbye\":

alert(\"Closing was found.\");

break;

default:

alert(\"Unexpected message was found.\");

}

SwitchStatementExample03.htm

在這個例子中,switch 語句使用的就是字符串。其中,第一種情形實際上是一個對字符串拼接操

作求值的表達式。由于這個字符串拼接表達式的結果與 switch 的參數(shù)相等,因此結果就會顯示

\"Greeting was found.\"。而且,使用表達式作為 case 值還可以實現(xiàn)下列操作:

var num = 25;

switch (true) {

case num < 0:

alert(\"Less than 0.\");

break;

case num >= 0 && num <= 10:

alert(\"Between 0 and 10.\");

break;

case num > 10 && num <= 20:

alert(\"Between 10 and 20.\");

break;

default:

alert(\"More than 20.\");

}

SwitchStatementExample04.htm

這個例子首先在 switch 語句外面聲明了變量 num。而之所以給 switch 語句傳遞表達式 true,

是因為每個 case 值都可以返回一個布爾值。這樣,每個 case 按照順序被求值,直到找到匹配的值或者

遇到 default 語句為止(這正是這個例子的最終結果)。

switch 語句在比較值時使用的是全等操作符,因此不會發(fā)生類型轉(zhuǎn)換(例如,

字符串\"10\"不等于數(shù)值 10)。

3.7 函數(shù)

函數(shù)對任何語言來說都是一個核心的概念。通過函數(shù)可以封裝任意多條語句,而且可以在任何地方、

任何時候調(diào)用執(zhí)行。ECMAScript 中的函數(shù)使用 function 關鍵字來聲明,后跟一組參數(shù)以及函數(shù)體。

函數(shù)的基本語法如下所示:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第81頁

3.7 函數(shù) 63

1

2

3

4

5

13

6

7

8

9

10

11

12

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

statements

}

以下是一個函數(shù)示例:

function sayHi(name, message) {

alert(\"Hello \" + name + \",\" + message);

}

FunctionExample01.htm

這個函數(shù)可以通過其函數(shù)名來調(diào)用,后面還要加上一對圓括號和參數(shù)(圓括號中的參數(shù)如果有多個,

可以用逗號隔開)。調(diào)用 sayHi()函數(shù)的代碼如下所示:

sayHi(\"Nicholas\", \"how are you today?\");

這個函數(shù)的輸出結果是\"Hello Nicholas,how are you today?\"。函數(shù)中定義中的命名參數(shù) name

和 message 被用作了字符串拼接的兩個操作數(shù),而結果最終通過警告框顯示了出來。

ECMAScript 中的函數(shù)在定義時不必指定是否返回值。實際上,任何函數(shù)在任何時候都可以通過

return 語句后跟要返回的值來實現(xiàn)返回值。請看下面的例子:

function sum(num1, num2) {

return num1 + num2;

}

FunctionExample02.htm

這個 sum()函數(shù)的作用是把兩個值加起來返回一個結果。我們注意到,除了 return 語句之外,沒

有任何聲明表示該函數(shù)會返回一個值。調(diào)用這個函數(shù)的示例代碼如下:

var result = sum(5, 10);

這個函數(shù)會在執(zhí)行完 return 語句之后停止并立即退出。因此,位于 return 語句之后的任何代碼

都永遠不會執(zhí)行。例如:

function sum(num1, num2) {

return num1 + num2;

alert(\"Hello world\"); // 永遠不會執(zhí)行

}

在這個例子中,由于調(diào)用 alert()函數(shù)的語句位于 return 語句之后,因此永遠不會顯示警告框。

當然,一個函數(shù)中也可以包含多個 return 語句,如下面這個例子中所示:

function diff(num1, num2) {

if (num1 < num2) {

return num2 - num1;

} else {

return num1 - num2;

}

}

FunctionExample03.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第82頁

64 第 3 章 基本概念

這個例子中定義的 diff()函數(shù)用于計算兩個數(shù)值的差。如果第一個數(shù)比第二個小,則用第二個數(shù)

減第一個數(shù);否則,用第一個數(shù)減第二個數(shù)。代碼中的兩個分支都具有自己的 return 語句,分別用于

執(zhí)行正確的計算。

另外,return 語句也可以不帶有任何返回值。在這種情況下,函數(shù)在停止執(zhí)行后將返回 undefined

值。這種用法一般用在需要提前停止函數(shù)執(zhí)行而又不需要返回值的情況下。比如在下面這個例子中,就

不會顯示警告框:

function sayHi(name, message) {

return;

alert(\"Hello \" + name + \",\" + message); //永遠不會調(diào)用

}

FunctionExample04.htm

推薦的做法是要么讓函數(shù)始終都返回一個值,要么永遠都不要返回值。否則,如

果函數(shù)有時候返回值,有時候有不返回值,會給調(diào)試代碼帶來不便。

嚴格模式對函數(shù)有一些限制:

? 不能把函數(shù)命名為 eval 或 arguments;

? 不能把參數(shù)命名為 eval 或 arguments;

? 不能出現(xiàn)兩個命名參數(shù)同名的情況。

如果發(fā)生以上情況,就會導致語法錯誤,代碼無法執(zhí)行。

3.7.1 理解參數(shù)

ECMAScript 函數(shù)的參數(shù)與大多數(shù)其他語言中函數(shù)的參數(shù)有所不同。ECMAScript 函數(shù)不介意傳遞進

來多少個參數(shù),也不在乎傳進來參數(shù)是什么數(shù)據(jù)類型。也就是說,即便你定義的函數(shù)只接收兩個參數(shù),

在調(diào)用這個函數(shù)時也未必一定要傳遞兩個參數(shù)??梢詡鬟f一個、三個甚至不傳遞參數(shù),而解析器永遠不

會有什么怨言。之所以會這樣,原因是 ECMAScript 中的參數(shù)在內(nèi)部是用一個數(shù)組來表示的。函數(shù)接收

到的始終都是這個數(shù)組,而不關心數(shù)組中包含哪些參數(shù)(如果有參數(shù)的話)。如果這個數(shù)組中不包含任

何元素,無所謂;如果包含多個元素,也沒有問題。實際上,在函數(shù)體內(nèi)可以通過 arguments 對象來

訪問這個參數(shù)數(shù)組,從而獲取傳遞給函數(shù)的每一個參數(shù)。

其實,arguments 對象只是與數(shù)組類似(它并不是 Array 的實例),因為可以使用方括號語法訪

問它的每一個元素(即第一個元素是 arguments[0],第二個元素是 argumetns[1],以此類推),使

用 length 屬性來確定傳遞進來多少個參數(shù)。在前面的例子中,sayHi()函數(shù)的第一個參數(shù)的名字叫

name,而該參數(shù)的值也可以通過訪問 arguments[0]來獲取。因此,那個函數(shù)也可以像下面這樣重寫,

即不顯式地使用命名參數(shù):

function sayHi() {

alert(\"Hello \" + arguments[0] + \",\" + arguments[1]);

}

FunctionExample05.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第83頁

3.7 函數(shù) 65

1

2

3

4

5

13

6

7

8

9

10

11

12

這個重寫后的函數(shù)中不包含命名的參數(shù)。雖然沒有使用 name 和 message 標識符,但函數(shù)的功能

依舊。這個事實說明了 ECMAScript 函數(shù)的一個重要特點:命名的參數(shù)只提供便利,但不是必需的。另

外,在命名參數(shù)方面,其他語言可能需要事先創(chuàng)建一個函數(shù)簽名,而將來的調(diào)用必須與該簽名一致。但

在 ECMAScript 中,沒有這些條條框框,解析器不會驗證命名參數(shù)。

通過訪問 arguments 對象的 length 屬性可以獲知有多少個參數(shù)傳遞給了函數(shù)。下面這個函數(shù)會

在每次被調(diào)用時,輸出傳入其中的參數(shù)個數(shù):

function howManyArgs() {

alert(arguments.length);

}

howManyArgs(\"string\", 45); //2

howManyArgs(); //0

howManyArgs(12); //1

FunctionExample06.htm

執(zhí)行以上代碼會依次出現(xiàn) 3 個警告框,分別顯示 2、0 和 1。由此可見,開發(fā)人員可以利用這一點讓

函數(shù)能夠接收任意個參數(shù)并分別實現(xiàn)適當?shù)墓δ?。請看下面的例子?/p>

function doAdd() {

if(arguments.length == 1) {

alert(arguments[0] + 10);

} else if (arguments.length == 2) {

alert(arguments[0] + arguments[1]);

}

}

doAdd(10); //20

doAdd(30, 20); //50

FunctionExample07.htm

函數(shù) doAdd()會在只有一個參數(shù)的情況下給該參數(shù)加上 10;如果是兩個參數(shù),則將那個參數(shù)簡單

相加并返回結果。因此,doAdd(10)會返回 20,而 doAdd(30,20)則返回 50。雖然這個特性算不上完

美的重載,但也足夠彌補 ECMAScript 的這一缺憾了。

另一個與參數(shù)相關的重要方面,就是 arguments 對象可以與命名參數(shù)一起使用,如下面的例子所示:

function doAdd(num1, num2) {

if(arguments.length == 1) {

alert(num1 + 10);

} else if (arguments.length == 2) {

alert(arguments[0] + num2);

}

}

FunctionExample08.htm

在重寫后的這個 doAdd()函數(shù)中,兩個命名參數(shù)都與 arguments 對象一起使用。由于 num1 的值

與 arguments[0]的值相同,因此它們可以互換使用(當然,num2 和 arguments[1]也是如此)。

關于 arguments 的行為,還有一點比較有意思。那就是它的值永遠與對應命名參數(shù)的值保持同步。

例如:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第84頁

66 第 3 章 基本概念

function doAdd(num1, num2) {

arguments[1] = 10;

alert(arguments[0] + num2);

}

FunctionExample09.htm

每次執(zhí)行這個 doAdd()函數(shù)都會重寫第二個參數(shù),將第二個參數(shù)的值修改為 10。因為 arguments

對象中的值會自動反映到對應的命名參數(shù),所以修改 arguments[1],也就修改了 num2,結果它們的

值都會變成 10。不過,這并不是說讀取這兩個值會訪問相同的內(nèi)存空間;它們的內(nèi)存空間是獨立的,但

它們的值會同步。另外還要記住,如果只傳入了一個參數(shù),那么為 arguments[1]設置的值不會反應到

命名參數(shù)中。這是因為 arguments 對象的長度是由傳入的參數(shù)個數(shù)決定的,不是由定義函數(shù)時的命名

參數(shù)的個數(shù)決定的。

關于參數(shù)還要記住最后一點:沒有傳遞值的命名參數(shù)將自動被賦予 undefined 值。這就跟定義了

變量但又沒有初始化一樣。例如,如果只給 doAdd()函數(shù)傳遞了一個參數(shù),則 num2 中就會保存

undefined 值。

嚴格模式對如何使用 arguments 對象做出了一些限制。首先,像前面例子中那樣的賦值會變得無

效。也就是說,即使把 arguments[1]設置為 10,num2 的值仍然還是 undefined。其次,重寫

arguments 的值會導致語法錯誤(代碼將不會執(zhí)行)。

ECMAScript 中的所有參數(shù)傳遞的都是值,不可能通過引用傳遞參數(shù)。

3.7.2 沒有重載

ECMAScript 函數(shù)不能像傳統(tǒng)意義上那樣實現(xiàn)重載。而在其他語言(如 Java)中,可以為一個函數(shù)

編寫兩個定義,只要這兩個定義的簽名(接受的參數(shù)的類型和數(shù)量)不同即可。如前所述,ECMAScirpt

函數(shù)沒有簽名,因為其參數(shù)是由包含零或多個值的數(shù)組來表示的。而沒有函數(shù)簽名,真正的重載是不可

能做到的。

如果在 ECMAScript 中定義了兩個名字相同的函數(shù),則該名字只屬于后定義的函數(shù)。請看下面的例子:

function addSomeNumber(num){

return num + 100;

}

function addSomeNumber(num) {

return num + 200;

}

var result = addSomeNumber(100); //300

FunctionExample10.htm

在此,函數(shù) addSomeNumber()被定義了兩次。第一個版本給參數(shù)加 100,而第二個版本給參數(shù)加

200。由于后定義的函數(shù)覆蓋了先定義的函數(shù),因此當在最后一行代碼中調(diào)用這個函數(shù)時,返回的結果

就是 300。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第85頁

3.8 小結 67

1

2

3

4

5

13

6

7

8

9

10

11

12

如前所述,通過檢查傳入函數(shù)中參數(shù)的類型和數(shù)量并作出不同的反應,可以模仿方法的重載。

3.8 小結

JavaScript 的核心語言特性在 ECMA-262 中是以名為 ECMAScript 的偽語言的形式來定義的。

ECMAScript 中包含了所有基本的語法、操作符、數(shù)據(jù)類型以及完成基本的計算任務所必需的對象,但

沒有對取得輸入和產(chǎn)生輸出的機制作出規(guī)定。理解 ECMAScript 及其紛繁復雜的各種細節(jié),是理解其在

Web 瀏覽器中的實現(xiàn)——JavaScript 的關鍵。目前大多數(shù)實現(xiàn)所遵循的都是 ECMA-262 第 3 版,但很多

也已經(jīng)著手開始實現(xiàn)第 5 版了。以下簡要總結了 ECMAScript 中基本的要素。

? ECMAScript 中的基本數(shù)據(jù)類型包括 Undefined、Null、Boolean、Number 和 String。

? 與其他語言不同,ECMScript 沒有為整數(shù)和浮點數(shù)值分別定義不同的數(shù)據(jù)類型,Number 類型可

用于表示所有數(shù)值。

? ECMAScript 中也有一種復雜的數(shù)據(jù)類型,即 Object 類型,該類型是這門語言中所有對象的基

礎類型。

? 嚴格模式為這門語言中容易出錯的地方施加了限制。

? ECMAScript 提供了很多與 C 及其他類 C 語言中相同的基本操作符,包括算術操作符、布爾操作

符、關系操作符、相等操作符及賦值操作符等。

? ECMAScript 從其他語言中借鑒了很多流控制語句,例如 if 語句、for 語句和 switch 語句等。

ECMAScript 中的函數(shù)與其他語言中的函數(shù)有諸多不同之處。

? 無須指定函數(shù)的返回值,因為任何 ECMAScript 函數(shù)都可以在任何時候返回任何值。

? 實際上,未指定返回值的函數(shù)返回的是一個特殊的 undefined 值。

? ECMAScript 中也沒有函數(shù)簽名的概念,因為其函數(shù)參數(shù)是以一個包含零或多個值的數(shù)組的形式

傳遞的。

? 可以向 ECMAScript 函數(shù)傳遞任意數(shù)量的參數(shù),并且可以通過 arguments 對象來訪問這些參數(shù)。

? 由于不存在函數(shù)簽名的特性,ECMAScript 函數(shù)不能重載。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第86頁

68 第 4 章 變量、作用域和內(nèi)存問題

變量、作用域和內(nèi)存問題

本章內(nèi)容

? 理解基本類型和引用類型的值

? 理解執(zhí)行環(huán)境

? 理解垃圾收集

照 ECMA-262 的定義,JavaScript 的變量與其他語言的變量有很大區(qū)別。JavaScript 變量松散

類型的本質(zhì),決定了它只是在特定時間用于保存特定值的一個名字而已。由于不存在定義某

個變量必須要保存何種數(shù)據(jù)類型值的規(guī)則,變量的值及其數(shù)據(jù)類型可以在腳本的生命周期內(nèi)改變。盡管

從某種角度看,這可能是一個既有趣又強大,同時又容易出問題的特性,但 JavaScript 變量實際的復雜

程度還遠不止如此。

4.1 基本類型和引用類型的值

ECMAScript 變量可能包含兩種不同數(shù)據(jù)類型的值:基本類型值和引用類型值。基本類型值指的是

簡單的數(shù)據(jù)段,而引用類型值指那些可能由多個值構成的對象。

在將一個值賦給變量時,解析器必須確定這個值是基本類型值還是引用類型值。第 3 章討論了 5 種

基本數(shù)據(jù)類型:Undefined、Null、Boolean、Number 和 String。這 5 種基本數(shù)據(jù)類型是按值訪問

的,因為可以操作保存在變量中的實際的值。

引用類型的值是保存在內(nèi)存中的對象。與其他語言不同,JavaScript 不允許直接訪問內(nèi)存中的位置,

也就是說不能直接操作對象的內(nèi)存空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。

為此,引用類型的值是按引用訪問的①。

在很多語言中,字符串以對象的形式來表示,因此被認為是引用類型的。

ECMAScript 放棄了這一傳統(tǒng)。

4.1.1 動態(tài)的屬性

定義基本類型值和引用類型值的方式是類似的:創(chuàng)建一個變量并為該變量賦值。但是,當這個值保

存到變量中以后,對不同類型值可以執(zhí)行的操作則大相徑庭。對于引用類型的值,我們可以為其添加屬

性和方法,也可以改變和刪除其屬性和方法。請看下面的例子:

——————————

① 這種說法不嚴密,當復制保存著對象的某個變量時,操作的是對象的引用。但在為對象添加屬性時,操作的是實際

的對象?!獔D靈社區(qū)“壯壯的前端之路”注

第 4 章

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第87頁

4.1 基本類型和引用類型的值 69

1

2

3

4

5

13

6

7

8

9

10

11

12

var person = new Object();

person.name = \"Nicholas\";

alert(person.name); //\"Nicholas\"

DynamicPropertiesExample01.htm

以上代碼創(chuàng)建了一個對象并將其保存在了變量 person 中。然后,我們?yōu)樵搶ο筇砑恿艘粋€名為

name 的屬性,并將字符串值\"Nicholas\"賦給了這個屬性。緊接著,又通過 alert()函數(shù)訪問了這個

新屬性。如果對象不被銷毀或者這個屬性不被刪除,則這個屬性將一直存在。

但是,我們不能給基本類型的值添加屬性,盡管這樣做不會導致任何錯誤。比如:

var name = \"Nicholas\";

name.age = 27;

alert(name.age); //undefined

DynamicPropertiesExample02.htm

在這個例子中,我們?yōu)樽址?name 定義了一個名為 age 的屬性,并為該屬性賦值 27。但在下一

行訪問這個屬性時,發(fā)現(xiàn)該屬性不見了。這說明只能給引用類型值動態(tài)地添加屬性,以便將來使用。

4.1.2 復制變量值

除了保存的方式不同之外,在從一個變量向另一個變量復制基本類型值和引用類型值時,也存在不

同。如果從一個變量向另一個變量復制基本類型的值,會在變量對象上創(chuàng)建一個新值,然后把該值復制

到為新變量分配的位置上。來看一個例子:

var num1 = 5;

var num2 = num1;

在此,num1 中保存的值是 5。當使用 num1 的值來初始化 num2 時,num2 中也保存了值 5。但 num2

中的 5 與 num1 中的 5 是完全獨立的,該值只是 num1 中 5 的一個副本。此后,這兩個變量可以參與任

何操作而不會相互影響。圖 4-1 形象地展示了復制基本類型值的過程。

圖 4-1

復制前的變量對象

復制后的變量對象

num1 5

(Number 類型)

num2 5

(Number 類型)

num1 5

(Number 類型)

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第88頁

70 第 4 章 變量、作用域和內(nèi)存問題

當從一個變量向另一個變量復制引用類型的值時,同樣也會將存儲在變量對象中的值復制一份放到

為新變量分配的空間中。不同的是,這個值的副本實際上是一個指針,而這個指針指向存儲在堆中的一

個對象。復制操作結束后,兩個變量實際上將引用同一個對象。因此,改變其中一個變量,就會影響另

一個變量,如下面的例子所示:

var obj1 = new Object();

var obj2 = obj1;

obj1.name = \"Nicholas\";

alert(obj2.name); //\"Nicholas\"

首先,變量 obj1 保存了一個對象的新實例。然后,這個值被復制到了 obj2 中;換句話說,obj1

和 obj2 都指向同一個對象。這樣,當為 obj1 添加 name 屬性后,可以通過 obj2 來訪問這個屬性,

因為這兩個變量引用的都是同一個對象。圖 4-2 展示了保存在變量對象中的變量和保存在堆中的對象之

間的這種關系。

圖 4-2

4.1.3 傳遞參數(shù)

ECMAScript 中所有函數(shù)的參數(shù)都是按值傳遞的。也就是說,把函數(shù)外部的值復制給函數(shù)內(nèi)部的參

數(shù),就和把值從一個變量復制到另一個變量一樣。基本類型值的傳遞如同基本類型變量的復制一樣,而

引用類型值的傳遞,則如同引用類型變量的復制一樣。有不少開發(fā)人員在這一點上可能會感到困惑,因

為訪問變量有按值和按引用兩種方式,而參數(shù)只能按值傳遞。

在向參數(shù)傳遞基本類型的值時,被傳遞的值會被復制給一個局部變量(即命名參數(shù),或者用

ECMAScript 的概念來說,就是 arguments 對象中的一個元素)。在向參數(shù)傳遞引用類型的值時,會把

這個值在內(nèi)存中的地址復制給一個局部變量,因此這個局部變量的變化會反映在函數(shù)的外部。請看下面

這個例子:

function addTen(num) {

num += 10;

return num;

}

復制前的變量對象 堆內(nèi)存

Object

Object

Object

Object

復制后的變量對象

obj1

obj2

obj1

(Object 類型)

(Object 類型)

(Object 類型)

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第89頁

4.1 基本類型和引用類型的值 71

1

2

3

4

5

13

6

7

8

9

10

11

12

var count = 20;

var result = addTen(count);

alert(count); //20,沒有變化

alert(result); //30

FunctionArgumentsExample01.htm

這里的函數(shù) addTen()有一個參數(shù) num,而參數(shù)實際上是函數(shù)的局部變量。在調(diào)用這個函數(shù)時,變

量count作為參數(shù)被傳遞給函數(shù),這個變量的值是20。于是,數(shù)值20被復制給參數(shù)num以便在addTen()

中使用。在函數(shù)內(nèi)部,參數(shù) num 的值被加上了 10,但這一變化不會影響函數(shù)外部的 count 變量。參數(shù)

num 與變量 count 互不相識,它們僅僅是具有相同的值。假如 num 是按引用傳遞的話,那么變量 count

的值也將變成 30,從而反映函數(shù)內(nèi)部的修改。當然,使用數(shù)值等基本類型值來說明按值傳遞參數(shù)比較簡

單,但如果使用對象,那問題就不怎么好理解了。再舉一個例子:

function setName(obj) {

obj.name = \"Nicholas\";

}

var person = new Object();

setName(person);

alert(person.name); //\"Nicholas\"

FunctionArgumentsExample02.htm

以上代碼中創(chuàng)建一個對象,并將其保存在了變量 person 中。然后,這個變量被傳遞到 setName()

函數(shù)中之后就被復制給了 obj。在這個函數(shù)內(nèi)部,obj 和 person 引用的是同一個對象。換句話說,即

使這個變量是按值傳遞的,obj 也會按引用來訪問同一個對象。于是,當在函數(shù)內(nèi)部為 obj 添加 name

屬性后,函數(shù)外部的 person 也將有所反映;因為 person 指向的對象在堆內(nèi)存中只有一個,而且是全

局對象。有很多開發(fā)人員錯誤地認為:在局部作用域中修改的對象會在全局作用域中反映出來,就說明

參數(shù)是按引用傳遞的。為了證明對象是按值傳遞的,我們再看一看下面這個經(jīng)過修改的例子:

function setName(obj) {

obj.name = \"Nicholas\";

obj = new Object();

obj.name = \"Greg\";

}

var person = new Object();

setName(person);

alert(person.name); //\"Nicholas\"

這個例子與前一個例子的唯一區(qū)別,就是在 setName()函數(shù)中添加了兩行代碼:一行代碼為 obj

重新定義了一個對象,另一行代碼為該對象定義了一個帶有不同值的 name 屬性。在把 person 傳遞給

setName()后,其 name 屬性被設置為\"Nicholas\"。然后,又將一個新對象賦給變量 obj,同時將其 name

屬性設置為\"Greg\"。如果 person 是按引用傳遞的,那么 person 就會自動被修改為指向其 name 屬性值

為\"Greg\"的新對象。但是,當接下來再訪問 person.name 時,顯示的值仍然是\"Nicholas\"。這說明

即使在函數(shù)內(nèi)部修改了參數(shù)的值,但原始的引用仍然保持未變。實際上,當在函數(shù)內(nèi)部重寫 obj 時,這

個變量引用的就是一個局部對象了。而這個局部對象會在函數(shù)執(zhí)行完畢后立即被銷毀。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第90頁

72 第 4 章 變量、作用域和內(nèi)存問題

可以把 ECMAScript 函數(shù)的參數(shù)想象成局部變量。

4.1.4 檢測類型

要檢測一個變量是不是基本數(shù)據(jù)類型?第 3 章介紹的 typeof 操作符是最佳的工具。說得更具體一

點,typeof 操作符是確定一個變量是字符串、數(shù)值、布爾值,還是 undefined 的最佳工具。如果變

量的值是一個對象或 null,則 typeof 操作符會像下面例子中所示的那樣返回\"object\":

var s = \"Nicholas\";

var b = true;

var i = 22;

var u;

var n = null;

var o = new Object();

alert(typeof s); //string

alert(typeof i); //number

alert(typeof b); //boolean

alert(typeof u); //undefined

alert(typeof n); //object

alert(typeof o); //object

DeterminingTypeExample01.htm

雖然在檢測基本數(shù)據(jù)類型時 typeof 是非常得力的助手,但在檢測引用類型的值時,這個操作符的

用處不大。通常,我們并不是想知道某個值是對象,而是想知道它是什么類型的對象。為此,ECMAScript

提供了 instanceof 操作符,其語法如下所示:

result = variable instanceof constructor

如果變量是給定引用類型(根據(jù)它的原型鏈來識別;第 6 章將介紹原型鏈)的實例,那么

instanceof 操作符就會返回 true。請看下面的例子:

alert(person instanceof Object); // 變量 person 是 Object 嗎?

alert(colors instanceof Array); // 變量 colors 是 Array 嗎?

alert(pattern instanceof RegExp); // 變量 pattern 是 RegExp 嗎?

根據(jù)規(guī)定,所有引用類型的值都是 Object 的實例。因此,在檢測一個引用類型值和 Object 構造

函數(shù)時,instanceof 操作符始終會返回 true。當然,如果使用 instanceof 操作符檢測基本類型的

值,則該操作符始終會返回 false,因為基本類型不是對象。

使用 typeof 操作符檢測函數(shù)時,該操作符會返回\"function\"。在 Safari 5 及

之前版本和 Chrome 7 及之前版本中使用 typeof 檢測正則表達式時,由于規(guī)范的原

因,這個操作符也返回\"function\"。ECMA-262 規(guī)定任何在內(nèi)部實現(xiàn)[[Call]]方法

的對象都應該在應用 typeof 操作符時返回\"function\"。由于上述瀏覽器中的正則

表達式也實現(xiàn)了這個方法,因此對正則表達式應用 typeof 會返回\"function\"。在

IE 和 Firefox 中,對正則表達式應用 typeof 會返回\"object\"。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第91頁

4.2 執(zhí)行環(huán)境及作用域 73

1

2

3

4

5

13

6

7

8

9

10

11

12

4.2 執(zhí)行環(huán)境及作用域

執(zhí)行環(huán)境(execution context,為簡單起見,有時也稱為“環(huán)境”)是 JavaScript 中最為重要的一個概

念。執(zhí)行環(huán)境定義了變量或函數(shù)有權訪問的其他數(shù)據(jù),決定了它們各自的行為。每個執(zhí)行環(huán)境都有一個

與之關聯(lián)的變量對象(variable object),環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。雖然我們

編寫的代碼無法訪問這個對象,但解析器在處理數(shù)據(jù)時會在后臺使用它。

全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境。根據(jù) ECMAScript 實現(xiàn)所在的宿主環(huán)境不同,表示執(zhí)行環(huán)

境的對象也不一樣。在 Web 瀏覽器中,全局執(zhí)行環(huán)境被認為是 window 對象(第 7 章將詳細討論),因

此所有全局變量和函數(shù)都是作為 window 對象的屬性和方法創(chuàng)建的。某個執(zhí)行環(huán)境中的所有代碼執(zhí)行完

畢后,該環(huán)境被銷毀,保存在其中的所有變量和函數(shù)定義也隨之銷毀(全局執(zhí)行環(huán)境直到應用程序退

出——例如關閉網(wǎng)頁或瀏覽器——時才會被銷毀)。

每個函數(shù)都有自己的執(zhí)行環(huán)境。當執(zhí)行流進入一個函數(shù)時,函數(shù)的環(huán)境就會被推入一個環(huán)境棧中。

而在函數(shù)執(zhí)行之后,棧將其環(huán)境彈出,把控制權返回給之前的執(zhí)行環(huán)境。ECMAScript 程序中的執(zhí)行流

正是由這個方便的機制控制著。

當代碼在一個環(huán)境中執(zhí)行時,會創(chuàng)建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是

保證對執(zhí)行環(huán)境有權訪問的所有變量和函數(shù)的有序訪問。作用域鏈的前端,始終都是當前執(zhí)行的代碼所

在環(huán)境的變量對象。如果這個環(huán)境是函數(shù),則將其活動對象(activation object)作為變量對象?;顒訉?/p>

象在最開始時只包含一個變量,即 arguments 對象(這個對象在全局環(huán)境中是不存在的)。作用域鏈中

的下一個變量對象來自包含(外部)環(huán)境,而再下一個變量對象則來自下一個包含環(huán)境。這樣,一直延

續(xù)到全局執(zhí)行環(huán)境;全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象。

標識符解析是沿著作用域鏈一級一級地搜索標識符的過程。搜索過程始終從作用域鏈的前端開始,

然后逐級地向后回溯,直至找到標識符為止(如果找不到標識符,通常會導致錯誤發(fā)生)。

請看下面的示例代碼:

var color = \"blue\";

function changeColor(){

if (color === \"blue\"){

color = \"red\";

} else {

color = \"blue\";

}

}

changeColor();

alert(\"Color is now \" + color);

ExecutionContextExample01.htm

在這個簡單的例子中,函數(shù) changeColor()的作用域鏈包含兩個對象:它自己的變量對象(其中

定義著 arguments 對象)和全局環(huán)境的變量對象??梢栽诤瘮?shù)內(nèi)部訪問變量 color,就是因為可以在

這個作用域鏈中找到它。

此外,在局部作用域中定義的變量可以在局部環(huán)境中與全局變量互換使用,如下面這個例子所示:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第92頁

74 第 4 章 變量、作用域和內(nèi)存問題

var color = \"blue\";

function changeColor(){

var anotherColor = \"red\";

function swapColors(){

var tempColor = anotherColor;

anotherColor = color;

color = tempColor;

// 這里可以訪問 color、anotherColor 和 tempColor

}

// 這里可以訪問 color 和 anotherColor,但不能訪問 tempColor

swapColors();

}

// 這里只能訪問 color

changeColor();

以上代碼共涉及 3 個執(zhí)行環(huán)境:全局環(huán)境、changeColor()的局部環(huán)境和 swapColors()的局部

環(huán)境。全局環(huán)境中有一個變量 color 和一個函數(shù) changeColor()。changeColor()的局部環(huán)境中有

一個名為 anotherColor 的變量和一個名為 swapColors()的函數(shù),但它也可以訪問全局環(huán)境中的變

量 color。swapColors()的局部環(huán)境中有一個變量 tempColor,該變量只能在這個環(huán)境中訪問到。

無論全局環(huán)境還是 changeColor()的局部環(huán)境都無權訪問 tempColor。然而,在 swapColors()內(nèi)部

則可以訪問其他兩個環(huán)境中的所有變量,因為那兩個環(huán)境是它的父執(zhí)行環(huán)境。圖 4-3 形象地展示了前面

這個例子的作用域鏈。

圖 4-3

圖 4-3 中的矩形表示特定的執(zhí)行環(huán)境。其中,內(nèi)部環(huán)境可以通過作用域鏈訪問所有的外部環(huán)境,但

外部環(huán)境不能訪問內(nèi)部環(huán)境中的任何變量和函數(shù)。這些環(huán)境之間的聯(lián)系是線性、有次序的。每個環(huán)境都

可以向上搜索作用域鏈,以查詢變量和函數(shù)名;但任何環(huán)境都不能通過向下搜索作用域鏈而進入另一個

執(zhí)行環(huán)境。對于這個例子中的 swapColors()而言,其作用域鏈中包含 3 個對象:swapColors()的變

量對象、changeColor()的變量對象和全局變量對象。swapColors()的局部環(huán)境開始時會先在自己的

變量對象中搜索變量和函數(shù)名,如果搜索不到則再搜索上一級作用域鏈。changeColor()的作用域鏈

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第93頁

4.2 執(zhí)行環(huán)境及作用域 75

1

2

3

4

5

13

6

7

8

9

10

11

12

中只包含兩個對象:它自己的變量對象和全局變量對象。這也就是說,它不能訪問 swapColors()的

環(huán)境。

函數(shù)參數(shù)也被當作變量來對待,因此其訪問規(guī)則與執(zhí)行環(huán)境中的其他變量相同。

4.2.1 延長作用域鏈

雖然執(zhí)行環(huán)境的類型總共只有兩種——全局和局部(函數(shù)),但還是有其他辦法來延長作用域鏈。

這么說是因為有些語句可以在作用域鏈的前端臨時增加一個變量對象,該變量對象會在代碼執(zhí)行后被移

除。在兩種情況下會發(fā)生這種現(xiàn)象。具體來說,就是當執(zhí)行流進入下列任何一個語句時,作用域鏈就會

得到加長:

? try-catch 語句的 catch 塊;

? with 語句。

這兩個語句都會在作用域鏈的前端添加一個變量對象。對 with 語句來說,會將指定的對象添加到

作用域鏈中。對 catch 語句來說,會創(chuàng)建一個新的變量對象,其中包含的是被拋出的錯誤對象的聲明。

下面看一個例子。

function buildUrl() {

var qs = \"?debug=true\";

with(location){

var url = href + qs;

}

return url;

}

ExecutionContextExample03.htm

在此,with 語句接收的是 location 對象,因此其變量對象中就包含了 location 對象的所有屬

性和方法,而這個變量對象被添加到了作用域鏈的前端。buildUrl()函數(shù)中定義了一個變量 qs。當在

with 語句中引用變量 href 時(實際引用的是 location.href),可以在當前執(zhí)行環(huán)境的變量對象中

找到。當引用變量 qs 時,引用的則是在 buildUrl()中定義的那個變量,而該變量位于函數(shù)環(huán)境的變

量對象中。至于 with 語句內(nèi)部,則定義了一個名為 url 的變量,因而 url 就成了函數(shù)執(zhí)行環(huán)境的一

部分,所以可以作為函數(shù)的值被返回。

在 IE8 及之前版本的 JavaScript 實現(xiàn)中,存在一個與標準不一致的地方,即在

catch 語句中捕獲的錯誤對象會被添加到執(zhí)行環(huán)境的變量對象,而不是 catch 語句

的變量對象中。換句話說,即使是在 catch 塊的外部也可以訪問到錯誤對象。IE9 修

復了這個問題。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第94頁

76 第 4 章 變量、作用域和內(nèi)存問題

4.2.2 沒有塊級作用域

JavaScript 沒有塊級作用域經(jīng)常會導致理解上的困惑。在其他類 C 的語言中,由花括號封閉的代碼

塊都有自己的作用域(如果用 ECMAScript 的話來講,就是它們自己的執(zhí)行環(huán)境),因而支持根據(jù)條件來

定義變量。例如,下面的代碼在 JavaScript 中并不會得到想象中的結果:

if (true) {

var color = \"blue\";

}

alert(color); //\"blue\"

這里是在一個 if 語句中定義了變量 color。如果是在 C、C++或 Java 中,color 會在 if 語句執(zhí)

行完畢后被銷毀。但在 JavaScript 中,if 語句中的變量聲明會將變量添加到當前的執(zhí)行環(huán)境(在這里是

全局環(huán)境)中。在使用 for 語句時尤其要牢記這一差異,例如:

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

doSomething(i);

}

alert(i); //10

對于有塊級作用域的語言來說,for 語句初始化變量的表達式所定義的變量,只會存在于循環(huán)的環(huán)

境之中。而對于 JavaScript 來說,由 for 語句創(chuàng)建的變量 i 即使在 for 循環(huán)執(zhí)行結束后,也依舊會存在

于循環(huán)外部的執(zhí)行環(huán)境中。

1. 聲明變量

使用 var 聲明的變量會自動被添加到最接近的環(huán)境中。在函數(shù)內(nèi)部,最接近的環(huán)境就是函數(shù)的局部

環(huán)境;在 with 語句中,最接近的環(huán)境是函數(shù)環(huán)境。如果初始化變量時沒有使用 var 聲明,該變量會自

動被添加到全局環(huán)境。如下所示:

function add(num1, num2) {

var sum = num1 + num2;

return sum;

}

var result = add(10, 20); //30

alert(sum); //由于 sum 不是有效的變量,因此會導致錯誤

ExecutionContextExample04.htm

以上代碼中的函數(shù) add()定義了一個名為 sum 的局部變量,該變量包含加法操作的結果。雖然結

果值從函數(shù)中返回了,但變量 sum 在函數(shù)外部是訪問不到的。如果省略這個例子中的 var 關鍵字,那

么當 add()執(zhí)行完畢后,sum 也將可以訪問到:

function add(num1, num2) {

sum = num1 + num2;

return sum;

}

var result = add(10, 20); //30

alert(sum); //30

ExecutionContextExample05.htm

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第95頁

4.2 執(zhí)行環(huán)境及作用域 77

1

2

3

4

5

13

6

7

8

9

10

11

12

這個例子中的變量 sum 在被初始化賦值時沒有使用 var 關鍵字。于是,當調(diào)用完 add()之后,添

加到全局環(huán)境中的變量 sum 將繼續(xù)存在;即使函數(shù)已經(jīng)執(zhí)行完畢,后面的代碼依舊可以訪問它。

在編寫 JavaScript 代碼的過程中,不聲明而直接初始化變量是一個常見的錯誤做

法,因為這樣可能會導致意外。我們建議在初始化變量之前,一定要先聲明,這樣就

可以避免類似問題。在嚴格模式下,初始化未經(jīng)聲明的變量會導致錯誤。

2. 查詢標識符

當在某個環(huán)境中為了讀取或?qū)懭攵靡粋€標識符時,必須通過搜索來確定該標識符實際代表什

么。搜索過程從作用域鏈的前端開始,向上逐級查詢與給定名字匹配的標識符。如果在局部環(huán)境中找到

了該標識符,搜索過程停止,變量就緒。如果在局部環(huán)境中沒有找到該變量名,則繼續(xù)沿作用域鏈向上

搜索。搜索過程將一直追溯到全局環(huán)境的變量對象。如果在全局環(huán)境中也沒有找到這個標識符,則意味

著該變量尚未聲明。

通過下面這個示例,可以理解查詢標識符的過程:

var color = \"blue\";

function getColor(){

return color;

}

alert(getColor()); //\"blue\"

ExecutionContextExample06.htm

調(diào)用本例中的函數(shù) getColor()時會引用變量 color。為了確定變量 color 的值,將開始一個兩

步的搜索過程。首先,搜索 getColor()的變量對象,查找其中是否包含一個名為 color 的標識符。

在沒有找到的情況下,搜索繼續(xù)到下一個變量對象(全局環(huán)境的變量對象),然后在那里找到了名為

color 的標識符。因為搜索到了定義這個變量的變量對象,搜索過程宣告結束。圖 4-4 形象地展示了上

述搜索過程。

圖 4-4

在這個搜索過程中,如果存在一個局部的變量的定義,則搜索會自動停止,不再進入另一個變量對

象。換句話說,如果局部環(huán)境中存在著同名標識符,就不會使用位于父環(huán)境中的標識符,如下面的例子

所示:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第96頁

78 第 4 章 變量、作用域和內(nèi)存問題

var color = \"blue\";

function getColor(){

var color = \"red\";

return color;

}

alert(getColor()); //\"red\"

ExecutionContextExample07.htm

修改后的代碼在 getColor()函數(shù)中聲明了一個名為 color 的局部變量。調(diào)用函數(shù)時,該變量

就會被聲明。而當函數(shù)中的第二行代碼執(zhí)行時,意味著必須找到并返回變量 color 的值。搜索過程

首先從局部環(huán)境中開始,而且在這里發(fā)現(xiàn)了一個名為 color 的變量,其值為\"red\"。因為變量已經(jīng)找

到了,所以搜索即行停止,return 語句就使用這個局部變量,并為函數(shù)會返回\"red\"。也就是說,

任何位于局部變量 color 的聲明之后的代碼,如果不使用 window.color 都無法訪問全局 color

變量。

變量查詢也不是沒有代價的。很明顯,訪問局部變量要比訪問全局變量更快,因

為不用向上搜索作用域鏈。JavaScript 引擎在優(yōu)化標識符查詢方面做得不錯,因此這

個差別在將來恐怕就可以忽略不計了。

4.3 垃圾收集

JavaScript 具有自動垃圾收集機制,也就是說,執(zhí)行環(huán)境會負責管理代碼執(zhí)行過程中使用的內(nèi)存。

而在 C 和 C++之類的語言中,開發(fā)人員的一項基本任務就是手工跟蹤內(nèi)存的使用情況,這是造成許多問

題的一個根源。在編寫 JavaScript 程序時,開發(fā)人員不用再關心內(nèi)存使用問題,所需內(nèi)存的分配以及無

用內(nèi)存的回收完全實現(xiàn)了自動管理。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續(xù)使用的變

量,然后釋放其占用的內(nèi)存。為此,垃圾收集器會按照固定的時間間隔(或代碼執(zhí)行中預定的收集時間),

周期性地執(zhí)行這一操作。

下面我們來分析一下函數(shù)中局部變量的正常生命周期。局部變量只在函數(shù)執(zhí)行的過程中存在。而在

這個過程中,會為局部變量在棧(或堆)內(nèi)存上分配相應的空間,以便存儲它們的值。然后在函數(shù)中使

用這些變量,直至函數(shù)執(zhí)行結束。此時,局部變量就沒有存在的必要了,因此可以釋放它們的內(nèi)存以供

將來使用。在這種情況下,很容易判斷變量是否還有存在的必要;但并非所有情況下都這么容易就能得

出結論。垃圾收集器必須跟蹤哪個變量有用哪個變量沒用,對于不再有用的變量打上標記,以備將來收

回其占用的內(nèi)存。用于標識無用變量的策略可能會因?qū)崿F(xiàn)而異,但具體到瀏覽器中的實現(xiàn),則通常有兩

個策略。

4.3.1 標記清除

JavaScript 中最常用的垃圾收集方式是標記清除(mark-and-sweep)。當變量進入環(huán)境(例如,在函

數(shù)中聲明一個變量)時,就將這個變量標記為“進入環(huán)境”。從邏輯上講,永遠不能釋放進入環(huán)境的變

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第97頁

4.3 垃圾收集 79

1

2

3

4

5

13

6

7

8

9

10

11

12

量所占用的內(nèi)存,因為只要執(zhí)行流進入相應的環(huán)境,就可能會用到它們。而當變量離開環(huán)境時,則將其

標記為“離開環(huán)境”。

可以使用任何方式來標記變量。比如,可以通過翻轉(zhuǎn)某個特殊的位來記錄一個變量何時進入環(huán)境,

或者使用一個“進入環(huán)境的”變量列表及一個“離開環(huán)境的”變量列表來跟蹤哪個變量發(fā)生了變化。說

到底,如何標記變量其實并不重要,關鍵在于采取什么策略。

垃圾收集器在運行的時候會給存儲在內(nèi)存中的所有變量都加上標記(當然,可以使用任何標記方

式)。然后,它會去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標記。而在此之后再被加上標記

的變量將被視為準備刪除的變量,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后,垃圾收集器

完成內(nèi)存清除工作,銷毀那些帶標記的值并回收它們所占用的內(nèi)存空間。

到 2008 年為止,IE、Firefox、Opera、Chrome 和 Safari 的 JavaScript 實現(xiàn)使用的都是標記清除式的

垃圾收集策略(或類似的策略),只不過垃圾收集的時間間隔互有不同。

4.3.2 引用計數(shù)

另一種不太常見的垃圾收集策略叫做引用計數(shù)(reference counting)。引用計數(shù)的含義是跟蹤記錄每

個值被引用的次數(shù)。當聲明了一個變量并將一個引用類型值賦給該變量時,則這個值的引用次數(shù)就是 1。

如果同一個值又被賦給另一個變量,則該值的引用次數(shù)加 1。相反,如果包含對這個值引用的變量又取

得了另外一個值,則這個值的引用次數(shù)減 1。當這個值的引用次數(shù)變成 0 時,則說明沒有辦法再訪問這

個值了,因而就可以將其占用的內(nèi)存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那

些引用次數(shù)為零的值所占用的內(nèi)存。

Netscape Navigator 3.0 是最早使用引用計數(shù)策略的瀏覽器,但很快它就遇到了一個嚴重的問題:循

環(huán)引用。循環(huán)引用指的是對象 A 中包含一個指向?qū)ο?B 的指針,而對象 B 中也包含一個指向?qū)ο?A 的

引用。請看下面這個例子:

function problem(){

var objectA = new Object();

var objectB = new Object();

objectA.someOtherObject = objectB;

objectB.anotherObject = objectA;

}

在這個例子中,objectA 和 objectB 通過各自的屬性相互引用;也就是說,這兩個對象的引用次

數(shù)都是 2。在采用標記清除策略的實現(xiàn)中,由于函數(shù)執(zhí)行之后,這兩個對象都離開了作用域,因此這種

相互引用不是個問題。但在采用引用計數(shù)策略的實現(xiàn)中,當函數(shù)執(zhí)行完畢后,objectA 和 objectB 還

將繼續(xù)存在,因為它們的引用次數(shù)永遠不會是 0。假如這個函數(shù)被重復多次調(diào)用,就會導致大量內(nèi)存得

不到回收。為此,Netscape 在 Navigator 4.0 中放棄了引用計數(shù)方式,轉(zhuǎn)而采用標記清除來實現(xiàn)其垃圾收

集機制。可是,引用計數(shù)導致的麻煩并未就此終結。

我們知道,IE 中有一部分對象并不是原生 JavaScript 對象。例如,其 BOM 和 DOM 中的對象就是

使用 C++以 COM(Component Object Model,組件對象模型)對象的形式實現(xiàn)的,而 COM 對象的垃圾

收集機制采用的就是引用計數(shù)策略。因此,即使 IE 的 JavaScript 引擎是使用標記清除策略來實現(xiàn)的,但

JavaScript 訪問的 COM 對象依然是基于引用計數(shù)策略的。換句話說,只要在 IE 中涉及 COM 對象,就會

存在循環(huán)引用的問題。下面這個簡單的例子,展示了使用 COM 對象導致的循環(huán)引用問題:

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第98頁

80 第 4 章 變量、作用域和內(nèi)存問題

var element = document.getElementById(\"some_element\");

var myObject = new Object();

myObject.element = element;

element.someObject = myObject;

這個例子在一個 DOM 元素(element)與一個原生 JavaScript 對象(myObject)之間創(chuàng)建了循環(huán)

引用。其中,變量 myObject 有一個名為 element 的屬性指向 element 對象;而變量 element 也有

一個屬性名叫 someObject 回指 myObject。由于存在這個循環(huán)引用,即使將例子中的 DOM 從頁面中

移除,它也永遠不會被回收。

為了避免類似這樣的循環(huán)引用問題,最好是在不使用它們的時候手工斷開原生 JavaScript 對象與

DOM 元素之間的連接。例如,可以使用下面的代碼消除前面例子創(chuàng)建的循環(huán)引用:

myObject.element = null;

element.someObject = null;

將變量設置為 null 意味著切斷變量與它此前引用的值之間的連接。當垃圾收集器下次運行時,就

會刪除這些值并回收它們占用的內(nèi)存。

為了解決上述問題,IE9 把 BOM 和 DOM 對象都轉(zhuǎn)換成了真正的 JavaScript 對象。這樣,就避免了

兩種垃圾收集算法并存導致的問題,也消除了常見的內(nèi)存泄漏現(xiàn)象。

導致循環(huán)引用的情況不止這些,其他一些情況將在本書中陸續(xù)介紹。

4.3.3 性能問題

垃圾收集器是周期性運行的,而且如果為變量分配的內(nèi)存數(shù)量很可觀,那么回收工作量也是相當大

的。在這種情況下,確定垃圾收集的時間間隔是一個非常重要的問題。說到垃圾收集器多長時間運行一

次,不禁讓人聯(lián)想到 IE 因此而聲名狼藉的性能問題。IE 的垃圾收集器是根據(jù)內(nèi)存分配量運行的,具體

一點說就是 256 個變量、4096 個對象(或數(shù)組)字面量和數(shù)組元素(slot)或者 64KB 的字符串。達到

上述任何一個臨界值,垃圾收集器就會運行。這種實現(xiàn)方式的問題在于,如果一個腳本中包含那么多變

量,那么該腳本很可能會在其生命周期中一直保有那么多的變量。而這樣一來,垃圾收集器就不得不頻

繁地運行。結果,由此引發(fā)的嚴重性能問題促使 IE7 重寫了其垃圾收集例程。

隨著 IE7 的發(fā)布,其 JavaScript 引擎的垃圾收集例程改變了工作方式:觸發(fā)垃圾收集的變量分配、

字面量和(或)數(shù)組元素的臨界值被調(diào)整為動態(tài)修正。IE7 中的各項臨界值在初始時與 IE6 相等。如果

垃圾收集例程回收的內(nèi)存分配量低于 15%,則變量、字面量和(或)數(shù)組元素的臨界值就會加倍。如果

例程回收了 85%的內(nèi)存分配量,則將各種臨界值重置回默認值。這一看似簡單的調(diào)整,極大地提升了 IE

在運行包含大量 JavaScript 的頁面時的性能。

事實上,在有的瀏覽器中可以觸發(fā)垃圾收集過程,但我們不建議讀者這樣做。在

IE 中,調(diào)用 window.CollectGarbage()方法會立即執(zhí)行垃圾收集。在 Opera 7 及更

高版本中,調(diào)用 window.opera.collect()也會啟動垃圾收集例程。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第99頁

4.4 小結 81

1

2

3

4

5

13

6

7

8

9

10

11

12

4.3.4 管理內(nèi)存

使用具備垃圾收集機制的語言編寫程序,開發(fā)人員一般不必操心內(nèi)存管理的問題。但是,JavaScript

在進行內(nèi)存管理及垃圾收集時面臨的問題還是有點與眾不同。其中最主要的一個問題,就是分配給 Web

瀏覽器的可用內(nèi)存數(shù)量通常要比分配給桌面應用程序的少。這樣做的目的主要是出于安全方面的考慮,

目的是防止運行 JavaScript 的網(wǎng)頁耗盡全部系統(tǒng)內(nèi)存而導致系統(tǒng)崩潰。內(nèi)存限制問題不僅會影響給變量

分配內(nèi)存,同時還會影響調(diào)用棧以及在一個線程中能夠同時執(zhí)行的語句數(shù)量。

因此,確保占用最少的內(nèi)存可以讓頁面獲得更好的性能。而優(yōu)化內(nèi)存占用的最佳方式,就是為執(zhí)行

中的代碼只保存必要的數(shù)據(jù)。一旦數(shù)據(jù)不再有用,最好通過將其值設置為 null 來釋放其引用——這個

做法叫做解除引用(dereferencing)。這一做法適用于大多數(shù)全局變量和全局對象的屬性。局部變量會在

它們離開執(zhí)行環(huán)境時自動被解除引用,如下面這個例子所示:

function createPerson(name){

var localPerson = new Object();

localPerson.name = name;

return localPerson;

}

var globalPerson = createPerson(\"Nicholas\");

// 手工解除 globalPerson 的引用

globalPerson = null;

在這個例子中,變量 globalPerson 取得了 createPerson()函數(shù)返回的值。在 createPerson()

函數(shù)內(nèi)部,我們創(chuàng)建了一個對象并將其賦給局部變量 localPerson,然后又為該對象添加了一個名為

name 的屬性。最后,當調(diào)用這個函數(shù)時,localPerson 以函數(shù)值的形式返回并賦給全局變量

globalPerson。由于 localPerson 在 createPerson()函數(shù)執(zhí)行完畢后就離開了其執(zhí)行環(huán)境,因此

無需我們顯式地去為它解除引用。但是對于全局變量 globalPerson 而言,則需要我們在不使用它的

時候手工為它解除引用,這也正是上面例子中最后一行代碼的目的。

不過,解除一個值的引用并不意味著自動回收該值所占用的內(nèi)存。解除引用的真正作用是讓值脫離

執(zhí)行環(huán)境,以便垃圾收集器下次運行時將其回收。

4.4 小結

JavaScript 變量可以用來保存兩種類型的值:基本類型值和引用類型值?;绢愋偷闹翟醋砸韵?5

種基本數(shù)據(jù)類型:Undefined、Null、Boolean、Number 和 String?;绢愋椭岛鸵妙愋椭稻?/p>

有以下特點:

? 基本類型值在內(nèi)存中占據(jù)固定大小的空間,因此被保存在棧內(nèi)存中;

? 從一個變量向另一個變量復制基本類型的值,會創(chuàng)建這個值的一個副本;

? 引用類型的值是對象,保存在堆內(nèi)存中;

? 包含引用類型值的變量實際上包含的并不是對象本身,而是一個指向該對象的指針;

? 從一個變量向另一個變量復制引用類型的值,復制的其實是指針,因此兩個變量最終都指向同

一個對象;

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

第100頁

82 第 4 章 變量、作用域和內(nèi)存問題

? 確定一個值是哪種基本類型可以使用 typeof 操作符,而確定一個值是哪種引用類型可以使用

instanceof 操作符。

所有變量(包括基本類型和引用類型)都存在于一個執(zhí)行環(huán)境(也稱為作用域)當中,這個執(zhí)

行環(huán)境決定了變量的生命周期,以及哪一部分代碼可以訪問其中的變量。以下是關于執(zhí)行環(huán)境的幾

點總結:

? 執(zhí)行環(huán)境有全局執(zhí)行環(huán)境(也稱為全局環(huán)境)和函數(shù)執(zhí)行環(huán)境之分;

? 每次進入一個新執(zhí)行環(huán)境,都會創(chuàng)建一個用于搜索變量和函數(shù)的作用域鏈;

? 函數(shù)的局部環(huán)境不僅有權訪問函數(shù)作用域中的變量,而且有權訪問其包含(父)環(huán)境,乃至全

局環(huán)境;

? 全局環(huán)境只能訪問在全局環(huán)境中定義的變量和函數(shù),而不能直接訪問局部環(huán)境中的任何數(shù)據(jù);

? 變量的執(zhí)行環(huán)境有助于確定應該何時釋放內(nèi)存。

JavaScript 是一門具有自動垃圾收集機制的編程語言,開發(fā)人員不必關心內(nèi)存分配和回收問題。可

以對 JavaScript 的垃圾收集例程作如下總結。

? 離開作用域的值將被自動標記為可以回收,因此將在垃圾收集期間被刪除。

? “標記清除”是目前主流的垃圾收集算法,這種算法的思想是給當前不使用的值加上標記,然

后再回收其內(nèi)存。

? 另一種垃圾收集算法是“引用計數(shù)”,這種算法的思想是跟蹤記錄所有值被引用的次數(shù)。JavaScript

引擎目前都不再使用這種算法;但在 IE 中訪問非原生 JavaScript 對象(如 DOM 元素)時,這種

算法仍然可能會導致問題。

? 當代碼中存在循環(huán)引用現(xiàn)象時,“引用計數(shù)”算法就會導致問題。

? 解除變量的引用不僅有助于消除循環(huán)引用現(xiàn)象,而且對垃圾收集也有好處。為了確保有效地回

收內(nèi)存,應該及時解除不再使用的全局對象、全局對象屬性以及循環(huán)引用變量的引用。

圖靈社區(qū)會員 StinkBC(StinkBC@gmail.com) 專享 尊重版權

百萬用戶使用云展網(wǎng)進行書冊翻頁效果制作,只要您有文檔,即可一鍵上傳,自動生成鏈接和二維碼(獨立電子書),支持分享到微信和網(wǎng)站!
收藏
轉(zhuǎn)發(fā)
下載
免費制作
其他案例
更多案例
免費制作
x
{{item.desc}}
下載
{{item.title}}
{{toast}}