面的文章我們已經介紹了構造函數模式和原型模式,接下來我們將介紹剩下的其他幾種模式。
創建自定義類型的最常見方式,就是組合使用構造函數模式與原型模式。構造函數模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性。結果,每個實例都會有自己的一份實例屬性的副本,但同時又共享著對方法的引用,最大限度地節省了內存。另外,這種混合模式還支持像構造函數傳遞參數。例:
構造函數模式與原型模式組合
在上面的例子的中,實例屬性都是在構造函數中定義的,而由所有實例共享屬性 constructor 和方法 sayName() 則是在原型中定義的。而修改了 p1.friends 屬性,并不會影響到 p2.friends 屬性,因為它們分別引用了不同的數組。這種構造函數與原型混合模式,是目前ECMAScript 中使用最廣泛,認同度最高的一種創建自定義類型的方法。
動態原型模式是把所有信息都封裝在構造函數中,而通過在構造函數中初始化原型(僅在必要的情況下),它保持了同時使用構造函數和原型的優點。也就是說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。例:
動態原型模式
上面的代碼示例中對原型所做的修改,能夠立即在所有實例中得到反映。不過需要注意的是:使用動態原型模式,不能使用對象字面量重寫原型(具體原因,請看上一篇文章)。
通常,在前面幾種模式都不適用的情況下,可以使用寄生構造函數模式。這種模式的基本思想是創建一個函數,該函數的作用僅僅是封裝創建對象的代碼,然后再返回新創建的對象,但從表面上看,這個函數又很像是典型的構造函數。例:
寄生構造函數模式
上面的代碼演示了寄生構造函數模式,除了使用 new 操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式是一摸一樣的。這種模式可以在特殊的情況下用來為對象創建構造函數。假如我們想創建一個具有額外方法的特殊數組,由于不能直接修改 Array 構造函數,因此我們可以使用這種模式。例:
寄生構造函數模式用例
關于寄生構造函數模式需要說明的是,返回的對象與構造函數或者構造函數的原型屬性之間沒有關系,也就是說,構造函數返回的對象與在構造函數外部創建的對象沒有什么不同。因此,不能通過 instanceof 操作符來確定對象類型。由于存在上述問題,在能使用其他模式的情況下,盡量不用使用這種模式。
介紹這個模式之前,我們先來簡單介紹一下什么是穩妥對象。所謂穩妥對象,指的是沒有公共屬性,而且其方法也不能引用this的對象。穩妥對象最適合在一些安全的環境中,或者在防止數據被其他應用程序修改時使用。穩妥構造函數模式與寄生構造函數模式類似,但是又有著不同:首先,新創建對象的實例方法不引用this,其次,不使用 new 操作符調用構造函數。例:
穩妥構造函數模式
上面的代碼演示了穩妥構造函數模式,變量 p 中保存的是一個穩妥對象,除了調用 sayName() 方法外,沒有別的方式可以訪問其數據成功。即使有其他的代碼會給這個對象添加方法或數據成員,但也不可能有別的辦法訪問傳入到構造函數中的原始數據。穩妥構造函數模式提供的這種安全性,使得它非常適合在某些安全執行環境。穩妥構造函數模式存在與寄生構造函數模式一樣的問題。
們創建的每個函數都有一個 prototype 屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說,不必在構造函數中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中,例:
prototype 屬性示例
在上面的代碼中,我們將 sayName() 方法和所有屬性直接添加到了 Person 的 prototype 屬性中,構造函數變成了空函數。即使如此,也仍然可以通過調用構造函數來創建新對象,而且新對象還具有相同的屬性和方法。但與構造函數模式不同的是,新對象的這些屬性和方法是所有實例共享的。也就是說,p1 和 p2 訪問的都是同一組屬性和同一個 sayName() 函數。
無論什么時候,只要創建了一個函數,就會根據一組特定的規則為該函數創建一個 prototype 屬性,這個屬性指向函數的原型對象。在默認情況下,所有的原型對象都會自動獲得一個 constructor 屬性,這個屬性是一個指向 prototype屬性所在函數的指針。在前面的例子中,Person.prototype.constructor 指向 Person 。而通過這個構造函數,我們還可繼續為原型對象添加其他屬性和方法。
創建了自定義的構造函數之后,其原型對象默認只會取得 constructor 屬性;至于其他方法,都是從 Object 繼承而來。當調用構造函數創建一個新實例后,該實例的內部包含一個指針,指向構造函數的原型對象。ECMA 第五版中管這個指針叫 [[Prototype]] ,這個連接存在與實例與構造函數的原型對象之間,而不是存在與實例與構造函數之間。
Person 對象和 Person.prototype 創建實例對象關系
上圖展示了 Person 構造函數、Person 的原型屬性以及 Person 現有的兩個實例之間的關系。Person.prototype 指向了原型對象,而 Person.prototype.constructor 又指會了 Person。原型對象中除了 constructor 屬性外,還包括后來添加的其他屬性。Person 的每個實例 p1 和 p2 都包含了一個內部屬性,該屬性僅僅指向了原型對象,也就是說,它們和構造函數沒有直接的關系。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但我們卻可以調用 p1.sayName(),這是通過查找對象屬性的過程來實現的。
雖然所有實現中都無法訪問到 [[Prototype]] ,但是可以通過 isPrototypeOf() 方法來確定對象之間是否存在這種關系。從本質上講,如果 [[Prototype]] 指向 isPrototypeOf() 方法的對象,那么這個方法返回 true,如下:
isPrototypeOf() 方法示例
另外,ECMAScript 5 中增加了一個新方法 Object.getPrototypeOf() ,這個方法返回 [[Prototype]] 的值,例:
Object.getPrototypeOf() 方法
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性。搜索首先從對象實例本身開始。如果在實例中找到了具有給定名字的屬性,則返回該屬性的值,如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到了這個屬性,就返回該屬性的值。也就是說,在我們調用 p1.sayName() 的時候,會先后執行兩次搜索,首先會搜索實例 p1 是否有 sayName 屬性,沒有,繼續搜索 p1 的原型是否有 sayName 屬性,有,它就讀取那個保存在原型對象中的函數。
雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例去重寫原型中的值。如果我們在實例中添加一個與原型中同名的屬性,那么該屬性將會屏蔽原型中的那個屬性。使用 delete 操作符可以刪除實例屬性,從而恢復對原型中屬性的訪問,例:
實例對象屬性和原型屬性操作訪問
使用 hasOwnProperty() 方法可以檢測一個屬性是存在于實例中,還是存在于原型中。
hasOwnProperty() 方法
有兩種方式使用 in 操作符:單獨使用和在 for-in 循環中使用。單獨使用時,in 操作符會在通過對象能夠訪問給定屬性時返回 true,無論該屬性存在于實例還是原型中。在使用 for-in 循環時,返回的是所有能夠通過對象訪問的,可枚舉的屬性,其中既包括存在于實例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]] 標記為false的屬性)的實例屬性也會在 for-in 循環中返回。
要取得對象上所有可枚舉的實例屬性,可以使用 ECMAScript 5 中的 Object.keys() 方法,該方法接收一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。
前面的例子中,每添加一個屬性和方法都要寫一遍 Person.prototype 。為了減少不必要的輸入,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象,如下:
簡化后的原型語法
在上面的代碼中,將Person.prototype 設置為一個對象字面量形式創建的新對象。最終的結果相同,但有一個例外,constructor 屬性不再指向 Person 了。前面介紹過,每創建一個函數,就會同時創建它的 prototype 對象,這個對象也會自動獲得 constructor 屬性。我們在這里使用的語法,本質上完全重寫了默認 prototype 對象,因此 constructor 屬性就變成了新對象的 constructor 屬性(指向 Object 構造函數),不再指向 Person 函數。
constructor 屬性被改變了
如果 constructor 屬性真的很重要的話,可以將它的值回寫:
回寫 constructor 屬性
注意:上面的代碼會導致 constructor 的 [[Enumerable]] 特性被設置為 true。默認情況下,原生的 constructor 屬性是不可枚舉的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引擎,可以使用 Object.defineProperty() 來修改此特性。
重設構造函數
我們已經知道了在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都會立即反映到實例對象上。
修改原型對象后實例對象的表現
盡管可以隨時為原型添加屬性和方法,并且修改能立即在所有對象實例中反映出來,但是如果重寫了整個原型對象,結果就不一樣了。調用構造函數時會為實例添加一個指向最初始原型的指針,而把原型修改為另一個對象就等于切斷了構造函數與最初始原型之間的的聯系。
修改整個原型對象
原型模式的最大問題是由其共享的本性所導致的。原型中所有屬性是被很多實例共享,這種共享對于函數非常合適,對于那些包含基本值的屬性也還行,通過在實例上添加同名屬性,可以屏蔽原型中對應屬性,然而,對于包含引用類型值的屬性來說,就會出問題:
原型對象問題
CMAScript 是 JavaScript 的核心,但是如果要在 Web 中使用 JavaScript,那么 BOM(browser object model 瀏覽器對象模型) 才是真正的核心。BOM 提供了很多對象,用于訪問瀏覽器的功能,這些功能與任何網頁內容無關。
BOM 的核心對象是 window,它表示一個瀏覽器的實例。在瀏覽器中,window 對象有雙重角色,它既是通過 JavaScript 訪問瀏覽器窗口的一個接口,又是 ECMAScript 規定的 Global 對象。這意味著在網頁中定義的任何一個對象、變量和函數,都以 window 作為其 Global 對象。
由于 window 對象同時扮演著 ECMAScript 中 Global 對象的角色,因此所有在全局作用域中聲明的變量、函數都會變成 window 對象的屬性和方法。例:
在全局作用域上定義的屬性和方法都會變成window對象的屬性和方法
上面的例子中,在全局作用域中定義了一個變量 age 和 一個函數 sayAge() ,它們會被自動歸在了 window 對象上。于是,使用 window.age 和 window.sayAge() 可以訪問這個變量和這個函數。
撇開全局變量會成為 window 對象的屬性不談,定義全局變量與在 window 對象上直接定義屬性還是有一點區別的:全局變量不能通過 delete 操作符刪除,而直接在 window 對象上定義的屬性則可以 。 例:
定義全局變量和定義 window 對象上屬性的區別
使用 var 語句添加的 window 屬性有一個名為 [[Configurable]] 的特性,這個特性的值被設置成 false,因此這樣定義的屬性是不能通過 delete 操作符刪除的。IE9 之前的版本在使用 delete 刪除 window 屬性語句時,不管該屬性最初是如何創建的,都會拋出錯誤。另外,嘗試訪問未聲明的變量會拋出錯誤,但是通過查詢 window 對象,可以知道某個可能未聲明的變量是否存在。例:
*請認真填寫需求信息,我們會在24小時內與您取得聯系。