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

javascript-gaojichengx有目錄u

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

javascript-gaojichengx有目錄u

5.7 單體內(nèi)置對(duì)象 133 1 2 3 45 13 67 8 9 1011 12eval(\"var msg = 'hello world'; \"); alert(msg); //\"hello world\" 在 eval()中創(chuàng)建的任何變量或函數(shù)都不會(huì)被提升,因?yàn)樵诮馕龃a的時(shí)候,它們被包含在一個(gè)字符串中;它們只在 eval()執(zhí)行的時(shí)候創(chuàng)建。嚴(yán)格模式下,在外部訪問(wèn)不到 eval()中創(chuàng)建的任何變量或函數(shù),因此前面兩個(gè)例子都會(huì)導(dǎo)致錯(cuò)誤。同樣,在嚴(yán)格模式下,為 eval 賦值也會(huì)導(dǎo)致錯(cuò)誤:\"use strict\"; eval = \"hi\"; //causes error 能夠解釋代碼字符串的能力非常強(qiáng)大,但也非常危險(xiǎn)。因此在使用 eval()時(shí)必須極為謹(jǐn)慎,特別是在用它執(zhí)行用戶輸入數(shù)據(jù)的情況下。否則,可能會(huì)有惡意用戶輸入威脅你的站點(diǎn)或應(yīng)用程序安全的代碼(即所謂的代碼注入)。3. Global 對(duì)象的屬性Global... [收起]
[展開(kāi)]
javascript-gaojichengx有目錄u
粉絲: {{bookData.followerCount}}
文本內(nèi)容
第151頁(yè)

5.7 單體內(nèi)置對(duì)象 133

1

2

3

4

5

13

6

7

8

9

10

11

12

eval(\"var msg = 'hello world'; \");

alert(msg); //\"hello world\"

在 eval()中創(chuàng)建的任何變量或函數(shù)都不會(huì)被提升,因?yàn)樵诮馕龃a的時(shí)候,它們被包含在一個(gè)字

符串中;它們只在 eval()執(zhí)行的時(shí)候創(chuàng)建。

嚴(yán)格模式下,在外部訪問(wèn)不到 eval()中創(chuàng)建的任何變量或函數(shù),因此前面兩個(gè)例子都會(huì)導(dǎo)致錯(cuò)誤。

同樣,在嚴(yán)格模式下,為 eval 賦值也會(huì)導(dǎo)致錯(cuò)誤:

\"use strict\";

eval = \"hi\"; //causes error

能夠解釋代碼字符串的能力非常強(qiáng)大,但也非常危險(xiǎn)。因此在使用 eval()時(shí)必

須極為謹(jǐn)慎,特別是在用它執(zhí)行用戶輸入數(shù)據(jù)的情況下。否則,可能會(huì)有惡意用戶輸

入威脅你的站點(diǎn)或應(yīng)用程序安全的代碼(即所謂的代碼注入)。

3. Global 對(duì)象的屬性

Global 對(duì)象還包含一些屬性,其中一部分屬性已經(jīng)在本書(shū)前面介紹過(guò)了。例如,特殊的值

undefined、NaN 以及 Infinity 都是 Global 對(duì)象的屬性。此外,所有原生引用類(lèi)型的構(gòu)造函數(shù),像

Object 和 Function,也都是 Global 對(duì)象的屬性。下表列出了 Global 對(duì)象的所有屬性。

屬 性 說(shuō) 明 屬 性 說(shuō) 明

undefined 特殊值undefined Date 構(gòu)造函數(shù)Date

NaN 特殊值NaN RegExp 構(gòu)造函數(shù)RegExp

Infinity 特殊值Infinity Error 構(gòu)造函數(shù)Error

Object 構(gòu)造函數(shù)Object EvalError 構(gòu)造函數(shù)EvalError

Array 構(gòu)造函數(shù)Array RangeError 構(gòu)造函數(shù)RangeError

Function 構(gòu)造函數(shù)Function ReferenceError 構(gòu)造函數(shù)ReferenceError

Boolean 構(gòu)造函數(shù)Boolean SyntaxError 構(gòu)造函數(shù)SyntaxError

String 構(gòu)造函數(shù)String TypeError 構(gòu)造函數(shù)TypeError

Number 構(gòu)造函數(shù)Number URIError 構(gòu)造函數(shù)URIError

ECMAScript 5 明確禁止給 undefined、NaN 和 Infinity 賦值,這樣做即使在非嚴(yán)格模式下也會(huì)

導(dǎo)致錯(cuò)誤。

4. window 對(duì)象

ECMAScript 雖然沒(méi)有指出如何直接訪問(wèn) Global 對(duì)象,但 Web 瀏覽器都是將這個(gè)全局對(duì)象作為

window 對(duì)象的一部分加以實(shí)現(xiàn)的。因此,在全局作用域中聲明的所有變量和函數(shù),就都成為了 window

對(duì)象的屬性。來(lái)看下面的例子。

var color = \"red\";

function sayColor(){

alert(window.color);

}

window.sayColor(); //\"red\"

GlobalObjectWindowExample01.htm

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

第152頁(yè)

134 第 5 章 引用類(lèi)型

這里定義了一個(gè)名為color的全局變量和一個(gè)名為sayColor()的全局函數(shù)。在sayColor()內(nèi)部,

我們通過(guò) window.color 來(lái)訪問(wèn) color 變量,以說(shuō)明全局變量是 window 對(duì)象的屬性。然后,又使用

window.sayColor()來(lái)直接通過(guò) window 對(duì)象調(diào)用這個(gè)函數(shù),結(jié)果顯示在了警告框中。

JavaScript中的window對(duì)象除了扮演ECMAScript規(guī)定的Global對(duì)象的角色外,

還承擔(dān)了很多別的任務(wù)。第 8 章在討論瀏覽器對(duì)象模型時(shí)將詳細(xì)介紹 window 對(duì)象。

另一種取得 Global 對(duì)象的方法是使用以下代碼:

var global = function(){

return this;

}();

以上代碼創(chuàng)建了一個(gè)立即調(diào)用的函數(shù)表達(dá)式,返回 this 的值。如前所述,在沒(méi)有給函數(shù)明確指定

this 值的情況下(無(wú)論是通過(guò)將函數(shù)添加為對(duì)象的方法,還是通過(guò)調(diào)用 call()或 apply()),this

值等于 Global 對(duì)象。而像這樣通過(guò)簡(jiǎn)單地返回 this 來(lái)取得 Global 對(duì)象,在任何執(zhí)行環(huán)境下都是可

行的。第 7 章將深入討論函數(shù)表達(dá)式。

5.7.2 Math對(duì)象

ECMAScript 還為保存數(shù)學(xué)公式和信息提供了一個(gè)公共位置,即 Math 對(duì)象。與我們?cè)?JavaScript 直

接編寫(xiě)的計(jì)算功能相比,Math 對(duì)象提供的計(jì)算功能執(zhí)行起來(lái)要快得多。Math 對(duì)象中還提供了輔助完成

這些計(jì)算的屬性和方法。

1. Math 對(duì)象的屬性

Math 對(duì)象包含的屬性大都是數(shù)學(xué)計(jì)算中可能會(huì)用到的一些特殊值。下表列出了這些屬性。

屬 性 說(shuō) 明

Math.E 自然對(duì)數(shù)的底數(shù),即常量e的值

Math.LN10 10的自然對(duì)數(shù)

Math.LN2 2的自然對(duì)數(shù)

Math.LOG2E 以2為底e的對(duì)數(shù)

Math.LOG10E 以10為底e的對(duì)數(shù)

Math.PI π的值

Math.SQRT1_2 1/2的平方根(即2的平方根的倒數(shù))

Math.SQRT2 2的平方根

雖然討論這些值的含義和用途超出了本書(shū)范圍,但你確實(shí)可以隨時(shí)使用它們。

2. min()和 max()方法

Math 對(duì)象還包含許多方法,用于輔助完成簡(jiǎn)單和復(fù)雜的數(shù)學(xué)計(jì)算。

其中,min()和 max()方法用于確定一組數(shù)值中的最小值和最大值。這兩個(gè)方法都可以接收任意多

個(gè)數(shù)值參數(shù),如下面的例子所示。

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

第153頁(yè)

5.7 單體內(nèi)置對(duì)象 135

1

2

3

4

5

13

6

7

8

9

10

11

12

var max = Math.max(3, 54, 32, 16);

alert(max); //54

var min = Math.min(3, 54, 32, 16);

alert(min); //3

MathObjectMinMaxExample01.htm

對(duì)于 3、54、32 和 16,Math.max()返回 54,而 Math.min()返回 3。這兩個(gè)方法經(jīng)常用于避免多

余的循環(huán)和在 if 語(yǔ)句中確定一組數(shù)的最大值。

要找到數(shù)組中的最大或最小值,可以像下面這樣使用 apply()方法。

var values = [1, 2, 3, 4, 5, 6, 7, 8];

var max = Math.max.apply(Math, values);

這個(gè)技巧的關(guān)鍵是把 Math 對(duì)象作為 apply()的第一個(gè)參數(shù),從而正確地設(shè)置 this 值。然后,可

以將任何數(shù)組作為第二個(gè)參數(shù)。

3. 舍入方法

下面來(lái)介紹將小數(shù)值舍入為整數(shù)的幾個(gè)方法:Math.ceil()、Math.floor()和 Math.round()。

這三個(gè)方法分別遵循下列舍入規(guī)則:

? Math.ceil()執(zhí)行向上舍入,即它總是將數(shù)值向上舍入為最接近的整數(shù);

? Math.floor()執(zhí)行向下舍入,即它總是將數(shù)值向下舍入為最接近的整數(shù);

? Math.round()執(zhí)行標(biāo)準(zhǔn)舍入,即它總是將數(shù)值四舍五入為最接近的整數(shù)(這也是我們?cè)跀?shù)學(xué)課

上學(xué)到的舍入規(guī)則)。

下面是使用這些方法的示例:

alert(Math.ceil(25.9)); //26

alert(Math.ceil(25.5)); //26

alert(Math.ceil(25.1)); //26

alert(Math.round(25.9)); //26

alert(Math.round(25.5)); //26

alert(Math.round(25.1)); //25

alert(Math.floor(25.9)); //25

alert(Math.floor(25.5)); //25

alert(Math.floor(25.1)); //25

MathObjectRoundingExample01.htm

對(duì)于所有介于 25 和 26(不包括 26)之間的數(shù)值,Math.ceil()始終返回 26,因?yàn)樗鼒?zhí)行的是向

上舍入。Math.round()方法只在數(shù)值大于等于 25.5 時(shí)返回 26;否則返回 25。最后,Math.floor()

對(duì)所有介于 25 和 26(不包括 26)之間的數(shù)值都返回 25。

4. random()方法

Math.random()方法返回大于等于 0 小于 1 的一個(gè)隨機(jī)數(shù)。對(duì)于某些站點(diǎn)來(lái)說(shuō),這個(gè)方法非常實(shí)用,

因?yàn)榭梢岳盟鼇?lái)隨機(jī)顯示一些名人名言和新聞事件。套用下面的公式,就可以利用 Math.random()

從某個(gè)整數(shù)范圍內(nèi)隨機(jī)選擇一個(gè)值。

值 = Math.floor(Math.random() * 可能值的總數(shù) + 第一個(gè)可能的值)

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

第154頁(yè)

136 第 5 章 引用類(lèi)型

公式中用到了 Math.floor()方法,這是因?yàn)?Math.random()總返回一個(gè)小數(shù)值。而用這個(gè)小數(shù)

值乘以一個(gè)整數(shù),然后再加上一個(gè)整數(shù),最終結(jié)果仍然還是一個(gè)小數(shù)。舉例來(lái)說(shuō),如果你想選擇一個(gè) 1

到 10 之間的數(shù)值,可以像下面這樣編寫(xiě)代碼:

var num = Math.floor(Math.random() * 10 + 1);

MathObjectRandomExample01.htm

總共有 10 個(gè)可能的值(1 到 10),而第一個(gè)可能的值是 1。而如果想要選擇一個(gè)介于 2 到 10 之間的

值,就應(yīng)該將上面的代碼改成這樣:

var num = Math.floor(Math.random() * 9 + 2);

MathObjectRandomExample02.htm

從 2 數(shù)到 10 要數(shù) 9 個(gè)數(shù),因此可能值的總數(shù)就是 9,而第一個(gè)可能的值就是 2。多數(shù)情況下,其實(shí)

都可以通過(guò)一個(gè)函數(shù)來(lái)計(jì)算可能值的總數(shù)和第一個(gè)可能的值,例如:

function selectFrom(lowerValue, upperValue) {

var choices = upperValue - lowerValue + 1;

return Math.floor(Math.random() * choices + lowerValue);

}

var num = selectFrom(2, 10);

alert(num); // 介于 2 和 10 之間(包括 2 和 10)的一個(gè)數(shù)值

MathObjectRandomExample03.htm

函數(shù) selectFrom()接受兩個(gè)參數(shù):應(yīng)該返回的最小值和最大值。而用最大值減最小值再加 1 得到

了可能值的總數(shù),然后它又把這些數(shù)值套用到了前面的公式中。這樣,通過(guò)調(diào)用 selectFrom(2,10)

就可以得到一個(gè)介于 2 和 10 之間(包括 2 和 10)的數(shù)值了。利用這個(gè)函數(shù),可以方便地從數(shù)組中隨機(jī)

取出一項(xiàng),例如:

var colors = [\"red\", \"green\", \"blue\", \"yellow\", \"black\", \"purple\", \"brown\"];

var color = colors[selectFrom(0, colors.length-1)];

alert(color); // 可能是數(shù)組中包含的任何一個(gè)字符串

MathObjectRandomExample03.htm

在這個(gè)例子中,傳遞給 selectFrom()的第二個(gè)參數(shù)是數(shù)組的長(zhǎng)度減 1,也就是數(shù)組中最后一項(xiàng)的位置。

5. 其他方法

Math 對(duì)象中還包含其他一些與完成各種簡(jiǎn)單或復(fù)雜計(jì)算有關(guān)的方法,但詳細(xì)討論其中每一個(gè)方法

的細(xì)節(jié)及適用情形超出了本書(shū)的范圍。下面我們就給出一個(gè)表格,其中列出了這些沒(méi)有介紹到的 Math

對(duì)象的方法。

方 法 說(shuō) 明 方 法 說(shuō) 明

Math.abs(num) 返回num 的絕對(duì)值 Math.asin(x) 返回x 的反正弦值

Math.exp(num) 返回Math.E 的num 次冪 Math.atan(x) 返回x 的反正切值

Math.log(num) 返回num 的自然對(duì)數(shù) Math.atan2(y,x) 返回y/x 的反正切值

Math.pow(num,power) 返回num 的power 次冪 Math.cos(x) 返回x 的余弦值

Math.sqrt(num) 返回num 的平方根 Math.sin(x) 返回x 的正弦值

Math.acos(x) 返回x 的反余弦值 Math.tan(x) 返回x 的正切值

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

第155頁(yè)

5.8 小結(jié) 137

1

2

3

4

5

13

6

7

8

9

10

11

12

雖然 ECMA-262 規(guī)定了這些方法,但不同實(shí)現(xiàn)可能會(huì)對(duì)這些方法采用不同的算法。畢竟,計(jì)算某個(gè)

值的正弦、余弦和正切的方式多種多樣。也正因?yàn)槿绱?,這些方法在不同的實(shí)現(xiàn)中可能會(huì)有不同的精度。

5.8 小結(jié)

對(duì)象在 JavaScript 中被稱為引用類(lèi)型的值,而且有一些內(nèi)置的引用類(lèi)型可以用來(lái)創(chuàng)建特定的對(duì)象,

現(xiàn)簡(jiǎn)要總結(jié)如下:

? 引用類(lèi)型與傳統(tǒng)面向?qū)ο蟪绦蛟O(shè)計(jì)中的類(lèi)相似,但實(shí)現(xiàn)不同;

? Object 是一個(gè)基礎(chǔ)類(lèi)型,其他所有類(lèi)型都從 Object 繼承了基本的行為;

? Array 類(lèi)型是一組值的有序列表,同時(shí)還提供了操作和轉(zhuǎn)換這些值的功能;

? Date 類(lèi)型提供了有關(guān)日期和時(shí)間的信息,包括當(dāng)前日期和時(shí)間以及相關(guān)的計(jì)算功能;

? RegExp 類(lèi)型是 ECMAScript 支持正則表達(dá)式的一個(gè)接口,提供了最基本的和一些高級(jí)的正則表

達(dá)式功能。

函數(shù)實(shí)際上是 Function 類(lèi)型的實(shí)例,因此函數(shù)也是對(duì)象;而這一點(diǎn)正是 JavaScript 最有特色的地

方。由于函數(shù)是對(duì)象,所以函數(shù)也擁有方法,可以用來(lái)增強(qiáng)其行為。

因?yàn)橛辛嘶景b類(lèi)型,所以 JavaScript 中的基本類(lèi)型值可以被當(dāng)作對(duì)象來(lái)訪問(wèn)。三種基本包裝類(lèi)

型分別是:Boolean、Number 和 String。以下是它們共同的特征:

? 每個(gè)包裝類(lèi)型都映射到同名的基本類(lèi)型;

? 在讀取模式下訪問(wèn)基本類(lèi)型值時(shí),就會(huì)創(chuàng)建對(duì)應(yīng)的基本包裝類(lèi)型的一個(gè)對(duì)象,從而方便了數(shù)據(jù)

操作;

? 操作基本類(lèi)型值的語(yǔ)句一經(jīng)執(zhí)行完畢,就會(huì)立即銷(xiāo)毀新創(chuàng)建的包裝對(duì)象。

在所有代碼執(zhí)行之前,作用域中就已經(jīng)存在兩個(gè)內(nèi)置對(duì)象:Global 和 Math。在大多數(shù) ECMAScript

實(shí)現(xiàn)中都不能直接訪問(wèn) Global 對(duì)象;不過(guò),Web 瀏覽器實(shí)現(xiàn)了承擔(dān)該角色的 window 對(duì)象。全局變

量和函數(shù)都是 Global 對(duì)象的屬性。Math 對(duì)象提供了很多屬性和方法,用于輔助完成復(fù)雜的數(shù)學(xué)計(jì)算

任務(wù)。

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

第156頁(yè)

138 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

面向?qū)ο蟮某绦蛟O(shè)計(jì)

本章內(nèi)容

? 理解對(duì)象屬性

? 理解并創(chuàng)建對(duì)象

? 理解繼承

向?qū)ο螅∣bject-Oriented,OO)的語(yǔ)言有一個(gè)標(biāo)志,那就是它們都有類(lèi)的概念,而通過(guò)類(lèi)可

以創(chuàng)建任意多個(gè)具有相同屬性和方法的對(duì)象。前面提到過(guò),ECMAScript 中沒(méi)有類(lèi)的概念,因

此它的對(duì)象也與基于類(lèi)的語(yǔ)言中的對(duì)象有所不同。

ECMA-262 把對(duì)象定義為:“無(wú)序?qū)傩缘募?,其屬性可以包含基本值、?duì)象或者函數(shù)?!眹?yán)格來(lái)講,

這就相當(dāng)于說(shuō)對(duì)象是一組沒(méi)有特定順序的值。對(duì)象的每個(gè)屬性或方法都有一個(gè)名字,而每個(gè)名字都映射

到一個(gè)值。正因?yàn)檫@樣(以及其他將要討論的原因),我們可以把 ECMAScript 的對(duì)象想象成散列表:無(wú)

非就是一組名值對(duì),其中值可以是數(shù)據(jù)或函數(shù)。

每個(gè)對(duì)象都是基于一個(gè)引用類(lèi)型創(chuàng)建的,這個(gè)引用類(lèi)型可以是第 5 章討論的原生類(lèi)型,也可以是開(kāi)

發(fā)人員定義的類(lèi)型。

6.1 理解對(duì)象

上一章曾經(jīng)介紹過(guò),創(chuàng)建自定義對(duì)象的最簡(jiǎn)單方式就是創(chuàng)建一個(gè) Object 的實(shí)例,然后再為它添加

屬性和方法,如下所示。

var person = new Object();

person.name = \"Nicholas\";

person.age = 29;

person.job = \"Software Engineer\";

person.sayName = function(){

alert(this.name);

};

CreatingObjectsExample01.htm

上面的例子創(chuàng)建了一個(gè)名為 person 的對(duì)象,并為它添加了三個(gè)屬性(name、age 和 job)和一個(gè)

方法(sayName())。其中,sayName()方法用于顯示 this.name(將被解析為 person.name)的值。

早期的 JavaScript 開(kāi)發(fā)人員經(jīng)常使用這個(gè)模式創(chuàng)建新對(duì)象。幾年后,對(duì)象字面量成為創(chuàng)建這種對(duì)象的首選

模式。前面的例子用對(duì)象字面量語(yǔ)法可以寫(xiě)成這樣:

第 6 章

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

第157頁(yè)

6.1 理解對(duì)象 139

1

2

3

4

5

13

6

7

8

9

10

11

12

var person = {

name: \"Nicholas\",

age: 29,

job: \"Software Engineer\",

sayName: function(){

alert(this.name);

}

};

這個(gè)例子中的 person 對(duì)象與前面例子中的 person 對(duì)象是一樣的,都有相同的屬性和方法。這些

屬性在創(chuàng)建時(shí)都帶有一些特征值(characteristic),JavaScript 通過(guò)這些特征值來(lái)定義它們的行為。

6.1.1 屬性類(lèi)型

ECMA-262 第 5 版在定義只有內(nèi)部才用的特性(attribute)時(shí),描述了屬性(property)的各種特征。

ECMA-262 定義這些特性是為了實(shí)現(xiàn) JavaScript 引擎用的,因此在 JavaScript 中不能直接訪問(wèn)它們。為了

表示特性是內(nèi)部值,該規(guī)范把它們放在了兩對(duì)兒方括號(hào)中,例如[[Enumerable]]。盡管 ECMA-262

第 3 版的定義有些不同,但本書(shū)只參考第 5 版的描述。

ECMAScript 中有兩種屬性:數(shù)據(jù)屬性和訪問(wèn)器屬性。

1. 數(shù)據(jù)屬性

數(shù)據(jù)屬性包含一個(gè)數(shù)據(jù)值的位置。在這個(gè)位置可以讀取和寫(xiě)入值。數(shù)據(jù)屬性有 4 個(gè)描述其行為的

特性。

? [[Configurable]]:表示能否通過(guò) delete 刪除屬性從而重新定義屬性,能否修改屬性的特

性,或者能否把屬性修改為訪問(wèn)器屬性。像前面例子中那樣直接在對(duì)象上定義的屬性,它們的

這個(gè)特性默認(rèn)值為 true。

? [[Enumerable]]:表示能否通過(guò) for-in 循環(huán)返回屬性。像前面例子中那樣直接在對(duì)象上定

義的屬性,它們的這個(gè)特性默認(rèn)值為 true。

? [[Writable]]:表示能否修改屬性的值。像前面例子中那樣直接在對(duì)象上定義的屬性,它們的

這個(gè)特性默認(rèn)值為 true。

? [[Value]]:包含這個(gè)屬性的數(shù)據(jù)值。讀取屬性值的時(shí)候,從這個(gè)位置讀;寫(xiě)入屬性值的時(shí)候,

把新值保存在這個(gè)位置。這個(gè)特性的默認(rèn)值為 undefined。

對(duì)于像前面例子中那樣直接在對(duì)象上定義的屬性,它們的[[Configurable]]、[[Enumerable]]

和[[Writable]]特性都被設(shè)置為 true,而[[Value]]特性被設(shè)置為指定的值。例如:

var person = {

name: \"Nicholas\"

};

這里創(chuàng)建了一個(gè)名為 name 的屬性,為它指定的值是\"Nicholas\"。也就是說(shuō),[[Value]]特性將

被設(shè)置為\"Nicholas\",而對(duì)這個(gè)值的任何修改都將反映在這個(gè)位置。

要修改屬性默認(rèn)的特性,必須使用 ECMAScript 5 的 Object.defineProperty()方法。這個(gè)方法

接收三個(gè)參數(shù):屬性所在的對(duì)象、屬性的名字和一個(gè)描述符對(duì)象。其中,描述符(descriptor)對(duì)象的屬

性必須是:configurable、enumerable、writable 和 value。設(shè)置其中的一或多個(gè)值,可以修改

對(duì)應(yīng)的特性值。例如:

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

第158頁(yè)

140 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

var person = {};

Object.defineProperty(person, \"name\", {

writable: false,

value: \"Nicholas\"

});

alert(person.name); //\"Nicholas\"

person.name = \"Greg\";

alert(person.name); //\"Nicholas\"

DataPropertiesExample01.htm

這個(gè)例子創(chuàng)建了一個(gè)名為 name 的屬性,它的值\"Nicholas\"是只讀的。這個(gè)屬性的值是不可修改

的,如果嘗試為它指定新值,則在非嚴(yán)格模式下,賦值操作將被忽略;在嚴(yán)格模式下,賦值操作將會(huì)導(dǎo)

致拋出錯(cuò)誤。

類(lèi)似的規(guī)則也適用于不可配置的屬性。例如:

var person = {};

Object.defineProperty(person, \"name\", {

configurable: false,

value: \"Nicholas\"

});

alert(person.name); //\"Nicholas\"

delete person.name;

alert(person.name); //\"Nicholas\"

DataPropertiesExample02.htm

把 configurable 設(shè)置為 false,表示不能從對(duì)象中刪除屬性。如果對(duì)這個(gè)屬性調(diào)用 delete,則

在非嚴(yán)格模式下什么也不會(huì)發(fā)生,而在嚴(yán)格模式下會(huì)導(dǎo)致錯(cuò)誤。而且,一旦把屬性定義為不可配置的,

就不能再把它變回可配置了。此時(shí),再調(diào)用 Object.defineProperty()方法修改除 writable 之外

的特性,都會(huì)導(dǎo)致錯(cuò)誤:

var person = {};

Object.defineProperty(person, \"name\", {

configurable: false,

value: \"Nicholas\"

});

//拋出錯(cuò)誤

Object.defineProperty(person, \"name\", {

configurable: true,

value: \"Nicholas\"

});

DataPropertiesExample03.htm

也就是說(shuō),可以多次調(diào)用 Object.defineProperty()方法修改同一個(gè)屬性,但在把 configurable

特性設(shè)置為 false 之后就會(huì)有限制了。

在調(diào)用 Object.defineProperty()方法時(shí),如果不指定,configurable、enumerable 和

writable 特性的默認(rèn)值都是 false。多數(shù)情況下,可能都沒(méi)有必要利用 Object.defineProperty()

方法提供的這些高級(jí)功能。不過(guò),理解這些概念對(duì)理解 JavaScript 對(duì)象卻非常有用。

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

第159頁(yè)

6.1 理解對(duì)象 141

1

2

3

4

5

13

6

7

8

9

10

11

12

IE8 是第一個(gè)實(shí)現(xiàn) Object.defineProperty()方法的瀏覽器版本。然而,這個(gè)

版本的實(shí)現(xiàn)存在諸多限制:只能在 DOM 對(duì)象上使用這個(gè)方法,而且只能創(chuàng)建訪問(wèn)器

屬性。由于實(shí)現(xiàn)不徹底,建議讀者不要在 IE8 中使用 Object.defineProperty()

方法。

2. 訪問(wèn)器屬性

訪問(wèn)器屬性不包含數(shù)據(jù)值;它們包含一對(duì)兒 getter 和 setter 函數(shù)(不過(guò),這兩個(gè)函數(shù)都不是必需的)。

在讀取訪問(wèn)器屬性時(shí),會(huì)調(diào)用 getter 函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;在寫(xiě)入訪問(wèn)器屬性時(shí),會(huì)調(diào)用

setter 函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。訪問(wèn)器屬性有如下 4 個(gè)特性。

? [[Configurable]]:表示能否通過(guò) delete 刪除屬性從而重新定義屬性,能否修改屬性的特

性,或者能否把屬性修改為數(shù)據(jù)屬性。對(duì)于直接在對(duì)象上定義的屬性,這個(gè)特性的默認(rèn)值為

true。

? [[Enumerable]]:表示能否通過(guò) for-in 循環(huán)返回屬性。對(duì)于直接在對(duì)象上定義的屬性,這

個(gè)特性的默認(rèn)值為 true。

? [[Get]]:在讀取屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為 undefined。

? [[Set]]:在寫(xiě)入屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為 undefined。

訪問(wèn)器屬性不能直接定義,必須使用 Object.defineProperty()來(lái)定義。請(qǐng)看下面的例子。

var book = {

_year: 2004,

edition: 1

};

Object.defineProperty(book, \"year\", {

get: function(){

return this._year;

},

set: function(newValue){

if (newValue > 2004) {

this._year = newValue;

this.edition += newValue - 2004;

}

}

});

book.year = 2005;

alert(book.edition); //2

AccessorPropertiesExample01.htm

以上代碼創(chuàng)建了一個(gè) book 對(duì)象,并給它定義兩個(gè)默認(rèn)的屬性:_year 和 edition。_year 前面

的下劃線是一種常用的記號(hào),用于表示只能通過(guò)對(duì)象方法訪問(wèn)的屬性。而訪問(wèn)器屬性 year 則包含一個(gè)

getter 函數(shù)和一個(gè) setter 函數(shù)。getter 函數(shù)返回_year 的值,setter 函數(shù)通過(guò)計(jì)算來(lái)確定正確的版本。因此,

把 year 屬性修改為 2005 會(huì)導(dǎo)致_year 變成 2005,而 edition 變?yōu)?2。這是使用訪問(wèn)器屬性的常見(jiàn)方

式,即設(shè)置一個(gè)屬性的值會(huì)導(dǎo)致其他屬性發(fā)生變化。

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

第160頁(yè)

142 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

不一定非要同時(shí)指定 getter 和 setter。只指定 getter 意味著屬性是不能寫(xiě),嘗試寫(xiě)入屬性會(huì)被忽略。

在嚴(yán)格模式下,嘗試寫(xiě)入只指定了 getter 函數(shù)的屬性會(huì)拋出錯(cuò)誤。類(lèi)似地,只指定 setter 函數(shù)的屬性也

不能讀,否則在非嚴(yán)格模式下會(huì)返回 undefined,而在嚴(yán)格模式下會(huì)拋出錯(cuò)誤。

支持 ECMAScript 5 的這個(gè)方法的瀏覽器有 IE9+(IE8 只是部分實(shí)現(xiàn))、Firefox 4+、Safari 5+、Opera

12+ 和 Chrome 。在這個(gè)方法之前,要?jiǎng)?chuàng)建訪問(wèn)器屬性,一般都使用兩個(gè)非標(biāo)準(zhǔn)的方法:

__defineGetter__()和__defineSetter__()。這兩個(gè)方法最初是由 Firefox 引入的,后來(lái) Safari 3、

Chrome 1 和 Opera 9.5 也給出了相同的實(shí)現(xiàn)。使用這兩個(gè)遺留的方法,可以像下面這樣重寫(xiě)前面的例子。

var book = {

_year: 2004,

edition: 1

};

//定義訪問(wèn)器的舊有方法

book.__defineGetter__(\"year\", function(){

return this._year;

});

book.__defineSetter__(\"year\", function(newValue){

if (newValue > 2004) {

this._year = newValue;

this.edition += newValue - 2004;

}

});

book.year = 2005;

alert(book.edition); //2

AccessorPropertiesExample02.htm

在不支持 Object.defineProperty() 方法的瀏覽器中不能修改 [[Configurable]] 和

[[Enumerable]]。

6.1.2 定義多個(gè)屬性

由于為對(duì)象定義多個(gè)屬性的可能性很大,ECMAScript 5 又定義了一個(gè) Object.defineProperties()方法。利用這個(gè)方法可以通過(guò)描述符一次定義多個(gè)屬性。這個(gè)方法接收兩個(gè)對(duì)象參數(shù):第一

個(gè)對(duì)象是要添加和修改其屬性的對(duì)象,第二個(gè)對(duì)象的屬性與第一個(gè)對(duì)象中要添加或修改的屬性一一對(duì)

應(yīng)。例如:

var book = {};

Object.defineProperties(book, {

_year: {

value: 2004

},

edition: {

value: 1

},

year: {

get: function(){

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

第161頁(yè)

6.1 理解對(duì)象 143

1

2

3

4

5

13

6

7

8

9

10

11

12

return this._year;

},

set: function(newValue){

if (newValue > 2004) {

this._year = newValue;

this.edition += newValue - 2004;

}

}

}

});

MultiplePropertiesExample01.htm

以上代碼在 book 對(duì)象上定義了兩個(gè)數(shù)據(jù)屬性(_year 和 edition)和一個(gè)訪問(wèn)器屬性(year)。

最終的對(duì)象與上一節(jié)中定義的對(duì)象相同。唯一的區(qū)別是這里的屬性都是在同一時(shí)間創(chuàng)建的。

支持 Object.defineProperties()方法的瀏覽器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和

Chrome。

6.1.3 讀取屬性的特性

使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述

符。這個(gè)方法接收兩個(gè)參數(shù):屬性所在的對(duì)象和要讀取其描述符的屬性名稱。返回值是一個(gè)對(duì)象,如果

是訪問(wèn)器屬性,這個(gè)對(duì)象的屬性有 configurable、enumerable、get 和 set;如果是數(shù)據(jù)屬性,這

個(gè)對(duì)象的屬性有 configurable、enumerable、writable 和 value。例如:

var book = {};

Object.defineProperties(book, {

_year: {

value: 2004

},

edition: {

value: 1

},

year: {

get: function(){

return this._year;

},

set: function(newValue){

if (newValue > 2004) {

this._year = newValue;

this.edition += newValue - 2004;

}

}

}

});

var descriptor = Object.getOwnPropertyDescriptor(book, \"_year\");

alert(descriptor.value); //2004

alert(descriptor.configurable); //false

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

第162頁(yè)

144 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

alert(typeof descriptor.get); //\"undefined\"

var descriptor = Object.getOwnPropertyDescriptor(book, \"year\");

alert(descriptor.value); //undefined

alert(descriptor.enumerable); //false

alert(typeof descriptor.get); //\"function\"

GetPropertyDescriptorExample01.htm

對(duì)于數(shù)據(jù)屬性_year,value 等于最初的值,configurable 是 false,而 get 等于 undefined。

對(duì)于訪問(wèn)器屬性 year,value 等于 undefined,enumerable 是 false,而 get 是一個(gè)指向 getter

函數(shù)的指針。

在 JavaScript 中,可以針對(duì)任何對(duì)象——包括 DOM 和 BOM 對(duì)象,使用 Object.getOwnPropertyDescriptor()方法。支持這個(gè)方法的瀏覽器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。

6.2 創(chuàng)建對(duì)象

雖然 Object 構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕?lái)創(chuàng)建單個(gè)對(duì)象,但這些方式有個(gè)明顯的缺點(diǎn):使用同

一個(gè)接口創(chuàng)建很多對(duì)象,會(huì)產(chǎn)生大量的重復(fù)代碼。為解決這個(gè)問(wèn)題,人們開(kāi)始使用工廠模式的一種變體。

6.2.1 工廠模式

工廠模式是軟件工程領(lǐng)域一種廣為人知的設(shè)計(jì)模式,這種模式抽象了創(chuàng)建具體對(duì)象的過(guò)程(本書(shū)后

面還將討論其他設(shè)計(jì)模式及其在 JavaScript 中的實(shí)現(xiàn))??紤]到在 ECMAScript 中無(wú)法創(chuàng)建類(lèi),開(kāi)發(fā)人員

就發(fā)明了一種函數(shù),用函數(shù)來(lái)封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié),如下面的例子所示。

function createPerson(name, age, job){

var o = new Object();

o.name = name;

o.age = age;

o.job = job;

o.sayName = function(){

alert(this.name);

};

return o;

}

var person1 = createPerson(\"Nicholas\", 29, \"Software Engineer\");

var person2 = createPerson(\"Greg\", 27, \"Doctor\");

FactoryPatternExample01.htm

函數(shù) createPerson()能夠根據(jù)接受的參數(shù)來(lái)構(gòu)建一個(gè)包含所有必要信息的 Person 對(duì)象??梢詿o(wú)

數(shù)次地調(diào)用這個(gè)函數(shù),而每次它都會(huì)返回一個(gè)包含三個(gè)屬性一個(gè)方法的對(duì)象。工廠模式雖然解決了創(chuàng)建

多個(gè)相似對(duì)象的問(wèn)題,但卻沒(méi)有解決對(duì)象識(shí)別的問(wèn)題(即怎樣知道一個(gè)對(duì)象的類(lèi)型)。隨著 JavaScript

的發(fā)展,又一個(gè)新模式出現(xiàn)了。

6.2.2 構(gòu)造函數(shù)模式

前幾章介紹過(guò),ECMAScript 中的構(gòu)造函數(shù)可用來(lái)創(chuàng)建特定類(lèi)型的對(duì)象。像 Object 和 Array 這樣

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

第163頁(yè)

6.2 創(chuàng)建對(duì)象 145

1

2

3

4

5

13

6

7

8

9

10

11

12

的原生構(gòu)造函數(shù),在運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中。此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù),從而定義

自定義對(duì)象類(lèi)型的屬性和方法。例如,可以使用構(gòu)造函數(shù)模式將前面的例子重寫(xiě)如下。

function Person(name, age, job){

this.name = name;

this.age = age;

this.job = job;

this.sayName = function(){

alert(this.name);

};

}

var person1 = new Person(\"Nicholas\", 29, \"Software Engineer\");

var person2 = new Person(\"Greg\", 27, \"Doctor\");

ConstructorPatternExample01.htm

在這個(gè)例子中,Person()函數(shù)取代了 createPerson()函數(shù)。我們注意到,Person()中的代碼

除了與 createPerson()中相同的部分外,還存在以下不同之處:

? 沒(méi)有顯式地創(chuàng)建對(duì)象;

? 直接將屬性和方法賦給了 this 對(duì)象;

? 沒(méi)有 return 語(yǔ)句。

此外,還應(yīng)該注意到函數(shù)名 Person 使用的是大寫(xiě)字母 P。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個(gè)

大寫(xiě)字母開(kāi)頭,而非構(gòu)造函數(shù)則應(yīng)該以一個(gè)小寫(xiě)字母開(kāi)頭。這個(gè)做法借鑒自其他 OO 語(yǔ)言,主要是為了

區(qū)別于 ECMAScript 中的其他函數(shù);因?yàn)闃?gòu)造函數(shù)本身也是函數(shù),只不過(guò)可以用來(lái)創(chuàng)建對(duì)象而已。

要?jiǎng)?chuàng)建 Person 的新實(shí)例,必須使用 new 操作符。以這種方式調(diào)用構(gòu)造函數(shù)實(shí)際上會(huì)經(jīng)歷以下 4

個(gè)步驟:

(1) 創(chuàng)建一個(gè)新對(duì)象;

(2) 將構(gòu)造函數(shù)的作用域賦給新對(duì)象(因此 this 就指向了這個(gè)新對(duì)象);

(3) 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性);

(4) 返回新對(duì)象。

在前面例子的最后,person1 和 person2 分別保存著 Person 的一個(gè)不同的實(shí)例。這兩個(gè)對(duì)象都

有一個(gè) constructor(構(gòu)造函數(shù))屬性,該屬性指向 Person,如下所示。

alert(person1.constructor == Person); //true

alert(person2.constructor == Person); //true

對(duì)象的 constructor 屬性最初是用來(lái)標(biāo)識(shí)對(duì)象類(lèi)型的。但是,提到檢測(cè)對(duì)象類(lèi)型,還是 instanceof 操作符要更可靠一些。我們?cè)谶@個(gè)例子中創(chuàng)建的所有對(duì)象既是 Object 的實(shí)例,同時(shí)也是 Person

的實(shí)例,這一點(diǎn)通過(guò) instanceof 操作符可以得到驗(yàn)證。

alert(person1 instanceof Object); //true

alert(person1 instanceof Person); //true

alert(person2 instanceof Object); //true

alert(person2 instanceof Person); //true

創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來(lái)可以將它的實(shí)例標(biāo)識(shí)為一種特定的類(lèi)型;而這正是構(gòu)造函數(shù)模式

勝過(guò)工廠模式的地方。在這個(gè)例子中,person1 和 person2 之所以同時(shí)是 Object 的實(shí)例,是因?yàn)樗?/p>

有對(duì)象均繼承自 Object(詳細(xì)內(nèi)容稍后討論)。

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

第164頁(yè)

146 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

以這種方式定義的構(gòu)造函數(shù)是定義在 Global 對(duì)象(在瀏覽器中是 window 對(duì)象)

中的。第 8 章將詳細(xì)討論瀏覽器對(duì)象模型(BOM)。

1. 將構(gòu)造函數(shù)當(dāng)作函數(shù)

構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別,就在于調(diào)用它們的方式不同。不過(guò),構(gòu)造函數(shù)畢竟也是函數(shù),不

存在定義構(gòu)造函數(shù)的特殊語(yǔ)法。任何函數(shù),只要通過(guò) new 操作符來(lái)調(diào)用,那它就可以作為構(gòu)造函數(shù);而

任何函數(shù),如果不通過(guò) new 操作符來(lái)調(diào)用,那它跟普通函數(shù)也不會(huì)有什么兩樣。例如,前面例子中定義

的 Person()函數(shù)可以通過(guò)下列任何一種方式來(lái)調(diào)用。

// 當(dāng)作構(gòu)造函數(shù)使用

var person = new Person(\"Nicholas\", 29, \"Software Engineer\");

person.sayName(); //\"Nicholas\"

// 作為普通函數(shù)調(diào)用

Person(\"Greg\", 27, \"Doctor\"); // 添加到 window

window.sayName(); //\"Greg\"

// 在另一個(gè)對(duì)象的作用域中調(diào)用

var o = new Object();

Person.call(o, \"Kristen\", 25, \"Nurse\");

o.sayName(); //\"Kristen\"

ConstructorPatternExample02.htm

這個(gè)例子中的前兩行代碼展示了構(gòu)造函數(shù)的典型用法,即使用 new 操作符來(lái)創(chuàng)建一個(gè)新對(duì)象。接下

來(lái)的兩行代碼展示了不使用new操作符調(diào)用Person()會(huì)出現(xiàn)什么結(jié)果:屬性和方法都被添加給window

對(duì)象了。有讀者可能還記得,當(dāng)在全局作用域中調(diào)用一個(gè)函數(shù)時(shí),this 對(duì)象總是指向 Global 對(duì)象(在

瀏覽器中就是 window 對(duì)象)。因此,在調(diào)用完函數(shù)之后,可以通過(guò) window 對(duì)象來(lái)調(diào)用 sayName()方

法,并且還返回了\"Greg\"。最后,也可以使用 call()(或者 apply())在某個(gè)特殊對(duì)象的作用域中

調(diào)用Person()函數(shù)。這里是在對(duì)象o 的作用域中調(diào)用的,因此調(diào)用后o 就擁有了所有屬性和sayName()

方法。

2. 構(gòu)造函數(shù)的問(wèn)題

構(gòu)造函數(shù)模式雖然好用,但也并非沒(méi)有缺點(diǎn)。使用構(gòu)造函數(shù)的主要問(wèn)題,就是每個(gè)方法都要在每個(gè)

實(shí)例上重新創(chuàng)建一遍。在前面的例子中,person1 和 person2 都有一個(gè)名為 sayName()的方法,但那

兩個(gè)方法不是同一個(gè) Function 的實(shí)例。不要忘了——ECMAScript 中的函數(shù)是對(duì)象,因此每定義一個(gè)

函數(shù),也就是實(shí)例化了一個(gè)對(duì)象。從邏輯角度講,此時(shí)的構(gòu)造函數(shù)也可以這樣定義。

function Person(name, age, job){

this.name = name;

this.age = age;

this.job = job;

this.sayName = new Function(\"alert(this.name)\"); // 與聲明函數(shù)在邏輯上是等價(jià)的

}

從這個(gè)角度上來(lái)看構(gòu)造函數(shù),更容易明白每個(gè) Person 實(shí)例都包含一個(gè)不同的 Function 實(shí)例(以

顯示 name 屬性)的本質(zhì)。說(shuō)明白些,以這種方式創(chuàng)建函數(shù),會(huì)導(dǎo)致不同的作用域鏈和標(biāo)識(shí)符解析,但

創(chuàng)建 Function 新實(shí)例的機(jī)制仍然是相同的。因此,不同實(shí)例上的同名函數(shù)是不相等的,以下代碼可以

證明這一點(diǎn)。

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

第165頁(yè)

6.2 創(chuàng)建對(duì)象 147

1

2

3

4

5

13

6

7

8

9

10

11

12

alert(person1.sayName == person2.sayName); //false

然而,創(chuàng)建兩個(gè)完成同樣任務(wù)的 Function 實(shí)例的確沒(méi)有必要;況且有 this 對(duì)象在,根本不用在

執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面。因此,大可像下面這樣,通過(guò)把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外

部來(lái)解決這個(gè)問(wèn)題。

function Person(name, age, job){

this.name = name;

this.age = age;

this.job = job;

this.sayName = sayName;

}

function sayName(){

alert(this.name);

}

var person1 = new Person(\"Nicholas\", 29, \"Software Engineer\");

var person2 = new Person(\"Greg\", 27, \"Doctor\");

ConstructorPatternExample03.htm

在這個(gè)例子中,我們把 sayName()函數(shù)的定義轉(zhuǎn)移到了構(gòu)造函數(shù)外部。而在構(gòu)造函數(shù)內(nèi)部,我們

將 sayName 屬性設(shè)置成等于全局的 sayName 函數(shù)。這樣一來(lái),由于 sayName 包含的是一個(gè)指向函數(shù)

的指針,因此 person1 和 person2 對(duì)象就共享了在全局作用域中定義的同一個(gè) sayName()函數(shù)。這

樣做確實(shí)解決了兩個(gè)函數(shù)做同一件事的問(wèn)題,可是新問(wèn)題又來(lái)了:在全局作用域中定義的函數(shù)實(shí)際上只

能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。而更讓人無(wú)法接受的是:如果對(duì)象需要定義很多方

法,那么就要定義很多個(gè)全局函數(shù),于是我們這個(gè)自定義的引用類(lèi)型就絲毫沒(méi)有封裝性可言了。好在,

這些問(wèn)題可以通過(guò)使用原型模式來(lái)解決。

6.2.3 原型模式

我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè) prototype(原型)屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,

而這個(gè)對(duì)象的用途是包含可以由特定類(lèi)型的所有實(shí)例共享的屬性和方法。如果按照字面意思來(lái)理解,那

么 prototype 就是通過(guò)調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)對(duì)象實(shí)例的原型對(duì)象。使用原型對(duì)象的好處是可以

讓所有對(duì)象實(shí)例共享它所包含的屬性和方法。換句話說(shuō),不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息,而是

可以將這些信息直接添加到原型對(duì)象中,如下面的例子所示。

function Person(){

}

Person.prototype.name = \"Nicholas\";

Person.prototype.age = 29;

Person.prototype.job = \"Software Engineer\";

Person.prototype.sayName = function(){

alert(this.name);

};

var person1 = new Person();

person1.sayName(); //\"Nicholas\"

var person2 = new Person();

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

第166頁(yè)

148 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

person2.sayName(); //\"Nicholas\"

alert(person1.sayName == person2.sayName); //true

PrototypePatternExample01.htm

在此,我們將 sayName()方法和所有屬性直接添加到了 Person 的 prototype 屬性中,構(gòu)造函數(shù)

變成了空函數(shù)。即使如此,也仍然可以通過(guò)調(diào)用構(gòu)造函數(shù)來(lái)創(chuàng)建新對(duì)象,而且新對(duì)象還會(huì)具有相同的屬

性和方法。但與構(gòu)造函數(shù)模式不同的是,新對(duì)象的這些屬性和方法是由所有實(shí)例共享的。換句話說(shuō),

person1 和 person2 訪問(wèn)的都是同一組屬性和同一個(gè) sayName()函數(shù)。要理解原型模式的工作原理,

必須先理解 ECMAScript 中原型對(duì)象的性質(zhì)。

1. 理解原型對(duì)象

無(wú)論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè) prototype

屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象。在默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè) constructor

(構(gòu)造函數(shù))屬性,這個(gè)屬性包含一個(gè)指向 prototype 屬性所在函數(shù)的指針。就拿前面的例子來(lái)說(shuō),

Person.prototype. constructor 指向 Person。而通過(guò)這個(gè)構(gòu)造函數(shù),我們還可繼續(xù)為原型對(duì)象

添加其他屬性和方法。

創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型對(duì)象默認(rèn)只會(huì)取得 constructor 屬性;至于其他方法,則

都是從 Object 繼承而來(lái)的。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針(內(nèi)部

屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMA-262 第 5 版中管這個(gè)指針叫[[Prototype]]。雖然在腳本中

沒(méi)有標(biāo)準(zhǔn)的方式訪問(wèn)[[Prototype]],但 Firefox、Safari 和 Chrome 在每個(gè)對(duì)象上都支持一個(gè)屬性

__proto__;而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見(jiàn)的。不過(guò),要明確的真正重要的一點(diǎn)就

是,這個(gè)連接存在于實(shí)例與構(gòu)造函數(shù)的原型對(duì)象之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間。

以前面使用 Person 構(gòu)造函數(shù)和 Person.prototype 創(chuàng)建實(shí)例的代碼為例,圖 6-1 展示了各個(gè)對(duì)

象之間的關(guān)系。

圖 6-1

圖 6-1 展示了 Person 構(gòu)造函數(shù)、Person 的原型屬性以及 Person 現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。

在此,Person.prototype 指向了原型對(duì)象,而 Person.prototype.constructor 又指回了 Person。

原型對(duì)象中除了包含 constructor 屬性之外,還包括后來(lái)添加的其他屬性。Person 的每個(gè)實(shí)例——

person1 和 person2 都包含一個(gè)內(nèi)部屬性,該屬性僅僅指向了 Person.prototype;換句話說(shuō),它們

與構(gòu)造函數(shù)沒(méi)有直接的關(guān)系。此外,要格外注意的是,雖然這兩個(gè)實(shí)例都不包含屬性和方法,但我們卻

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

第167頁(yè)

6.2 創(chuàng)建對(duì)象 149

1

2

3

4

5

13

6

7

8

9

10

11

12

可以調(diào)用 person1.sayName()。這是通過(guò)查找對(duì)象屬性的過(guò)程來(lái)實(shí)現(xiàn)的。

雖然在所有實(shí)現(xiàn)中都無(wú)法訪問(wèn)到[[Prototype]],但可以通過(guò) isPrototypeOf()方法來(lái)確定對(duì)象之

間是否存在這種關(guān)系。從本質(zhì)上講,如果[[Prototype]]指向調(diào)用 isPrototypeOf()方法的對(duì)象

(Person.prototype),那么這個(gè)方法就返回 true,如下所示:

alert(Person.prototype.isPrototypeOf(person1)); //true

alert(Person.prototype.isPrototypeOf(person2)); //true

這里,我們用原型對(duì)象的 isPrototypeOf()方法測(cè)試了 person1 和 person2。因?yàn)樗鼈儍?nèi)部都

有一個(gè)指向 Person.prototype 的指針,因此都返回了 true。

ECMAScript 5 增加了一個(gè)新方法,叫 Object.getPrototypeOf(),在所有支持的實(shí)現(xiàn)中,這個(gè)

方法返回[[Prototype]]的值。例如:

alert(Object.getPrototypeOf(person1) == Person.prototype); //true

alert(Object.getPrototypeOf(person1).name); //\"Nicholas\"

這里的第一行代碼只是確定 Object.getPrototypeOf()返回的對(duì)象實(shí)際就是這個(gè)對(duì)象的原型。

第二行代碼取得了原型對(duì)象中 name 屬性的值,也就是\"Nicholas\"。使用 Object.getPrototypeOf()

可以方便地取得一個(gè)對(duì)象的原型,而這在利用原型實(shí)現(xiàn)繼承(本章稍后會(huì)討論)的情況下是非常重要的。

支持這個(gè)方法的瀏覽器有 IE9+、Firefox 3.5+、Safari 5+、Opera 12+和 Chrome。

每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索首先

從對(duì)象實(shí)例本身開(kāi)始。如果在實(shí)例中找到了具有給定名字的屬性,則返回該屬性的值;如果沒(méi)有找到,

則繼續(xù)搜索指針指向的原型對(duì)象,在原型對(duì)象中查找具有給定名字的屬性。如果在原型對(duì)象中找到了這

個(gè)屬性,則返回該屬性的值。也就是說(shuō),在我們調(diào)用 person1.sayName()的時(shí)候,會(huì)先后執(zhí)行兩次搜

索。首先,解析器會(huì)問(wèn):“實(shí)例 person1 有 sayName 屬性嗎?”答:“沒(méi)有。”然后,它繼續(xù)搜索,再

問(wèn):“person1 的原型有 sayName 屬性嗎?”答:“有。”于是,它就讀取那個(gè)保存在原型對(duì)象中的函

數(shù)。當(dāng)我們調(diào)用 person2.sayName()時(shí),將會(huì)重現(xiàn)相同的搜索過(guò)程,得到相同的結(jié)果。而這正是多個(gè)

對(duì)象實(shí)例共享原型所保存的屬性和方法的基本原理。

前面提到過(guò),原型最初只包含 constructor 屬性,而該屬性也是共享的,因此

可以通過(guò)對(duì)象實(shí)例訪問(wèn)。

雖然可以通過(guò)對(duì)象實(shí)例訪問(wèn)保存在原型中的值,但卻不能通過(guò)對(duì)象實(shí)例重寫(xiě)原型中的值。如果我們

在實(shí)例中添加了一個(gè)屬性,而該屬性與實(shí)例原型中的一個(gè)屬性同名,那我們就在實(shí)例中創(chuàng)建該屬性,該

屬性將會(huì)屏蔽原型中的那個(gè)屬性。來(lái)看下面的例子。

function Person(){

}

Person.prototype.name = \"Nicholas\";

Person.prototype.age = 29;

Person.prototype.job = \"Software Engineer\";

Person.prototype.sayName = function(){

alert(this.name);

};

var person1 = new Person();

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

第168頁(yè)

150 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

var person2 = new Person();

person1.name = \"Greg\";

alert(person1.name); //\"Greg\"——來(lái)自實(shí)例

alert(person2.name); //\"Nicholas\"——來(lái)自原型

PrototypePatternExample02.htm

在這個(gè)例子中,person1 的 name 被一個(gè)新值給屏蔽了。但無(wú)論訪問(wèn) person1.name 還是訪問(wèn)

person2.name 都能夠正常地返回值,即分別是\"Greg\"(來(lái)自對(duì)象實(shí)例)和\"Nicholas\"(來(lái)自原型)。

當(dāng)在 alert()中訪問(wèn) person1.name 時(shí),需要讀取它的值,因此就會(huì)在這個(gè)實(shí)例上搜索一個(gè)名為 name

的屬性。這個(gè)屬性確實(shí)存在,于是就返回它的值而不必再搜索原型了。當(dāng)以同樣的方式訪問(wèn) person2.

name 時(shí),并沒(méi)有在實(shí)例上發(fā)現(xiàn)該屬性,因此就會(huì)繼續(xù)搜索原型,結(jié)果在那里找到了 name 屬性。

當(dāng)為對(duì)象實(shí)例添加一個(gè)屬性時(shí),這個(gè)屬性就會(huì)屏蔽原型對(duì)象中保存的同名屬性;換句話說(shuō),添加這

個(gè)屬性只會(huì)阻止我們?cè)L問(wèn)原型中的那個(gè)屬性,但不會(huì)修改那個(gè)屬性。即使將這個(gè)屬性設(shè)置為 null,也

只會(huì)在實(shí)例中設(shè)置這個(gè)屬性,而不會(huì)恢復(fù)其指向原型的連接。不過(guò),使用 delete 操作符則可以完全刪

除實(shí)例屬性,從而讓我們能夠重新訪問(wèn)原型中的屬性,如下所示。

function Person(){

}

Person.prototype.name = \"Nicholas\";

Person.prototype.age = 29;

Person.prototype.job = \"Software Engineer\";

Person.prototype.sayName = function(){

alert(this.name);

};

var person1 = new Person();

var person2 = new Person();

person1.name = \"Greg\";

alert(person1.name); //\"Greg\"——來(lái)自實(shí)例

alert(person2.name); //\"Nicholas\"——來(lái)自原型

delete person1.name;

alert(person1.name); //\"Nicholas\"——來(lái)自原型

PrototypePatternExample03.htm

在這個(gè)修改后的例子中,我們使用 delete 操作符刪除了 person1.name,之前它保存的\"Greg\"

值屏蔽了同名的原型屬性。把它刪除以后,就恢復(fù)了對(duì)原型中 name 屬性的連接。因此,接下來(lái)再調(diào)用

person1.name 時(shí),返回的就是原型中 name 屬性的值了。

使用 hasOwnProperty()方法可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。這個(gè)方法(不

要忘了它是從 Object 繼承來(lái)的)只在給定屬性存在于對(duì)象實(shí)例中時(shí),才會(huì)返回 true。來(lái)看下面這個(gè)例子。

function Person(){

}

Person.prototype.name = \"Nicholas\";

Person.prototype.age = 29;

Person.prototype.job = \"Software Engineer\";

Person.prototype.sayName = function(){

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

第169頁(yè)

6.2 創(chuàng)建對(duì)象 151

1

2

3

4

5

13

6

7

8

9

10

11

12

alert(this.name);

};

var person1 = new Person();

var person2 = new Person();

alert(person1.hasOwnProperty(\"name\")); //false

person1.name = \"Greg\";

alert(person1.name); //\"Greg\"——來(lái)自實(shí)例

alert(person1.hasOwnProperty(\"name\")); //true

alert(person2.name); //\"Nicholas\"——來(lái)自原型

alert(person2.hasOwnProperty(\"name\")); //false

delete person1.name;

alert(person1.name); //\"Nicholas\"——來(lái)自原型

alert(person1.hasOwnProperty(\"name\")); //false

通過(guò)使用 hasOwnProperty()方法,什么時(shí)候訪問(wèn)的是實(shí)例屬性,什么時(shí)候訪問(wèn)的是原型屬性就

一清二楚了。調(diào)用 person1.hasOwnProperty( \"name\")時(shí),只有當(dāng) person1 重寫(xiě) name 屬性后才會(huì)

返回 true,因?yàn)橹挥羞@時(shí)候 name 才是一個(gè)實(shí)例屬性,而非原型屬性。圖 6-2 展示了上面例子在不同情

況下的實(shí)現(xiàn)與原型的關(guān)系(為了簡(jiǎn)單起見(jiàn),圖中省略了與 Person 構(gòu)造函數(shù)的關(guān)系)。

圖 6-2

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

第170頁(yè)

152 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法只能用于實(shí)例屬

性,要取得原型屬性的描述符,必須直接在原型對(duì)象上調(diào)用 Object.getOwnPropertyDescriptor()方法。

2. 原型與 in 操作符

有兩種方式使用 in 操作符:?jiǎn)为?dú)使用和在 for-in 循環(huán)中使用。在單獨(dú)使用時(shí),in 操作符會(huì)在通

過(guò)對(duì)象能夠訪問(wèn)給定屬性時(shí)返回 true,無(wú)論該屬性存在于實(shí)例中還是原型中。看一看下面的例子。

function Person(){

}

Person.prototype.name = \"Nicholas\";

Person.prototype.age = 29;

Person.prototype.job = \"Software Engineer\";

Person.prototype.sayName = function(){

alert(this.name);

};

var person1 = new Person();

var person2 = new Person();

alert(person1.hasOwnProperty(\"name\")); //false

alert(\"name\" in person1); //true

person1.name = \"Greg\";

alert(person1.name); //\"Greg\" ——來(lái)自實(shí)例

alert(person1.hasOwnProperty(\"name\")); //true

alert(\"name\" in person1); //true

alert(person2.name); //\"Nicholas\" ——來(lái)自原型

alert(person2.hasOwnProperty(\"name\")); //false

alert(\"name\" in person2); //true

delete person1.name;

alert(person1.name); //\"Nicholas\" ——來(lái)自原型

alert(person1.hasOwnProperty(\"name\")); //false

alert(\"name\" in person1); //true

PrototypePatternExample04.htm

在以上代碼執(zhí)行的整個(gè)過(guò)程中,name 屬性要么是直接在對(duì)象上訪問(wèn)到的,要么是通過(guò)原型訪問(wèn)到

的。因此,調(diào)用\"name\" in person1 始終都返回 true,無(wú)論該屬性存在于實(shí)例中還是存在于原型中。

同時(shí)使用 hasOwnProperty()方法和 in 操作符,就可以確定該屬性到底是存在于對(duì)象中,還是存在于

原型中,如下所示。

function hasPrototypeProperty(object, name){

return !object.hasOwnProperty(name) && (name in object);

}

由于 in 操作符只要通過(guò)對(duì)象能夠訪問(wèn)到屬性就返回 true,hasOwnProperty()只在屬性存在于

實(shí)例中時(shí)才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以確

定屬性是原型中的屬性。下面來(lái)看一看上面定義的函數(shù) hasPrototypeProperty()的用法。

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

第171頁(yè)

6.2 創(chuàng)建對(duì)象 153

1

2

3

4

5

13

6

7

8

9

10

11

12

function Person(){

}

Person.prototype.name = \"Nicholas\";

Person.prototype.age = 29;

Person.prototype.job = \"Software Engineer\";

Person.prototype.sayName = function(){

alert(this.name);

};

var person = new Person();

alert(hasPrototypeProperty(person, \"name\")); //true

person.name = \"Greg\";

alert(hasPrototypeProperty(person, \"name\")); //false

PrototypePatternExample05.htm

在這里,name 屬性先是存在于原型中,因此 hasPrototypeProperty()返回 true。當(dāng)在實(shí)例中

重寫(xiě) name 屬性后,該屬性就存在于實(shí)例中了,因此 hasPrototypeProperty()返回 false。即使原

型中仍然有 name 屬性,但由于現(xiàn)在實(shí)例中也有了這個(gè)屬性,因此原型中的 name 屬性就用不到了。

在使用 for-in 循環(huán)時(shí),返回的是所有能夠通過(guò)對(duì)象訪問(wèn)的、可枚舉的(enumerated)屬性,其中

既包括存在于實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(即將

[[Enumerable]]標(biāo)記為 false 的屬性)的實(shí)例屬性也會(huì)在 for-in 循環(huán)中返回,因?yàn)楦鶕?jù)規(guī)定,所

有開(kāi)發(fā)人員定義的屬性都是可枚舉的——只有在 IE8 及更早版本中例外。

IE 早期版本的實(shí)現(xiàn)中存在一個(gè) bug,即屏蔽不可枚舉屬性的實(shí)例屬性不會(huì)出現(xiàn)在 for-in 循環(huán)中。

例如:

var o = {

toString : function(){

return \"My Object\";

}

};

for (var prop in o){

if (prop == \"toString\"){

alert(\"Found toString\"); //在 IE 中不會(huì)顯示

}

}

PrototypePatternExample06.htm

當(dāng)以上代碼運(yùn)行時(shí),應(yīng)該會(huì)顯示一個(gè)警告框,表明找到了 toString()方法。這里的對(duì)象 o 定義了

一個(gè)名為 toString()的方法,該方法屏蔽了原型中(不可枚舉)的 toString()方法。在 IE 中,由

于其實(shí)現(xiàn)認(rèn)為原型的 toString()方法被打上了值為 false 的[[Enumerable]]標(biāo)記,因此應(yīng)該跳過(guò)

該屬性,結(jié)果我們就不會(huì)看到警告框。該 bug 會(huì)影響默認(rèn)不可枚舉的所有屬性和方法,包括:

hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和 valueOf()。

ECMAScript 5 也將 constructor 和 prototype 屬性的[[Enumerable]]特性設(shè)置為 false,但并不

是所有瀏覽器都照此實(shí)現(xiàn)。

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

第172頁(yè)

154 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

要取得對(duì)象上所有可枚舉的實(shí)例屬性,可以使用 ECMAScript 5 的 Object.keys()方法。這個(gè)方法

接收一個(gè)對(duì)象作為參數(shù),返回一個(gè)包含所有可枚舉屬性的字符串?dāng)?shù)組。例如:

function Person(){

}

Person.prototype.name = \"Nicholas\";

Person.prototype.age = 29;

Person.prototype.job = \"Software Engineer\";

Person.prototype.sayName = function(){

alert(this.name);

};

var keys = Object.keys(Person.prototype);

alert(keys); //\"name,age,job,sayName\"

var p1 = new Person();

p1.name = \"Rob\";

p1.age = 31;

var p1keys = Object.keys(p1);

alert(p1keys); //\"name,age\"

ObjectKeysExample01.htm

這里,變量 keys 中將保存一個(gè)數(shù)組,數(shù)組中是字符串\"name\"、\"age\"、\"job\"和\"sayName\"。這

個(gè)順序也是它們?cè)?for-in 循環(huán)中出現(xiàn)的順序。如果是通過(guò) Person 的實(shí)例調(diào)用,則 Object.keys()

返回的數(shù)組只包含\"name\"和\"age\"這兩個(gè)實(shí)例屬性。

如果你想要得到所有實(shí)例屬性,無(wú)論它是否可枚舉,都可以使用 Object.getOwnPropertyNames()

方法。

var keys = Object.getOwnPropertyNames(Person.prototype);

alert(keys); //\"constructor,name,age,job,sayName\"

ObjectPropertyNamesExample01.htm

注意結(jié)果中包含了不可枚舉的 constructor 屬性。Object.keys()和 Object.getOwnPropertyNames()方法都可以用來(lái)替代 for-in 循環(huán)。支持這兩個(gè)方法的瀏覽器有 IE9+、Firefox 4+、Safari 5+、Opera

12+和 Chrome。

3. 更簡(jiǎn)單的原型語(yǔ)法

讀者大概注意到了,前面例子中每添加一個(gè)屬性和方法就要敲一遍 Person.prototype。為減少

不必要的輸入,也為了從視覺(jué)上更好地封裝原型的功能,更常見(jiàn)的做法是用一個(gè)包含所有屬性和方法的

對(duì)象字面量來(lái)重寫(xiě)整個(gè)原型對(duì)象,如下面的例子所示。

function Person(){

}

Person.prototype = {

name : \"Nicholas\",

age : 29,

job: \"Software Engineer\",

sayName : function () {

alert(this.name);

}

};

PrototypePatternExample07.htm

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

第173頁(yè)

6.2 創(chuàng)建對(duì)象 155

1

2

3

4

5

13

6

7

8

9

10

11

12

在上面的代碼中,我們將 Person.prototype 設(shè)置為等于一個(gè)以對(duì)象字面量形式創(chuàng)建的新對(duì)象。

最終結(jié)果相同,但有一個(gè)例外:constructor 屬性不再指向 Person 了。前面曾經(jīng)介紹過(guò),每創(chuàng)建一

個(gè)函數(shù),就會(huì)同時(shí)創(chuàng)建它的 prototype 對(duì)象,這個(gè)對(duì)象也會(huì)自動(dòng)獲得 constructor 屬性。而我們?cè)?/p>

這里使用的語(yǔ)法,本質(zhì)上完全重寫(xiě)了默認(rèn)的 prototype 對(duì)象,因此 constructor 屬性也就變成了新

對(duì)象的 constructor 屬性(指向 Object 構(gòu)造函數(shù)),不再指向 Person 函數(shù)。此時(shí),盡管 instanceof

操作符還能返回正確的結(jié)果,但通過(guò) constructor 已經(jīng)無(wú)法確定對(duì)象的類(lèi)型了,如下所示。

var friend = new Person();

alert(friend instanceof Object); //true

alert(friend instanceof Person); //true

alert(friend.constructor == Person); //false

alert(friend.constructor == Object); //true

PrototypePatternExample07.htm

在此,用 instanceof 操作符測(cè)試 Object 和 Person 仍然返回 true,但 constructor 屬性則

等于 Object 而不等于 Person 了。如果 constructor 的值真的很重要,可以像下面這樣特意將它設(shè)

置回適當(dāng)?shù)闹怠?/p>

function Person(){

}

Person.prototype = {

constructor : Person,

name : \"Nicholas\",

age : 29,

job: \"Software Engineer\",

sayName : function () {

alert(this.name);

}

};

PrototypePatternExample07.htm

以上代碼特意包含了一個(gè) constructor 屬性,并將它的值設(shè)置為 Person,從而確保了通過(guò)該屬

性能夠訪問(wèn)到適當(dāng)?shù)闹怠?/p>

注意,以這種方式重設(shè) constructor 屬性會(huì)導(dǎo)致它的[[Enumerable]]特性被設(shè)置為 true。默認(rèn)

情況下,原生的 constructor 屬性是不可枚舉的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引

擎,可以試一試 Object.defineProperty()。

function Person(){

}

Person.prototype = {

name : \"Nicholas\",

age : 29,

job : \"Software Engineer\",

sayName : function () {

alert(this.name);

}

};

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

第174頁(yè)

156 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

//重設(shè)構(gòu)造函數(shù),只適用于 ECMAScript 5 兼容的瀏覽器

Object.defineProperty(Person.prototype, \"constructor\", {

enumerable: false,

value: Person

});

4. 原型的動(dòng)態(tài)性

由于在原型中查找值的過(guò)程是一次搜索,因此我們對(duì)原型對(duì)象所做的任何修改都能夠立即從實(shí)例上

反映出來(lái)——即使是先創(chuàng)建了實(shí)例后修改原型也照樣如此。請(qǐng)看下面的例子。

var friend = new Person();

Person.prototype.sayHi = function(){

alert(\"hi\");

};

friend.sayHi(); //\"hi\"(沒(méi)有問(wèn)題!)

PrototypePatternExample09.htm

以上代碼先創(chuàng)建了 Person 的一個(gè)實(shí)例,并將其保存在 person 中。然后,下一條語(yǔ)句在 Person.

prototype 中添加了一個(gè)方法 sayHi()。即使 person 實(shí)例是在添加新方法之前創(chuàng)建的,但它仍然可

以訪問(wèn)這個(gè)新方法。其原因可以歸結(jié)為實(shí)例與原型之間的松散連接關(guān)系。當(dāng)我們調(diào)用 person.sayHi()

時(shí),首先會(huì)在實(shí)例中搜索名為 sayHi 的屬性,在沒(méi)找到的情況下,會(huì)繼續(xù)搜索原型。因?yàn)閷?shí)例與原型

之間的連接只不過(guò)是一個(gè)指針,而非一個(gè)副本,因此就可以在原型中找到新的 sayHi 屬性并返回保存

在那里的函數(shù)。

盡管可以隨時(shí)為原型添加屬性和方法,并且修改能夠立即在所有對(duì)象實(shí)例中反映出來(lái),但如果是重

寫(xiě)整個(gè)原型對(duì)象,那么情況就不一樣了。我們知道,調(diào)用構(gòu)造函數(shù)時(shí)會(huì)為實(shí)例添加一個(gè)指向最初原型的

[[Prototype]]指針,而把原型修改為另外一個(gè)對(duì)象就等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系。

請(qǐng)記?。簩?shí)例中的指針僅指向原型,而不指向構(gòu)造函數(shù)??聪旅娴睦?。

function Person(){

}

var friend = new Person();

Person.prototype = {

constructor: Person,

name : \"Nicholas\",

age : 29,

job : \"Software Engineer\",

sayName : function () {

alert(this.name);

}

};

friend.sayName(); //error

PrototypePatternExample10.htm

在這個(gè)例子中,我們先創(chuàng)建了 Person 的一個(gè)實(shí)例,然后又重寫(xiě)了其原型對(duì)象。然后在調(diào)用

friend.sayName()時(shí)發(fā)生了錯(cuò)誤,因?yàn)?friend 指向的原型中不包含以該名字命名的屬性。圖 6-3 展

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

第175頁(yè)

6.2 創(chuàng)建對(duì)象 157

1

2

3

4

5

13

6

7

8

9

10

11

12

示了這個(gè)過(guò)程的內(nèi)幕。

圖 6-3

從圖 6-3 可以看出,重寫(xiě)原型對(duì)象切斷了現(xiàn)有原型與任何之前已經(jīng)存在的對(duì)象實(shí)例之間的聯(lián)系;它

們引用的仍然是最初的原型。

5. 原生對(duì)象的原型

原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類(lèi)型方面,就連所有原生的引用類(lèi)型,都是采用這種模式

創(chuàng)建的。所有原生引用類(lèi)型(Object、Array、String,等等)都在其構(gòu)造函數(shù)的原型上定義了方法。

例如,在 Array.prototype 中可以找到 sort()方法,而在 String.prototype 中可以找到

substring()方法,如下所示。

alert(typeof Array.prototype.sort); //\"function\"

alert(typeof String.prototype.substring); //\"function\"

通過(guò)原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,而且也可以定義新方法??梢韵裥薷淖?/p>

定義對(duì)象的原型一樣修改原生對(duì)象的原型,因此可以隨時(shí)添加方法。下面的代碼就給基本包裝類(lèi)型

String 添加了一個(gè)名為 startsWith()的方法。

String.prototype.startsWith = function (text) {

return this.indexOf(text) == 0;

};

var msg = \"Hello world!\";

alert(msg.startsWith(\"Hello\")); //true

PrototypePatternExample11.htm

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

第176頁(yè)

158 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

這里新定義的 startsWith()方法會(huì)在傳入的文本位于一個(gè)字符串開(kāi)始時(shí)返回 true。既然方法被

添加給了 String.prototype,那么當(dāng)前環(huán)境中的所有字符串就都可以調(diào)用它。由于 msg 是字符串,

而且后臺(tái)會(huì)調(diào)用 String 基本包裝函數(shù)創(chuàng)建這個(gè)字符串,因此通過(guò) msg 就可以調(diào)用 startsWith()方法。

盡管可以這樣做,但我們不推薦在產(chǎn)品化的程序中修改原生對(duì)象的原型。如果因

某個(gè)實(shí)現(xiàn)中缺少某個(gè)方法,就在原生對(duì)象的原型中添加這個(gè)方法,那么當(dāng)在另一個(gè)支

持該方法的實(shí)現(xiàn)中運(yùn)行代碼時(shí),就可能會(huì)導(dǎo)致命名沖突。而且,這樣做也可能會(huì)意外

地重寫(xiě)原生方法。

6. 原型對(duì)象的問(wèn)題

原型模式也不是沒(méi)有缺點(diǎn)。首先,它省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),結(jié)果所有實(shí)例在

默認(rèn)情況下都將取得相同的屬性值。雖然這會(huì)在某種程度上帶來(lái)一些不方便,但還不是原型的最大問(wèn)題。

原型模式的最大問(wèn)題是由其共享的本性所導(dǎo)致的。

原型中所有屬性是被很多實(shí)例共享的,這種共享對(duì)于函數(shù)非常合適。對(duì)于那些包含基本值的屬性倒

也說(shuō)得過(guò)去,畢竟(如前面的例子所示),通過(guò)在實(shí)例上添加一個(gè)同名屬性,可以隱藏原型中的對(duì)應(yīng)屬

性。然而,對(duì)于包含引用類(lèi)型值的屬性來(lái)說(shuō),問(wèn)題就比較突出了。來(lái)看下面的例子。

function Person(){

}

Person.prototype = {

constructor: Person,

name : \"Nicholas\",

age : 29,

job : \"Software Engineer\",

friends : [\"Shelby\", \"Court\"],

sayName : function () {

alert(this.name);

}

};

var person1 = new Person();

var person2 = new Person();

person1.friends.push(\"Van\");

alert(person1.friends); //\"Shelby,Court,Van\"

alert(person2.friends); //\"Shelby,Court,Van\"

alert(person1.friends === person2.friends); //true

PrototypePatternExample12.htm

在此,Person.prototype 對(duì)象有一個(gè)名為 friends 的屬性,該屬性包含一個(gè)字符串?dāng)?shù)組。然后,

創(chuàng)建了 Person 的兩個(gè)實(shí)例。接著,修改了 person1.friends 引用的數(shù)組,向數(shù)組中添加了一個(gè)字符

串。由于 friends 數(shù)組存在于 Person.prototype 而非 person1 中,所以剛剛提到的修改也會(huì)通過(guò)

person2.friends(與 person1.friends 指向同一個(gè)數(shù)組)反映出來(lái)。假如我們的初衷就是像這樣

在所有實(shí)例中共享一個(gè)數(shù)組,那么對(duì)這個(gè)結(jié)果我沒(méi)有話可說(shuō)??墒?,實(shí)例一般都是要有屬于自己的全部

屬性的。而這個(gè)問(wèn)題正是我們很少看到有人單獨(dú)使用原型模式的原因所在。

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

第177頁(yè)

6.2 創(chuàng)建對(duì)象 159

1

2

3

4

5

13

6

7

8

9

10

11

12

6.2.4 組合使用構(gòu)造函數(shù)模式和原型模式

創(chuàng)建自定義類(lèi)型的最常見(jiàn)方式,就是組合使用構(gòu)造函數(shù)模式與原型模式。構(gòu)造函數(shù)模式用于定義實(shí)

例屬性,而原型模式用于定義方法和共享的屬性。結(jié)果,每個(gè)實(shí)例都會(huì)有自己的一份實(shí)例屬性的副本,

但同時(shí)又共享著對(duì)方法的引用,最大限度地節(jié)省了內(nèi)存。另外,這種混成模式還支持向構(gòu)造函數(shù)傳遞參

數(shù);可謂是集兩種模式之長(zhǎng)。下面的代碼重寫(xiě)了前面的例子。

function Person(name, age, job){

this.name = name;

this.age = age;

this.job = job;

this.friends = [\"Shelby\", \"Court\"];

}

Person.prototype = {

constructor : Person,

sayName : function(){

alert(this.name);

}

}

var person1 = new Person(\"Nicholas\", 29, \"Software Engineer\");

var person2 = new Person(\"Greg\", 27, \"Doctor\");

person1.friends.push(\"Van\");

alert(person1.friends); //\"Shelby,Count,Van\"

alert(person2.friends); //\"Shelby,Count\"

alert(person1.friends === person2.friends); //false

alert(person1.sayName === person2.sayName); //true

HybridPatternExample01.htm

在這個(gè)例子中,實(shí)例屬性都是在構(gòu)造函數(shù)中定義的,而由所有實(shí)例共享的屬性 constructor 和方

法 sayName()則是在原型中定義的。而修改了 person1.friends(向其中添加一個(gè)新字符串),并不

會(huì)影響到 person2.friends,因?yàn)樗鼈兎謩e引用了不同的數(shù)組。

這種構(gòu)造函數(shù)與原型混成的模式,是目前在 ECMAScript 中使用最廣泛、認(rèn)同度最高的一種創(chuàng)建自

定義類(lèi)型的方法??梢哉f(shuō),這是用來(lái)定義引用類(lèi)型的一種默認(rèn)模式。

6.2.5 動(dòng)態(tài)原型模式

有其他 OO 語(yǔ)言經(jīng)驗(yàn)的開(kāi)發(fā)人員在看到獨(dú)立的構(gòu)造函數(shù)和原型時(shí),很可能會(huì)感到非常困惑。動(dòng)態(tài)原

型模式正是致力于解決這個(gè)問(wèn)題的一個(gè)方案,它把所有信息都封裝在了構(gòu)造函數(shù)中,而通過(guò)在構(gòu)造函數(shù)

中初始化原型(僅在必要的情況下),又保持了同時(shí)使用構(gòu)造函數(shù)和原型的優(yōu)點(diǎn)。換句話說(shuō),可以通過(guò)

檢查某個(gè)應(yīng)該存在的方法是否有效,來(lái)決定是否需要初始化原型。來(lái)看一個(gè)例子。

function Person(name, age, job){

//屬性

this.name = name;

this.age = age;

this.job = job;

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

第178頁(yè)

160 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

//方法

if (typeof this.sayName != \"function\"){

Person.prototype.sayName = function(){

alert(this.name);

};

}

}

var friend = new Person(\"Nicholas\", 29, \"Software Engineer\");

friend.sayName();

DynamicPrototypeExample01.htm

注意構(gòu)造函數(shù)代碼中加粗的部分。這里只在 sayName()方法不存在的情況下,才會(huì)將它添加到原

型中。這段代碼只會(huì)在初次調(diào)用構(gòu)造函數(shù)時(shí)才會(huì)執(zhí)行。此后,原型已經(jīng)完成初始化,不需要再做什么修

改了。不過(guò)要記住,這里對(duì)原型所做的修改,能夠立即在所有實(shí)例中得到反映。因此,這種方法確實(shí)可

以說(shuō)非常完美。其中,if 語(yǔ)句檢查的可以是初始化之后應(yīng)該存在的任何屬性或方法——不必用一大堆

if 語(yǔ)句檢查每個(gè)屬性和每個(gè)方法;只要檢查其中一個(gè)即可。對(duì)于采用這種模式創(chuàng)建的對(duì)象,還可以使

用 instanceof 操作符確定它的類(lèi)型。

使用動(dòng)態(tài)原型模式時(shí),不能使用對(duì)象字面量重寫(xiě)原型。前面已經(jīng)解釋過(guò)了,如果

在已經(jīng)創(chuàng)建了實(shí)例的情況下重寫(xiě)原型,那么就會(huì)切斷現(xiàn)有實(shí)例與新原型之間的聯(lián)系。

6.2.6 寄生構(gòu)造函數(shù)模式

通常,在前述的幾種模式都不適用的情況下,可以使用寄生(parasitic)構(gòu)造函數(shù)模式。這種模式

的基本思想是創(chuàng)建一個(gè)函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建對(duì)象的代碼,然后再返回新創(chuàng)建的對(duì)象;但

從表面上看,這個(gè)函數(shù)又很像是典型的構(gòu)造函數(shù)。下面是一個(gè)例子。

function Person(name, age, job){

var o = new Object();

o.name = name;

o.age = age;

o.job = job;

o.sayName = function(){

alert(this.name);

};

return o;

}

var friend = new Person(\"Nicholas\", 29, \"Software Engineer\");

friend.sayName(); //\"Nicholas\"

HybridFactoryPatternExample01.htm

在這個(gè)例子中,Person 函數(shù)創(chuàng)建了一個(gè)新對(duì)象,并以相應(yīng)的屬性和方法初始化該對(duì)象,然后又返

回了這個(gè)對(duì)象。除了使用 new 操作符并把使用的包裝函數(shù)叫做構(gòu)造函數(shù)之外,這個(gè)模式跟工廠模式其實(shí)

是一模一樣的。構(gòu)造函數(shù)在不返回值的情況下,默認(rèn)會(huì)返回新對(duì)象實(shí)例。而通過(guò)在構(gòu)造函數(shù)的末尾添加

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

第179頁(yè)

6.2 創(chuàng)建對(duì)象 161

1

2

3

4

5

13

6

7

8

9

10

11

12

一個(gè) return 語(yǔ)句,可以重寫(xiě)調(diào)用構(gòu)造函數(shù)時(shí)返回的值。

這個(gè)模式可以在特殊的情況下用來(lái)為對(duì)象創(chuàng)建構(gòu)造函數(shù)。假設(shè)我們想創(chuàng)建一個(gè)具有額外方法的特殊

數(shù)組。由于不能直接修改 Array 構(gòu)造函數(shù),因此可以使用這個(gè)模式。

function SpecialArray(){

//創(chuàng)建數(shù)組

var values = new Array();

//添加值

values.push.apply(values, arguments);

//添加方法

values.toPipedString = function(){

return this.join(\"|\");

};

//返回?cái)?shù)組

return values;

}

var colors = new SpecialArray(\"red\", \"blue\", \"green\");

alert(colors.toPipedString()); //\"red|blue|green\"

HybridFactoryPatternExample02.htm

在這個(gè)例子中,我們創(chuàng)建了一個(gè)名叫 SpecialArray 的構(gòu)造函數(shù)。在這個(gè)函數(shù)內(nèi)部,首先創(chuàng)建了

一個(gè)數(shù)組,然后 push()方法(用構(gòu)造函數(shù)接收到的所有參數(shù))初始化了數(shù)組的值。隨后,又給數(shù)組實(shí)

例添加了一個(gè) toPipedString()方法,該方法返回以豎線分割的數(shù)組值。最后,將數(shù)組以函數(shù)值的形

式返回。接著,我們調(diào)用了 SpecialArray 構(gòu)造函數(shù),向其中傳入了用于初始化數(shù)組的值,此后又調(diào)

用了 toPipedString()方法。

關(guān)于寄生構(gòu)造函數(shù)模式,有一點(diǎn)需要說(shuō)明:首先,返回的對(duì)象與構(gòu)造函數(shù)或者與構(gòu)造函數(shù)的原型屬

性之間沒(méi)有關(guān)系;也就是說(shuō),構(gòu)造函數(shù)返回的對(duì)象與在構(gòu)造函數(shù)外部創(chuàng)建的對(duì)象沒(méi)有什么不同。為此,

不能依賴 instanceof 操作符來(lái)確定對(duì)象類(lèi)型。由于存在上述問(wèn)題,我們建議在可以使用其他模式的情

況下,不要使用這種模式。

6.2.7 穩(wěn)妥構(gòu)造函數(shù)模式

道格拉斯·克羅克福德(Douglas Crockford)發(fā)明了 JavaScript 中的穩(wěn)妥對(duì)象(durable objects)這

個(gè)概念。所謂穩(wěn)妥對(duì)象,指的是沒(méi)有公共屬性,而且其方法也不引用 this 的對(duì)象。穩(wěn)妥對(duì)象最適合在

一些安全的環(huán)境中(這些環(huán)境中會(huì)禁止使用 this 和 new),或者在防止數(shù)據(jù)被其他應(yīng)用程序(如 Mashup

程序)改動(dòng)時(shí)使用。穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類(lèi)似的模式,但有兩點(diǎn)不同:一是新創(chuàng)建對(duì)象的

實(shí)例方法不引用 this;二是不使用 new 操作符調(diào)用構(gòu)造函數(shù)。按照穩(wěn)妥構(gòu)造函數(shù)的要求,可以將前面

的 Person 構(gòu)造函數(shù)重寫(xiě)如下。

function Person(name, age, job){

//創(chuàng)建要返回的對(duì)象

var o = new Object();

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

第180頁(yè)

162 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

//可以在這里定義私有變量和函數(shù)

//添加方法

o.sayName = function(){

alert(name);

};

//返回對(duì)象

return o;

}

注意,在以這種模式創(chuàng)建的對(duì)象中,除了使用 sayName()方法之外,沒(méi)有其他辦法訪問(wèn) name 的值。

可以像下面使用穩(wěn)妥的 Person 構(gòu)造函數(shù)。

var friend = Person(\"Nicholas\", 29, \"Software Engineer\");

friend.sayName(); //\"Nicholas\"

這樣,變量 friend 中保存的是一個(gè)穩(wěn)妥對(duì)象,而除了調(diào)用 sayName()方法外,沒(méi)有別的方式可

以訪問(wèn)其數(shù)據(jù)成員。即使有其他代碼會(huì)給這個(gè)對(duì)象添加方法或數(shù)據(jù)成員,但也不可能有別的辦法訪問(wèn)傳

入到構(gòu)造函數(shù)中的原始數(shù)據(jù)。穩(wěn)妥構(gòu)造函數(shù)模式提供的這種安全性,使得它非常適合在某些安全執(zhí)行環(huán)

境——例如,ADsafe(www.adsafe.org)和 Caja(http://code.google.com/p/google-caja/)提供的環(huán)境——

下使用。

與寄生構(gòu)造函數(shù)模式類(lèi)似,使用穩(wěn)妥構(gòu)造函數(shù)模式創(chuàng)建的對(duì)象與構(gòu)造函數(shù)之間也

沒(méi)有什么關(guān)系,因此 instanceof 操作符對(duì)這種對(duì)象也沒(méi)有意義。

6.3 繼承

繼承是 OO 語(yǔ)言中的一個(gè)最為人津津樂(lè)道的概念。許多 OO 語(yǔ)言都支持兩種繼承方式:接口繼承和

實(shí)現(xiàn)繼承。接口繼承只繼承方法簽名,而實(shí)現(xiàn)繼承則繼承實(shí)際的方法。如前所述,由于函數(shù)沒(méi)有簽名,

在 ECMAScript 中無(wú)法實(shí)現(xiàn)接口繼承。ECMAScript 只支持實(shí)現(xiàn)繼承,而且其實(shí)現(xiàn)繼承主要是依靠原型鏈

來(lái)實(shí)現(xiàn)的。

6.3.1 原型鏈

ECMAScript 中描述了原型鏈的概念,并將原型鏈作為實(shí)現(xiàn)繼承的主要方法。其基本思想是利用原

型讓一個(gè)引用類(lèi)型繼承另一個(gè)引用類(lèi)型的屬性和方法。簡(jiǎn)單回顧一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:每

個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個(gè)指向原型

對(duì)象的內(nèi)部指針。那么,假如我們讓原型對(duì)象等于另一個(gè)類(lèi)型的實(shí)例,結(jié)果會(huì)怎么樣呢?顯然,此時(shí)的

原型對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著一個(gè)指向另一個(gè)構(gòu)造函數(shù)

的指針。假如另一個(gè)原型又是另一個(gè)類(lèi)型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)

例與原型的鏈條。這就是所謂原型鏈的基本概念。

實(shí)現(xiàn)原型鏈有一種基本模式,其代碼大致如下。

function SuperType(){

this.property = true;

}

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

第181頁(yè)

6.3 繼承 163

1

2

3

4

5

13

6

7

8

9

10

11

12

SuperType.prototype.getSuperValue = function(){

return this.property;

};

function SubType(){

this.subproperty = false;

}

//繼承了 SuperType

SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function (){

return this.subproperty;

};

var instance = new SubType();

alert(instance.getSuperValue()); //true

PrototypeChainingExample01.htm

以上代碼定義了兩個(gè)類(lèi)型:SuperType 和 SubType。每個(gè)類(lèi)型分別有一個(gè)屬性和一個(gè)方法。它們

的主要區(qū)別是 SubType 繼承了 SuperType,而繼承是通過(guò)創(chuàng)建 SuperType 的實(shí)例,并將該實(shí)例賦給

SubType.prototype 實(shí)現(xiàn)的。實(shí)現(xiàn)的本質(zhì)是重寫(xiě)原型對(duì)象,代之以一個(gè)新類(lèi)型的實(shí)例。換句話說(shuō),原

來(lái)存在于 SuperType 的實(shí)例中的所有屬性和方法,現(xiàn)在也存在于 SubType.prototype 中了。在確立了

繼承關(guān)系之后,我們給 SubType.prototype 添加了一個(gè)方法,這樣就在繼承了 SuperType 的屬性和方

法的基礎(chǔ)上又添加了一個(gè)新方法。這個(gè)例子中的實(shí)例以及構(gòu)造函數(shù)和原型之間的關(guān)系如圖 6-4所示。

圖 6-4

在上面的代碼中,我們沒(méi)有使用 SubType 默認(rèn)提供的原型,而是給它換了一個(gè)新原型;這個(gè)新原型

就是 SuperType 的實(shí)例。于是,新原型不僅具有作為一個(gè) SuperType 的實(shí)例所擁有的全部屬性和方法,

而且其內(nèi)部還有一個(gè)指針,指向了 SuperType 的原型。最終結(jié)果就是這樣的:instance 指向 SubType

的原型, SubType 的原型又指向 SuperType 的原型。 getSuperValue() 方法仍然還在

SuperType.prototype 中,但 property 則位于 SubType.prototype 中。這是因?yàn)?property 是一

個(gè)實(shí)例屬性,而 getSuperValue()則是一個(gè)原型方法。既然 SubType.prototype 現(xiàn)在是 SuperType

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

第182頁(yè)

164 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

的實(shí)例,那么 property 當(dāng)然就位于該實(shí)例中了。此外,要注意 instance.constructor 現(xiàn)在指向的

是 SuperType,這是因?yàn)樵瓉?lái) SubType.prototype 中的 constructor 被重寫(xiě)了的緣故①。

通過(guò)實(shí)現(xiàn)原型鏈,本質(zhì)上擴(kuò)展了本章前面介紹的原型搜索機(jī)制。讀者大概還記得,當(dāng)以讀取模式訪

問(wèn)一個(gè)實(shí)例屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒(méi)有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型。在

通過(guò)原型鏈實(shí)現(xiàn)繼承的情況下,搜索過(guò)程就得以沿著原型鏈繼續(xù)向上。就拿上面的例子來(lái)說(shuō),調(diào)用

instance.getSuperValue()會(huì)經(jīng)歷三個(gè)搜索步驟:1)搜索實(shí)例;2)搜索 SubType.prototype;

3)搜索 SuperType.prototype,最后一步才會(huì)找到該方法。在找不到屬性或方法的情況下,搜索過(guò)

程總是要一環(huán)一環(huán)地前行到原型鏈末端才會(huì)停下來(lái)。

1. 別忘記默認(rèn)的原型

事實(shí)上,前面例子中展示的原型鏈還少一環(huán)。我們知道,所有引用類(lèi)型默認(rèn)都繼承了 Object,而

這個(gè)繼承也是通過(guò)原型鏈實(shí)現(xiàn)的。大家要記住,所有函數(shù)的默認(rèn)原型都是 Object 的實(shí)例,因此默認(rèn)原

型都會(huì)包含一個(gè)內(nèi)部指針,指向 Object.prototype。這也正是所有自定義類(lèi)型都會(huì)繼承 toString()、

valueOf()等默認(rèn)方法的根本原因。所以,我們說(shuō)上面例子展示的原型鏈中還應(yīng)該包括另外一個(gè)繼承層

次。圖 6-5 為我們展示了該例子中完整的原型鏈。

圖 6-5

一句話,SubType 繼承了 SuperType,而 SuperType 繼承了 Object。當(dāng)調(diào)用 instance.toString()

時(shí),實(shí)際上調(diào)用的是保存在 Object.prototype 中的那個(gè)方法。

——————————

① 實(shí)際上,不是 SubType 的原型的 constructor 屬性被重寫(xiě)了,而是 SubType 的原型指向了另一個(gè)對(duì)象——

SuperType 的原型,而這個(gè)原型對(duì)象的 constructor 屬性指向的是 SuperType。

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

第183頁(yè)

6.3 繼承 165

1

2

3

4

5

13

6

7

8

9

10

11

12

2. 確定原型和實(shí)例的關(guān)系

可以通過(guò)兩種方式來(lái)確定原型和實(shí)例之間的關(guān)系。第一種方式是使用 instanceof 操作符,只要用

這個(gè)操作符來(lái)測(cè)試實(shí)例與原型鏈中出現(xiàn)過(guò)的構(gòu)造函數(shù),結(jié)果就會(huì)返回 true。以下幾行代碼就說(shuō)明了這

一點(diǎn)。

alert(instance instanceof Object); //true

alert(instance instanceof SuperType); //true

alert(instance instanceof SubType); //true

PrototypeChainingExample01.htm

由于原型鏈的關(guān)系,我們可以說(shuō) instance 是 Object、SuperType 或 SubType 中任何一個(gè)類(lèi)型

的實(shí)例。因此,測(cè)試這三個(gè)構(gòu)造函數(shù)的結(jié)果都返回了 true。

第二種方式是使用 isPrototypeOf()方法。同樣,只要是原型鏈中出現(xiàn)過(guò)的原型,都可以說(shuō)是該

原型鏈所派生的實(shí)例的原型,因此 isPrototypeOf()方法也會(huì)返回 true,如下所示。

alert(Object.prototype.isPrototypeOf(instance)); //true

alert(SuperType.prototype.isPrototypeOf(instance)); //true

alert(SubType.prototype.isPrototypeOf(instance)); //true

PrototypeChainingExample01.htm

3. 謹(jǐn)慎地定義方法

子類(lèi)型有時(shí)候需要重寫(xiě)超類(lèi)型中的某個(gè)方法,或者需要添加超類(lèi)型中不存在的某個(gè)方法。但不管怎

樣,給原型添加方法的代碼一定要放在替換原型的語(yǔ)句之后。來(lái)看下面的例子。

function SuperType(){

this.property = true;

}

SuperType.prototype.getSuperValue = function(){

return this.property;

};

function SubType(){

this.subproperty = false;

}

//繼承了 SuperType

SubType.prototype = new SuperType();

//添加新方法

SubType.prototype.getSubValue = function (){

return this.subproperty;

};

//重寫(xiě)超類(lèi)型中的方法

SubType.prototype.getSuperValue = function (){

return false;

};

var instance = new SubType();

alert(instance.getSuperValue()); //false

PrototypeChainingExample02.htm

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

第184頁(yè)

166 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

在以上代碼中,加粗的部分是兩個(gè)方法的定義。第一個(gè)方法 getSubValue()被添加到了 SubType

中。第二個(gè)方法 getSuperValue()是原型鏈中已經(jīng)存在的一個(gè)方法,但重寫(xiě)這個(gè)方法將會(huì)屏蔽原來(lái)的

那個(gè)方法。換句話說(shuō),當(dāng)通過(guò) SubType 的實(shí)例調(diào)用 getSuperValue()時(shí),調(diào)用的就是這個(gè)重新定義

的方法;但通過(guò) SuperType 的實(shí)例調(diào)用 getSuperValue()時(shí),還會(huì)繼續(xù)調(diào)用原來(lái)的那個(gè)方法。這里

要格外注意的是,必須在用 SuperType 的實(shí)例替換原型之后,再定義這兩個(gè)方法。

還有一點(diǎn)需要提醒讀者,即在通過(guò)原型鏈實(shí)現(xiàn)繼承時(shí),不能使用對(duì)象字面量創(chuàng)建原型方法。因?yàn)檫@

樣做就會(huì)重寫(xiě)原型鏈,如下面的例子所示。

function SuperType(){

this.property = true;

}

SuperType.prototype.getSuperValue = function(){

return this.property;

};

function SubType(){

this.subproperty = false;

}

//繼承了 SuperType

SubType.prototype = new SuperType();

//使用字面量添加新方法,會(huì)導(dǎo)致上一行代碼無(wú)效

SubType.prototype = {

getSubValue : function (){

return this.subproperty;

},

someOtherMethod : function (){

return false;

}

};

var instance = new SubType();

alert(instance.getSuperValue()); //error!

PrototypeChainingExample03.htm

以上代碼展示了剛剛把 SuperType 的實(shí)例賦值給原型,緊接著又將原型替換成一個(gè)對(duì)象字面量而

導(dǎo)致的問(wèn)題。由于現(xiàn)在的原型包含的是一個(gè) Object 的實(shí)例,而非 SuperType 的實(shí)例,因此我們?cè)O(shè)想

中的原型鏈已經(jīng)被切斷——SubType 和 SuperType 之間已經(jīng)沒(méi)有關(guān)系了。

4. 原型鏈的問(wèn)題

原型鏈雖然很強(qiáng)大,可以用它來(lái)實(shí)現(xiàn)繼承,但它也存在一些問(wèn)題。其中,最主要的問(wèn)題來(lái)自包含引

用類(lèi)型值的原型。想必大家還記得,我們前面介紹過(guò)包含引用類(lèi)型值的原型屬性會(huì)被所有實(shí)例共享;而

這也正是為什么要在構(gòu)造函數(shù)中,而不是在原型對(duì)象中定義屬性的原因。在通過(guò)原型來(lái)實(shí)現(xiàn)繼承時(shí),原

型實(shí)際上會(huì)變成另一個(gè)類(lèi)型的實(shí)例。于是,原先的實(shí)例屬性也就順理成章地變成了現(xiàn)在的原型屬性了。

下列代碼可以用來(lái)說(shuō)明這個(gè)問(wèn)題。

function SuperType(){

this.colors = [\"red\", \"blue\", \"green\"];

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

第185頁(yè)

6.3 繼承 167

1

2

3

4

5

13

6

7

8

9

10

11

12

}

function SubType(){

}

//繼承了 SuperType

SubType.prototype = new SuperType();

var instance1 = new SubType();

instance1.colors.push(\"black\");

alert(instance1.colors); //\"red,blue,green,black\"

var instance2 = new SubType();

alert(instance2.colors); //\"red,blue,green,black\"

PrototypeChainingExample04.htm

這個(gè)例子中的 SuperType 構(gòu)造函數(shù)定義了一個(gè) colors 屬性,該屬性包含一個(gè)數(shù)組(引用類(lèi)型值)。

SuperType 的每個(gè)實(shí)例都會(huì)有各自包含自己數(shù)組的 colors 屬性。當(dāng) SubType 通過(guò)原型鏈繼承了

SuperType 之后,SubType.prototype 就變成了 SuperType 的一個(gè)實(shí)例,因此它也擁有了一個(gè)它自

己的 colors 屬性——就跟專門(mén)創(chuàng)建了一個(gè) SubType.prototype.colors 屬性一樣。但結(jié)果是什么

呢?結(jié)果是 SubType 的所有實(shí)例都會(huì)共享這一個(gè) colors 屬性。而我們對(duì) instance1.colors 的修改

能夠通過(guò) instance2.colors 反映出來(lái),就已經(jīng)充分證實(shí)了這一點(diǎn)。

原型鏈的第二個(gè)問(wèn)題是:在創(chuàng)建子類(lèi)型的實(shí)例時(shí),不能向超類(lèi)型的構(gòu)造函數(shù)中傳遞參數(shù)。實(shí)際上,

應(yīng)該說(shuō)是沒(méi)有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類(lèi)型的構(gòu)造函數(shù)傳遞參數(shù)。有鑒于此,再加上

前面剛剛討論過(guò)的由于原型中包含引用類(lèi)型值所帶來(lái)的問(wèn)題,實(shí)踐中很少會(huì)單獨(dú)使用原型鏈。

6.3.2 借用構(gòu)造函數(shù)

在解決原型中包含引用類(lèi)型值所帶來(lái)問(wèn)題的過(guò)程中,開(kāi)發(fā)人員開(kāi)始使用一種叫做借用構(gòu)造函數(shù)

(constructor stealing)的技術(shù)(有時(shí)候也叫做偽造對(duì)象或經(jīng)典繼承)。這種技術(shù)的基本思想相當(dāng)簡(jiǎn)單,即

在子類(lèi)型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類(lèi)型構(gòu)造函數(shù)。別忘了,函數(shù)只不過(guò)是在特定環(huán)境中執(zhí)行代碼的對(duì)象,

因此通過(guò)使用 apply()和 call()方法也可以在(將來(lái))新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù),如下所示:

function SuperType(){

this.colors = [\"red\", \"blue\", \"green\"];

}

function SubType(){

//繼承了 SuperType

SuperType.call(this);

}

var instance1 = new SubType();

instance1.colors.push(\"black\");

alert(instance1.colors); //\"red,blue,green,black\"

var instance2 = new SubType();

alert(instance2.colors); //\"red,blue,green\"

ConstructorStealingExample01.htm

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

第186頁(yè)

168 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

代碼中加粗的那一行代碼“借調(diào)”了超類(lèi)型的構(gòu)造函數(shù)。通過(guò)使用 call()方法(或 apply()方法

也可以),我們實(shí)際上是在(未來(lái)將要)新創(chuàng)建的 SubType 實(shí)例的環(huán)境下調(diào)用了 SuperType 構(gòu)造函數(shù)。

這樣一來(lái),就會(huì)在新 SubType 對(duì)象上執(zhí)行 SuperType()函數(shù)中定義的所有對(duì)象初始化代碼。結(jié)果,

SubType 的每個(gè)實(shí)例就都會(huì)具有自己的 colors 屬性的副本了。

1. 傳遞參數(shù)

相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大的優(yōu)勢(shì),即可以在子類(lèi)型構(gòu)造函數(shù)中向超類(lèi)型構(gòu)造函

數(shù)傳遞參數(shù)??聪旅孢@個(gè)例子。

function SuperType(name){

this.name = name;

}

function SubType(){

//繼承了 SuperType,同時(shí)還傳遞了參數(shù)

SuperType.call(this, \"Nicholas\");

//實(shí)例屬性

this.age = 29;

}

var instance = new SubType();

alert(instance.name); //\"Nicholas\";

alert(instance.age); //29

ConstructorStealingExample02.htm

以上代碼中的 SuperType 只接受一個(gè)參數(shù) name,該參數(shù)會(huì)直接賦給一個(gè)屬性。在 SubType 構(gòu)造

函數(shù)內(nèi)部調(diào)用 SuperType 構(gòu)造函數(shù)時(shí),實(shí)際上是為 SubType 的實(shí)例設(shè)置了 name 屬性。為了確保

SuperType 構(gòu)造函數(shù)不會(huì)重寫(xiě)子類(lèi)型的屬性,可以在調(diào)用超類(lèi)型構(gòu)造函數(shù)后,再添加應(yīng)該在子類(lèi)型中

定義的屬性。

2. 借用構(gòu)造函數(shù)的問(wèn)題

如果僅僅是借用構(gòu)造函數(shù),那么也將無(wú)法避免構(gòu)造函數(shù)模式存在的問(wèn)題——方法都在構(gòu)造函數(shù)中定

義,因此函數(shù)復(fù)用就無(wú)從談起了。而且,在超類(lèi)型的原型中定義的方法,對(duì)子類(lèi)型而言也是不可見(jiàn)的,結(jié)

果所有類(lèi)型都只能使用構(gòu)造函數(shù)模式??紤]到這些問(wèn)題,借用構(gòu)造函數(shù)的技術(shù)也是很少單獨(dú)使用的。

6.3.3 組合繼承

組合繼承(combination inheritance),有時(shí)候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的

技術(shù)組合到一塊,從而發(fā)揮二者之長(zhǎng)的一種繼承模式。其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方

法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,既通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)

復(fù)用,又能夠保證每個(gè)實(shí)例都有它自己的屬性。下面來(lái)看一個(gè)例子。

function SuperType(name){

this.name = name;

this.colors = [\"red\", \"blue\", \"green\"];

}

SuperType.prototype.sayName = function(){

alert(this.name);

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

第187頁(yè)

6.3 繼承 169

1

2

3

4

5

13

6

7

8

9

10

11

12

};

function SubType(name, age){

//繼承屬性

SuperType.call(this, name);

this.age = age;

}

//繼承方法

SubType.prototype = new SuperType();

SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function(){

alert(this.age);

};

var instance1 = new SubType(\"Nicholas\", 29);

instance1.colors.push(\"black\");

alert(instance1.colors); //\"red,blue,green,black\"

instance1.sayName(); //\"Nicholas\";

instance1.sayAge(); //29

var instance2 = new SubType(\"Greg\", 27);

alert(instance2.colors); //\"red,blue,green\"

instance2.sayName(); //\"Greg\";

instance2.sayAge(); //27

CombinationInheritanceExample01.htm

在這個(gè)例子中,SuperType 構(gòu)造函數(shù)定義了兩個(gè)屬性:name 和 colors。SuperType 的原型定義

了一個(gè)方法 sayName()。SubType 構(gòu)造函數(shù)在調(diào)用 SuperType 構(gòu)造函數(shù)時(shí)傳入了 name 參數(shù),緊接著

又定義了它自己的屬性 age。然后,將 SuperType 的實(shí)例賦值給 SubType 的原型,然后又在該新原型

上定義了方法 sayAge()。這樣一來(lái),就可以讓兩個(gè)不同的 SubType 實(shí)例既分別擁有自己屬性——包

括 colors 屬性,又可以使用相同的方法了。

組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為 JavaScript 中最常用的繼

承模式。而且,instanceof 和 isPrototypeOf()也能夠用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。

6.3.4 原型式繼承

道格拉斯·克羅克福德在 2006 年寫(xiě)了一篇文章,題為 Prototypal Inheritance in JavaScript (JavaScript

中的原型式繼承)。在這篇文章中,他介紹了一種實(shí)現(xiàn)繼承的方法,這種方法并沒(méi)有使用嚴(yán)格意義上的

構(gòu)造函數(shù)。他的想法是借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象,同時(shí)還不必因此創(chuàng)建自定義類(lèi)型。為

了達(dá)到這個(gè)目的,他給出了如下函數(shù)。

function object(o){

function F(){}

F.prototype = o;

return new F();

}

在 object()函數(shù)內(nèi)部,先創(chuàng)建了一個(gè)臨時(shí)性的構(gòu)造函數(shù),然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的

原型,最后返回了這個(gè)臨時(shí)類(lèi)型的一個(gè)新實(shí)例。從本質(zhì)上講,object()對(duì)傳入其中的對(duì)象執(zhí)行了一次

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

第188頁(yè)

170 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

淺復(fù)制。來(lái)看下面的例子。

var person = {

name: \"Nicholas\",

friends: [\"Shelby\", \"Court\", \"Van\"]

};

var anotherPerson = object(person);

anotherPerson.name = \"Greg\";

anotherPerson.friends.push(\"Rob\");

var yetAnotherPerson = object(person);

yetAnotherPerson.name = \"Linda\";

yetAnotherPerson.friends.push(\"Barbie\");

alert(person.friends); //\"Shelby,Court,Van,Rob,Barbie\"

PrototypalInheritanceExample01.htm

克羅克福德主張的這種原型式繼承,要求你必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)。如果有這么

一個(gè)對(duì)象的話,可以把它傳遞給 object()函數(shù),然后再根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可。在這

個(gè)例子中,可以作為另一個(gè)對(duì)象基礎(chǔ)的是 person 對(duì)象,于是我們把它傳入到 object()函數(shù)中,然后該

函數(shù)就會(huì)返回一個(gè)新對(duì)象。這個(gè)新對(duì)象將 person 作為原型,所以它的原型中就包含一個(gè)基本類(lèi)型值屬性

和一個(gè)引用類(lèi)型值屬性。這意味著 person.friends 不僅屬于 person 所有,而且也會(huì)被 anotherPerson

以及 yetAnotherPerson 共享。實(shí)際上,這就相當(dāng)于又創(chuàng)建了 person 對(duì)象的兩個(gè)副本。

ECMAScript 5 通過(guò)新增 Object.create()方法規(guī)范化了原型式繼承。這個(gè)方法接收兩個(gè)參數(shù):一

個(gè)用作新對(duì)象原型的對(duì)象和(可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象。在傳入一個(gè)參數(shù)的情況下,

Object.create()與 object()方法的行為相同。

var person = {

name: \"Nicholas\",

friends: [\"Shelby\", \"Court\", \"Van\"]

};

var anotherPerson = Object.create(person);

anotherPerson.name = \"Greg\";

anotherPerson.friends.push(\"Rob\");

var yetAnotherPerson = Object.create(person);

yetAnotherPerson.name = \"Linda\";

yetAnotherPerson.friends.push(\"Barbie\");

alert(person.friends); //\"Shelby,Court,Van,Rob,Barbie\"

PrototypalInheritanceExample02.htm

Object.create()方法的第二個(gè)參數(shù)與Object.defineProperties()方法的第二個(gè)參數(shù)格式相

同:每個(gè)屬性都是通過(guò)自己的描述符定義的。以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬

性。例如:

var person = {

name: \"Nicholas\",

friends: [\"Shelby\", \"Court\", \"Van\"]

};

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

第189頁(yè)

6.3 繼承 171

1

2

3

4

5

13

6

7

8

9

10

11

12

var anotherPerson = Object.create(person, {

name: {

value: \"Greg\"

}

});

alert(anotherPerson.name); //\"Greg\"

PrototypalInheritanceExample03.htm

支持 Object.create()方法的瀏覽器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。

在沒(méi)有必要興師動(dòng)眾地創(chuàng)建構(gòu)造函數(shù),而只想讓一個(gè)對(duì)象與另一個(gè)對(duì)象保持類(lèi)似的情況下,原型式

繼承是完全可以勝任的。不過(guò)別忘了,包含引用類(lèi)型值的屬性始終都會(huì)共享相應(yīng)的值,就像使用原型模

式一樣。

6.3.5 寄生式繼承

寄生式(parasitic)繼承是與原型式繼承緊密相關(guān)的一種思路,并且同樣也是由克羅克福德推而廣

之的。寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類(lèi)似,即創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù),該

函數(shù)在內(nèi)部以某種方式來(lái)增強(qiáng)對(duì)象,最后再像真地是它做了所有工作一樣返回對(duì)象。以下代碼示范了寄

生式繼承模式。

function createAnother(original){

var clone = object(original); //通過(guò)調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象

clone.sayHi = function(){ //以某種方式來(lái)增強(qiáng)這個(gè)對(duì)象

alert(\"hi\");

};

return clone; //返回這個(gè)對(duì)象

}

在這個(gè)例子中,createAnother()函數(shù)接收了一個(gè)參數(shù),也就是將要作為新對(duì)象基礎(chǔ)的對(duì)象。然

后,把這個(gè)對(duì)象(original)傳遞給 object()函數(shù),將返回的結(jié)果賦值給 clone。再為 clone 對(duì)象

添加一個(gè)新方法 sayHi(),最后返回 clone 對(duì)象??梢韵裣旅孢@樣來(lái)使用 createAnother()函數(shù):

var person = {

name: \"Nicholas\",

friends: [\"Shelby\", \"Court\", \"Van\"]

};

var anotherPerson = createAnother(person);

anotherPerson.sayHi(); //\"hi\"

這個(gè)例子中的代碼基于 person 返回了一個(gè)新對(duì)象——anotherPerson。新對(duì)象不僅具有 person

的所有屬性和方法,而且還有自己的 sayHi()方法。

在主要考慮對(duì)象而不是自定義類(lèi)型和構(gòu)造函數(shù)的情況下,寄生式繼承也是一種有用的模式。前面示

范繼承模式時(shí)使用的 object()函數(shù)不是必需的;任何能夠返回新對(duì)象的函數(shù)都適用于此模式。

使用寄生式繼承來(lái)為對(duì)象添加函數(shù),會(huì)由于不能做到函數(shù)復(fù)用而降低效率;這一

點(diǎn)與構(gòu)造函數(shù)模式類(lèi)似。

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

第190頁(yè)

172 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

6.3.6 寄生組合式繼承

前面說(shuō)過(guò),組合繼承是 JavaScript 最常用的繼承模式;不過(guò),它也有自己的不足。組合繼承最大的

問(wèn)題就是無(wú)論什么情況下,都會(huì)調(diào)用兩次超類(lèi)型構(gòu)造函數(shù):一次是在創(chuàng)建子類(lèi)型原型的時(shí)候,另一次是

在子類(lèi)型構(gòu)造函數(shù)內(nèi)部。沒(méi)錯(cuò),子類(lèi)型最終會(huì)包含超類(lèi)型對(duì)象的全部實(shí)例屬性,但我們不得不在調(diào)用子

類(lèi)型構(gòu)造函數(shù)時(shí)重寫(xiě)這些屬性。再來(lái)看一看下面組合繼承的例子。

function SuperType(name){

this.name = name;

this.colors = [\"red\", \"blue\", \"green\"];

}

SuperType.prototype.sayName = function(){

alert(this.name);

};

function SubType(name, age){

SuperType.call(this, name); //第二次調(diào)用 SuperType()

this.age = age;

}

SubType.prototype = new SuperType(); //第一次調(diào)用 SuperType()

SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function(){

alert(this.age);

};

加粗字體的行中是調(diào)用 SuperType 構(gòu)造函數(shù)的代碼。在第一次調(diào)用 SuperType 構(gòu)造函數(shù)時(shí),

SubType.prototype 會(huì)得到兩個(gè)屬性:name 和 colors;它們都是 SuperType 的實(shí)例屬性,只不過(guò)

現(xiàn)在位于 SubType 的原型中。當(dāng)調(diào)用 SubType 構(gòu)造函數(shù)時(shí),又會(huì)調(diào)用一次 SuperType 構(gòu)造函數(shù),這

一次又在新對(duì)象上創(chuàng)建了實(shí)例屬性 name 和 colors。于是,這兩個(gè)屬性就屏蔽了原型中的兩個(gè)同名屬

性。圖 6-6 展示了上述過(guò)程。

如圖 6-6 所示,有兩組 name 和 colors 屬性:一組在實(shí)例上,一組在 SubType 原型中。這就是調(diào)

用兩次 SuperType 構(gòu)造函數(shù)的結(jié)果。好在我們已經(jīng)找到了解決這個(gè)問(wèn)題方法——寄生組合式繼承。

所謂寄生組合式繼承,即通過(guò)借用構(gòu)造函數(shù)來(lái)繼承屬性,通過(guò)原型鏈的混成形式來(lái)繼承方法。其背

后的基本思路是:不必為了指定子類(lèi)型的原型而調(diào)用超類(lèi)型的構(gòu)造函數(shù),我們所需要的無(wú)非就是超類(lèi)型

原型的一個(gè)副本而已。本質(zhì)上,就是使用寄生式繼承來(lái)繼承超類(lèi)型的原型,然后再將結(jié)果指定給子類(lèi)型

的原型。寄生組合式繼承的基本模式如下所示。

function inheritPrototype(subType, superType){

var prototype = object(superType.prototype); //創(chuàng)建對(duì)象

prototype.constructor = subType; //增強(qiáng)對(duì)象

subType.prototype = prototype; //指定對(duì)象

}

這個(gè)示例中的 inheritPrototype()函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡(jiǎn)單形式。這個(gè)函數(shù)接收兩

個(gè)參數(shù):子類(lèi)型構(gòu)造函數(shù)和超類(lèi)型構(gòu)造函數(shù)。在函數(shù)內(nèi)部,第一步是創(chuàng)建超類(lèi)型原型的一個(gè)副本。第二

步是為創(chuàng)建的副本添加 constructor 屬性,從而彌補(bǔ)因重寫(xiě)原型而失去的默認(rèn)的 constructor 屬性。

最后一步,將新創(chuàng)建的對(duì)象(即副本)賦值給子類(lèi)型的原型。這樣,我們就可以用調(diào)用 inheritPrototype()函數(shù)的語(yǔ)句,去替換前面例子中為子類(lèi)型原型賦值的語(yǔ)句了,例如:

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

第191頁(yè)

6.3 繼承 173

1

2

3

4

5

13

6

7

8

9

10

11

12

function SuperType(name){

this.name = name;

this.colors = [\"red\", \"blue\", \"green\"];

}

SuperType.prototype.sayName = function(){

alert(this.name);

};

function SubType(name, age){

SuperType.call(this, name);

this.age = age;

}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){

alert(this.age);

};

ParasiticCombinationInheritanceExample01.htm

圖 6-6

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

第192頁(yè)

174 第 6 章 面向?qū)ο蟮某绦蛟O(shè)計(jì)

這個(gè)例子的高效率體現(xiàn)在它只調(diào)用了一次 SuperType 構(gòu)造函數(shù),并且因此避免了在 SubType.

prototype 上面創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用

instanceof 和 isPrototypeOf()。開(kāi)發(fā)人員普遍認(rèn)為寄生組合式繼承是引用類(lèi)型最理想的繼承范式。

YUI 的 YAHOO.lang.extend()方法采用了寄生組合繼承,從而讓這種模式首次

出現(xiàn)在了一個(gè)應(yīng)用非常廣泛的 JavaScript 庫(kù)中。要了解有關(guān) YUI 的更多信息,請(qǐng)?jiān)L問(wèn)

http://developer. yahoo.com/yui/。

6.4 小結(jié)

ECMAScript 支持面向?qū)ο螅∣O)編程,但不使用類(lèi)或者接口。對(duì)象可以在代碼執(zhí)行過(guò)程中創(chuàng)建和

增強(qiáng),因此具有動(dòng)態(tài)性而非嚴(yán)格定義的實(shí)體。在沒(méi)有類(lèi)的情況下,可以采用下列模式創(chuàng)建對(duì)象。

? 工廠模式,使用簡(jiǎn)單的函數(shù)創(chuàng)建對(duì)象,為對(duì)象添加屬性和方法,然后返回對(duì)象。這個(gè)模式后來(lái)

被構(gòu)造函數(shù)模式所取代。

? 構(gòu)造函數(shù)模式,可以創(chuàng)建自定義引用類(lèi)型,可以像創(chuàng)建內(nèi)置對(duì)象實(shí)例一樣使用 new 操作符。不

過(guò),構(gòu)造函數(shù)模式也有缺點(diǎn),即它的每個(gè)成員都無(wú)法得到復(fù)用,包括函數(shù)。由于函數(shù)可以不局

限于任何對(duì)象(即與對(duì)象具有松散耦合的特點(diǎn)),因此沒(méi)有理由不在多個(gè)對(duì)象間共享函數(shù)。

? 原型模式,使用構(gòu)造函數(shù)的 prototype 屬性來(lái)指定那些應(yīng)該共享的屬性和方法。組合使用構(gòu)造

函數(shù)模式和原型模式時(shí),使用構(gòu)造函數(shù)定義實(shí)例屬性,而使用原型定義共享的屬性和方法。

JavaScript 主要通過(guò)原型鏈實(shí)現(xiàn)繼承。原型鏈的構(gòu)建是通過(guò)將一個(gè)類(lèi)型的實(shí)例賦值給另一個(gè)構(gòu)造函

數(shù)的原型實(shí)現(xiàn)的。這樣,子類(lèi)型就能夠訪問(wèn)超類(lèi)型的所有屬性和方法,這一點(diǎn)與基于類(lèi)的繼承很相似。

原型鏈的問(wèn)題是對(duì)象實(shí)例共享所有繼承的屬性和方法,因此不適宜單獨(dú)使用。解決這個(gè)問(wèn)題的技術(shù)是借

用構(gòu)造函數(shù),即在子類(lèi)型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類(lèi)型構(gòu)造函數(shù)。這樣就可以做到每個(gè)實(shí)例都具有自己的

屬性,同時(shí)還能保證只使用構(gòu)造函數(shù)模式來(lái)定義類(lèi)型。使用最多的繼承模式是組合繼承,這種模式使用

原型鏈繼承共享的屬性和方法,而通過(guò)借用構(gòu)造函數(shù)繼承實(shí)例屬性。

此外,還存在下列可供選擇的繼承模式。

? 原型式繼承,可以在不必預(yù)先定義構(gòu)造函數(shù)的情況下實(shí)現(xiàn)繼承,其本質(zhì)是執(zhí)行對(duì)給定對(duì)象的淺

復(fù)制。而復(fù)制得到的副本還可以得到進(jìn)一步改造。

? 寄生式繼承,與原型式繼承非常相似,也是基于某個(gè)對(duì)象或某些信息創(chuàng)建一個(gè)對(duì)象,然后增強(qiáng)

對(duì)象,最后返回對(duì)象。為了解決組合繼承模式由于多次調(diào)用超類(lèi)型構(gòu)造函數(shù)而導(dǎo)致的低效率問(wèn)

題,可以將這個(gè)模式與組合繼承一起使用。

? 寄生組合式繼承,集寄生式繼承和組合繼承的優(yōu)點(diǎn)與一身,是實(shí)現(xiàn)基于類(lèi)型繼承的最有效方式。

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

第193頁(yè)

7.1 遞歸 175

1

2

3

4

5

13

6

7

8

9

10

11

12

函數(shù)表達(dá)式

本章內(nèi)容

? 函數(shù)表達(dá)式的特征

? 使用函數(shù)實(shí)現(xiàn)遞歸

? 使用閉包定義私有變量

數(shù)表達(dá)式是 JavaScript 中的一個(gè)既強(qiáng)大又容易令人困惑的特性。第 5 章曾介紹過(guò),定義函數(shù)的

方式有兩種:一種是函數(shù)聲明,另一種就是函數(shù)表達(dá)式。函數(shù)聲明的語(yǔ)法是這樣的。

function functionName(arg0, arg1, arg2) {

//函數(shù)體

}

首先是 function 關(guān)鍵字,然后是函數(shù)的名字,這就是指定函數(shù)名的方式。Firefox、Safari、Chrome

和 Opera 都給函數(shù)定義了一個(gè)非標(biāo)準(zhǔn)的 name 屬性,通過(guò)這個(gè)屬性可以訪問(wèn)到給函數(shù)指定的名字。這個(gè)

屬性的值永遠(yuǎn)等于跟在 function 關(guān)鍵字后面的標(biāo)識(shí)符。

//只在 Firefox、Safari、Chrome 和 Opera 有效

alert(functionName.name); //\"functionName\"

FunctionNameExample01.htm

關(guān)于函數(shù)聲明,它的一個(gè)重要特征就是函數(shù)聲明提升(function declaration hoisting),意思是在執(zhí)行

代碼之前會(huì)先讀取函數(shù)聲明。這就意味著可以把函數(shù)聲明放在調(diào)用它的語(yǔ)句后面。

sayHi();

function sayHi(){

alert(\"Hi!\");

}

FunctionDeclarationHoisting01.htm

這個(gè)例子不會(huì)拋出錯(cuò)誤,因?yàn)樵诖a執(zhí)行之前會(huì)先讀取函數(shù)聲明。

第二種創(chuàng)建函數(shù)的方式是使用函數(shù)表達(dá)式。函數(shù)表達(dá)式有幾種不同的語(yǔ)法形式。下面是最常見(jiàn)的一

種形式。

var functionName = function(arg0, arg1, arg2){

//函數(shù)體

};

第 7 章

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

第194頁(yè)

176 第 7 章 函數(shù)表達(dá)式

這種形式看起來(lái)好像是常規(guī)的變量賦值語(yǔ)句,即創(chuàng)建一個(gè)函數(shù)并將它賦值給變量 functionName。

這種情況下創(chuàng)建的函數(shù)叫做匿名函數(shù)(anonymous function),因?yàn)?function 關(guān)鍵字后面沒(méi)有標(biāo)識(shí)符。

(匿名函數(shù)有時(shí)候也叫拉姆達(dá)函數(shù)。)匿名函數(shù)的 name 屬性是空字符串。

函數(shù)表達(dá)式與其他表達(dá)式一樣,在使用前必須先賦值。以下代碼會(huì)導(dǎo)致錯(cuò)誤。

sayHi(); //錯(cuò)誤:函數(shù)還不存在

var sayHi = function(){

alert(\"Hi!\");

};

理解函數(shù)提升的關(guān)鍵,就是理解函數(shù)聲明與函數(shù)表達(dá)式之間的區(qū)別。例如,執(zhí)行以下代碼的結(jié)果可

能會(huì)讓人意想不到。

//不要這樣做!

if(condition){

function sayHi(){

alert(\"Hi!\");

}

} else {

function sayHi(){

alert(\"Yo!\");

}

}

FunctionDeclarationsErrorExample01.htm

表面上看,以上代碼表示在 condition 為 true 時(shí),使用一個(gè) sayHi()的定義;否則,就使用另

一個(gè)定義。實(shí)際上,這在 ECMAScript 中屬于無(wú)效語(yǔ)法,JavaScript 引擎會(huì)嘗試修正錯(cuò)誤,將其轉(zhuǎn)換為合

理的狀態(tài)。但問(wèn)題是瀏覽器嘗試修正錯(cuò)誤的做法并不一致。大多數(shù)瀏覽器會(huì)返回第二個(gè)聲明,忽略

condition;Firefox 會(huì)在 condition 為 true 時(shí)返回第一個(gè)聲明。因此這種使用方式很危險(xiǎn),不應(yīng)該

出現(xiàn)在你的代碼中。不過(guò),如果是使用函數(shù)表達(dá)式,那就沒(méi)有什么問(wèn)題了。

//可以這樣做

var sayHi;

if(condition){

sayHi = function(){

alert(\"Hi!\");

};

} else {

sayHi = function(){

alert(\"Yo!\");

};

}

這個(gè)例子不會(huì)有什么意外,不同的函數(shù)會(huì)根據(jù) condition 被賦值給 sayHi。

能夠創(chuàng)建函數(shù)再賦值給變量,也就能夠把函數(shù)作為其他函數(shù)的值返回。還記得第 5 章中的那個(gè)

createComparisonFunction()函數(shù)嗎:

function createComparisonFunction(propertyName) {

return function(object1, object2){

var value1 = object1[propertyName];

var value2 = object2[propertyName];

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

第195頁(yè)

7.1 遞歸 177

1

2

3

4

5

13

6

7

8

9

10

11

12

if (value1 < value2){

return -1;

} else if (value1 > value2){

return 1;

} else {

return 0;

}

};

}

createComparisonFunction()就返回了一個(gè)匿名函數(shù)。返回的函數(shù)可能會(huì)被賦值給一個(gè)變量,

或者以其他方式被調(diào)用;不過(guò),在 createComparisonFunction()函數(shù)內(nèi)部,它是匿名的。在把函數(shù)

當(dāng)成值來(lái)使用的情況下,都可以使用匿名函數(shù)。不過(guò),這并不是匿名函數(shù)唯一的用途。

7.1 遞歸

遞歸函數(shù)是在一個(gè)函數(shù)通過(guò)名字調(diào)用自身的情況下構(gòu)成的,如下所示。

function factorial(num){

if (num <= 1){

return 1;

} else {

return num * factorial(num-1);

}

}

RecursionExample01.htm

這是一個(gè)經(jīng)典的遞歸階乘函數(shù)。雖然這個(gè)函數(shù)表面看來(lái)沒(méi)什么問(wèn)題,但下面的代碼卻可能導(dǎo)致它出錯(cuò)。

var anotherFactorial = factorial;

factorial = null;

alert(anotherFactorial(4)); //出錯(cuò)!

RecursionExample01.htm

以上代碼先把 factorial()函數(shù)保存在變量 anotherFactorial 中,然后將 factorial 變量設(shè)

置為 null,結(jié)果指向原始函數(shù)的引用只剩下一個(gè)。但在接下來(lái)調(diào)用 anotherFactorial()時(shí),由于必

須執(zhí)行 factorial(),而 factorial 已經(jīng)不再是函數(shù),所以就會(huì)導(dǎo)致錯(cuò)誤。在這種情況下,使用 arguments.callee 可以解決這個(gè)問(wèn)題。

我們知道,arguments.callee 是一個(gè)指向正在執(zhí)行的函數(shù)的指針,因此可以用它來(lái)實(shí)現(xiàn)對(duì)函數(shù)

的遞歸調(diào)用,例如:

function factorial(num){

if (num <= 1){

return 1;

} else {

return num * arguments.callee(num-1);

}

}

RecursionExample02.htm

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

第196頁(yè)

178 第 7 章 函數(shù)表達(dá)式

加粗的代碼顯示,通過(guò)使用 arguments.callee 代替函數(shù)名,可以確保無(wú)論怎樣調(diào)用函數(shù)都不會(huì)

出問(wèn)題。因此,在編寫(xiě)遞歸函數(shù)時(shí),使用 arguments.callee 總比使用函數(shù)名更保險(xiǎn)。

但在嚴(yán)格模式下,不能通過(guò)腳本訪問(wèn) arguments.callee,訪問(wèn)這個(gè)屬性會(huì)導(dǎo)致錯(cuò)誤。不過(guò),可

以使用命名函數(shù)表達(dá)式來(lái)達(dá)成相同的結(jié)果。例如:

var factorial = (function f(num){

if (num <= 1){

return 1;

} else {

return num * f(num-1);

}

});

以上代碼創(chuàng)建了一個(gè)名為 f()的命名函數(shù)表達(dá)式,然后將它賦值給變量 factorial。即便把函數(shù)

賦值給了另一個(gè)變量,函數(shù)的名字 f 仍然有效,所以遞歸調(diào)用照樣能正確完成。這種方式在嚴(yán)格模式和

非嚴(yán)格模式下都行得通。

7.2 閉包

有不少開(kāi)發(fā)人員總是搞不清匿名函數(shù)和閉包這兩個(gè)概念,因此經(jīng)?;煊?。閉包是指有權(quán)訪問(wèn)另一個(gè)

函數(shù)作用域中的變量的函數(shù)。創(chuàng)建閉包的常見(jiàn)方式,就是在一個(gè)函數(shù)內(nèi)部創(chuàng)建另一個(gè)函數(shù),仍以前面的

createComparisonFunction()函數(shù)為例,注意加粗的代碼。

function createComparisonFunction(propertyName) {

return function(object1, object2){

var value1 = object1[propertyName];

var value2 = object2[propertyName];

if (value1 < value2){

return -1;

} else if (value1 > value2){

return 1;

} else {

return 0;

}

};

}

在這個(gè)例子中,突出的那兩行代碼是內(nèi)部函數(shù)(一個(gè)匿名函數(shù))中的代碼,這兩行代碼訪問(wèn)了外部

函數(shù)中的變量 propertyName。即使這個(gè)內(nèi)部函數(shù)被返回了,而且是在其他地方被調(diào)用了,但它仍然可

以訪問(wèn)變量 propertyName。之所以還能夠訪問(wèn)這個(gè)變量,是因?yàn)閮?nèi)部函數(shù)的作用域鏈中包含

createComparisonFunction()的作用域。要徹底搞清楚其中的細(xì)節(jié),必須從理解函數(shù)被調(diào)用的時(shí)候

都會(huì)發(fā)生什么入手。

第 4 章介紹了作用域鏈的概念。而有關(guān)如何創(chuàng)建作用域鏈以及作用域鏈有什么作用的細(xì)節(jié),對(duì)徹底

理解閉包至關(guān)重要。當(dāng)某個(gè)函數(shù)被調(diào)用時(shí),會(huì)創(chuàng)建一個(gè)執(zhí)行環(huán)境(execution context)及相應(yīng)的作用域鏈。

然后,使用 arguments 和其他命名參數(shù)的值來(lái)初始化函數(shù)的活動(dòng)對(duì)象(activation object)。但在作用域

鏈中,外部函數(shù)的活動(dòng)對(duì)象始終處于第二位,外部函數(shù)的外部函數(shù)的活動(dòng)對(duì)象處于第三位,……直至作

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

第197頁(yè)

7.2 閉包 179

1

2

3

4

5

13

6

7

8

9

10

11

12

為作用域鏈終點(diǎn)的全局執(zhí)行環(huán)境。

在函數(shù)執(zhí)行過(guò)程中,為讀取和寫(xiě)入變量的值,就需要在作用域鏈中查找變量。來(lái)看下面的例子。

function compare(value1, value2){

if (value1 < value2){

return -1;

} else if (value1 > value2){

return 1;

} else {

return 0;

}

}

var result = compare(5, 10);

以上代碼先定義了 compare()函數(shù),然后又在全局作用域中調(diào)用了它。當(dāng)調(diào)用 compare()時(shí),會(huì)

創(chuàng)建一個(gè)包含 arguments、value1 和 value2 的活動(dòng)對(duì)象。全局執(zhí)行環(huán)境的變量對(duì)象(包含 result

和 compare)在 compare()執(zhí)行環(huán)境的作用域鏈中則處于第二位。圖 7-1 展示了包含上述關(guān)系的

compare()函數(shù)執(zhí)行時(shí)的作用域鏈。

圖 7-1

后臺(tái)的每個(gè)執(zhí)行環(huán)境都有一個(gè)表示變量的對(duì)象——變量對(duì)象。全局環(huán)境的變量對(duì)象始終存在,而像

compare()函數(shù)這樣的局部環(huán)境的變量對(duì)象,則只在函數(shù)執(zhí)行的過(guò)程中存在。在創(chuàng)建 compare()函數(shù)

時(shí),會(huì)創(chuàng)建一個(gè)預(yù)先包含全局變量對(duì)象的作用域鏈,這個(gè)作用域鏈被保存在內(nèi)部的[[Scope]]屬性中。

當(dāng)調(diào)用 compare()函數(shù)時(shí),會(huì)為函數(shù)創(chuàng)建一個(gè)執(zhí)行環(huán)境,然后通過(guò)復(fù)制函數(shù)的[[Scope]]屬性中的對(duì)

象構(gòu)建起執(zhí)行環(huán)境的作用域鏈。此后,又有一個(gè)活動(dòng)對(duì)象(在此作為變量對(duì)象使用)被創(chuàng)建并被推入執(zhí)

行環(huán)境作用域鏈的前端。對(duì)于這個(gè)例子中 compare()函數(shù)的執(zhí)行環(huán)境而言,其作用域鏈中包含兩個(gè)變

量對(duì)象:本地活動(dòng)對(duì)象和全局變量對(duì)象。顯然,作用域鏈本質(zhì)上是一個(gè)指向變量對(duì)象的指針列表,它只

引用但不實(shí)際包含變量對(duì)象。

無(wú)論什么時(shí)候在函數(shù)中訪問(wèn)一個(gè)變量時(shí),就會(huì)從作用域鏈中搜索具有相應(yīng)名字的變量。一般來(lái)講,

當(dāng)函數(shù)執(zhí)行完畢后,局部活動(dòng)對(duì)象就會(huì)被銷(xiāo)毀,內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對(duì)象)。

但是,閉包的情況又有所不同。

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

第198頁(yè)

180 第 7 章 函數(shù)表達(dá)式

在另一個(gè)函數(shù)內(nèi)部定義的函數(shù)會(huì)將包含函數(shù)(即外部函數(shù))的活動(dòng)對(duì)象添加到它的作用域鏈中。因

此,在 createComparisonFunction()函數(shù)內(nèi)部定義的匿名函數(shù)的作用域鏈中,實(shí)際上將會(huì)包含外部

函數(shù) createComparisonFunction()的活動(dòng)對(duì)象。圖 7-2 展示了當(dāng)下列代碼執(zhí)行時(shí),包含函數(shù)與內(nèi)部

匿名函數(shù)的作用域鏈。

var compare = createComparisonFunction(\"name\");

var result = compare({ name: \"Nicholas\" }, { name: \"Greg\" });

在匿名函數(shù)從 createComparisonFunction()中被返回后,它的作用域鏈被初始化為包含

createComparisonFunction()函數(shù)的活動(dòng)對(duì)象和全局變量對(duì)象。這樣,匿名函數(shù)就可以訪問(wèn)在

createComparisonFunction()中定義的所有變量。更為重要的是,createComparisonFunction()

函數(shù)在執(zhí)行完畢后,其活動(dòng)對(duì)象也不會(huì)被銷(xiāo)毀,因?yàn)槟涿瘮?shù)的作用域鏈仍然在引用這個(gè)活動(dòng)對(duì)象。換

句話說(shuō),當(dāng) createComparisonFunction()函數(shù)返回后,其執(zhí)行環(huán)境的作用域鏈會(huì)被銷(xiāo)毀,但它的活

動(dòng)對(duì)象仍然會(huì)留在內(nèi)存中;直到匿名函數(shù)被銷(xiāo)毀后,createComparisonFunction()的活動(dòng)對(duì)象才會(huì)

被銷(xiāo)毀,例如:

//創(chuàng)建函數(shù)

var compareNames = createComparisonFunction(\"name\");

//調(diào)用函數(shù)

var result = compareNames({ name: \"Nicholas\" }, { name: \"Greg\" });

//解除對(duì)匿名函數(shù)的引用(以便釋放內(nèi)存)

compareNames = null;

首先,創(chuàng)建的比較函數(shù)被保存在變量compareNames 中。而通過(guò)將compareNames 設(shè)置為等于null

解除該函數(shù)的引用,就等于通知垃圾回收例程將其清除。隨著匿名函數(shù)的作用域鏈被銷(xiāo)毀,其他作用域

(除了全局作用域)也都可以安全地銷(xiāo)毀了。圖 7-2 展示了調(diào)用 compareNames()的過(guò)程中產(chǎn)生的作用

域鏈之間的關(guān)系。

圖 7-2

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

第199頁(yè)

7.2 閉包 181

1

2

3

4

5

13

6

7

8

9

10

11

12

由于閉包會(huì)攜帶包含它的函數(shù)的作用域,因此會(huì)比其他函數(shù)占用更多的內(nèi)存。過(guò)

度使用閉包可能會(huì)導(dǎo)致內(nèi)存占用過(guò)多,我們建議讀者只在絕對(duì)必要時(shí)再考慮使用閉

包。雖然像 V8 等優(yōu)化后的 JavaScript 引擎會(huì)嘗試回收被閉包占用的內(nèi)存,但請(qǐng)大家

還是要慎重使用閉包。

7.2.1 閉包與變量

作用域鏈的這種配置機(jī)制引出了一個(gè)值得注意的副作用,即閉包只能取得包含函數(shù)中任何變量的最

后一個(gè)值。別忘了閉包所保存的是整個(gè)變量對(duì)象,而不是某個(gè)特殊的變量。下面這個(gè)例子可以清晰地說(shuō)

明這個(gè)問(wèn)題。

function createFunctions(){

var result = new Array();

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

result[i] = function(){

return i;

};

}

return result;

}

ClosureExample01.htm

這個(gè)函數(shù)會(huì)返回一個(gè)函數(shù)數(shù)組。表面上看,似乎每個(gè)函數(shù)都應(yīng)該返自己的索引值,即位置 0 的函數(shù)

返回 0,位置 1 的函數(shù)返回 1,以此類(lèi)推。但實(shí)際上,每個(gè)函數(shù)都返回 10。因?yàn)槊總€(gè)函數(shù)的作用域鏈中

都保存著 createFunctions() 函數(shù)的活動(dòng)對(duì)象,所以它們引用的都是同一個(gè)變量 i 。 當(dāng)

createFunctions()函數(shù)返回后,變量 i 的值是 10,此時(shí)每個(gè)函數(shù)都引用著保存變量 i 的同一個(gè)變量

對(duì)象,所以在每個(gè)函數(shù)內(nèi)部 i 的值都是 10。但是,我們可以通過(guò)創(chuàng)建另一個(gè)匿名函數(shù)強(qiáng)制讓閉包的行為

符合預(yù)期,如下所示。

function createFunctions(){

var result = new Array();

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

result[i] = function(num){

return function(){

return num;

};

}(i);

}

return result;

}

ClosureExample02.htm

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

第200頁(yè)

182 第 7 章 函數(shù)表達(dá)式

在重寫(xiě)了前面的 createFunctions()函數(shù)后,每個(gè)函數(shù)就會(huì)返回各自不同的索引值了。在這個(gè)版

本中,我們沒(méi)有直接把閉包賦值給數(shù)組,而是定義了一個(gè)匿名函數(shù),并將立即執(zhí)行該匿名函數(shù)的結(jié)果賦

給數(shù)組。這里的匿名函數(shù)有一個(gè)參數(shù) num,也就是最終的函數(shù)要返回的值。在調(diào)用每個(gè)匿名函數(shù)時(shí),我

們傳入了變量 i。由于函數(shù)參數(shù)是按值傳遞的,所以就會(huì)將變量 i 的當(dāng)前值復(fù)制給參數(shù) num。而在這個(gè)

匿名函數(shù)內(nèi)部,又創(chuàng)建并返回了一個(gè)訪問(wèn) num 的閉包。這樣一來(lái),result 數(shù)組中的每個(gè)函數(shù)都有自己

num 變量的一個(gè)副本,因此就可以返回各自不同的數(shù)值了。

7.2.2 關(guān)于this對(duì)象

在閉包中使用 this 對(duì)象也可能會(huì)導(dǎo)致一些問(wèn)題。我們知道,this 對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)

行環(huán)境綁定的:在全局函數(shù)中,this 等于 window,而當(dāng)函數(shù)被作為某個(gè)對(duì)象的方法調(diào)用時(shí),this 等

于那個(gè)對(duì)象。不過(guò),匿名函數(shù)的執(zhí)行環(huán)境具有全局性,因此其 this 對(duì)象通常指向 window①。但有時(shí)候

由于編寫(xiě)閉包的方式不同,這一點(diǎn)可能不會(huì)那么明顯。下面來(lái)看一個(gè)例子。

var name = \"The Window\";

var object = {

name : \"My Object\",

getNameFunc : function(){

return function(){

return this.name;

};

}

};

alert(object.getNameFunc()()); //\"The Window\"(在非嚴(yán)格模式下)

ThisObjectExample01.htm

以上代碼先創(chuàng)建了一個(gè)全局變量 name,又創(chuàng)建了一個(gè)包含 name 屬性的對(duì)象。這個(gè)對(duì)象還包含一

個(gè)方法——getNameFunc(),它返回一個(gè)匿名函數(shù),而匿名函數(shù)又返回 this.name。由于 getNameFunc()

返回一個(gè)函數(shù),因此調(diào)用 object.getNameFunc()()就會(huì)立即調(diào)用它返回的函數(shù),結(jié)果就是返回一個(gè)

字符串。然而,這個(gè)例子返回的字符串是\"The Window\",即全局 name 變量的值。為什么匿名函數(shù)沒(méi)

有取得其包含作用域(或外部作用域)的 this 對(duì)象呢?

前面曾經(jīng)提到過(guò),每個(gè)函數(shù)在被調(diào)用時(shí)都會(huì)自動(dòng)取得兩個(gè)特殊變量:this 和 arguments。內(nèi)部函

數(shù)在搜索這兩個(gè)變量時(shí),只會(huì)搜索到其活動(dòng)對(duì)象為止,因此永遠(yuǎn)不可能直接訪問(wèn)外部函數(shù)中的這兩個(gè)變

量(這一點(diǎn)通過(guò)圖 7-2 可以看得更清楚)。不過(guò),把外部作用域中的 this 對(duì)象保存在一個(gè)閉包能夠訪問(wèn)

到的變量里,就可以讓閉包訪問(wèn)該對(duì)象了,如下所示。

var name = \"The Window\";

var object = {

name : \"My Object\",

getNameFunc : function(){

——————————

① 當(dāng)然,在通過(guò) call()或 apply()改變函數(shù)執(zhí)行環(huán)境的情況下,this 就會(huì)指向其他對(duì)象。

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

百萬(wàn)用戶使用云展網(wǎng)進(jìn)行書(shū)冊(cè)翻頁(yè)效果制作,只要您有文檔,即可一鍵上傳,自動(dòng)生成鏈接和二維碼(獨(dú)立電子書(shū)),支持分享到微信和網(wǎng)站!
收藏
轉(zhuǎn)發(fā)
下載
免費(fèi)制作
其他案例
更多案例
免費(fèi)制作
x
{{item.desc}}
下載
{{item.title}}
{{toast}}