對象:無序屬性的集合,其屬性可以包含基本值、對象或者函數。無非就是一組名值對
一、理解對象
ECMA-262 第五版在定義只有內部才用的特性時,描述了屬性的各種特征。定義這些特性是為了實現 JavaScript 引擎用的,因此在 JavaScript 中不能直接訪問它們。
有兩種內部屬性:數據屬性和訪問器屬性。
數據屬性:configurable、enumerable、writable 和 value。
訪問器屬性:configurable、enumerable、get 和 set。
要修改屬性默認的特性:Object.defineProperty() 方法
定義多個屬性:Object.defineProperties() 方法
讀取屬性的特性:Object.getOwnPropertyDescriptor() 方法
二、創建對象
雖然 Object 構造函數或對象字面量都可以用來創建單個對象,但這些方式有個明顯的缺點:使用同一個接口創建很多對象,會產生大量的重復的代碼。為解決這個問題,人們開始使用工廠模式的一種變體。
1. 工廠模式
工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
2. 構造函數模式
構造函數可用來創建特定類型的對象。像 Object 和 Array 這樣的原生構造函數,在運行時會自動出現在執行環境中。此外,也可以創建自定義的構造函數,從而定義自定義對象類型的屬性的方法。例如,可以使用構造函數模式將前面的例子重寫如下。
按照慣例,構造函數始終都應該以一個大寫字母開頭。構造函數本身也是函數,只不過可以用來創建對象而已。
要創建 Person 的新實例,必須使用 new 操作符。
在前面的例子的最后,person1 和 person2 分別保存著 Person 的一個不同的實例。這兩個對象都有一個 constructor(構造函數)屬性,該屬性指向 Person,如下所示
使用構造函數的主要問題,就是每個方法都要在每個實例上重新創建一遍。在前面的例子中,person1 和person2 都有一個名為 sayName() 的方法,但那兩個方法不是同一個 Function 的實例。不要忘了——ECMAScript 中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。不同實例上的同名函數是不相等的。
通過把函數定義轉移到構造函數外部來解決這個問題
這樣做確實解決來兩個函數做同一件事的問題,可是新問題又來了:在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那么就要定義很多個全局函數,于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型模式來解決。
3. 原型模式
我們創建的每個函數都有一個 prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。如果按照字面意思來理解,那么 prototype 就是通過調用構造函數而創建的那個對象實例的原型對象。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。
理解原型對象
在默認情況下,所有原型對象都會自動獲得一個 constructor(構造函數)屬性,這個屬性是一個指向 prototype 屬性所在函數的指針。就拿前面的例子來說,Person.prototype.constructor 指向 Person。
當調用構造函數創建一個新實例后,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。這個指針叫 [[Prototype]]。
雖然在腳本中沒有標準的方式訪問這個屬性,但 Firefox、Safari 和 Chrome 在每個對象上都支持一個屬性 _proto_ ;而在其他實現中,這個屬性對腳本則是完全不可見的。不過,要明確的真正重要的一點就是,這個連接存在于實例與構造函數的原型對象之間,而不是存在于實例與構造函數之間。
雖然在所有實現中都無法訪問到?[[Prototype]],但可以通過 isPrototypeOf() 方法來確定對象之間是否存在這種關系。從本質上講,如果?[[Prototype]] 指向調用?isPrototypeOf() 方法的對象(Person.prototype),那么這個方法就返回 true。
ECMAScript5 增加了一個新方法,叫 Object.getPrototypeOf(),在所有支持的實現中,這個方法返回?[[Prototype]] 的值。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標時具有給定名字的屬性。搜索首先從對象實例本身開始。如果在實例中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性的值。這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
當為對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性;換句話說,添加這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設置為 null,也只會在實例中設置這個屬性,而不會恢復其指向原型的連接。不過使用 delete 操作符則可以完全刪除實例屬性,從而讓我們能夠重新訪問原型中屬性。
使用 hasOwnProperty() 方法可以檢測一個屬性是存在于實例中,還是存在于原型中。這個方法只在給定屬性存在對象實例中時,才會返回 true。
原型與 in 操作符
有兩種方式使用 in 操作符:單獨使用和在 for-in 循環中使用。在單獨使用時,in 操作符會在通過對象能夠訪問給定屬性時返回 true,無論該屬性存在于實例中還是原型中。
在使用 for-in 循環時,返回的是所有能夠通過對象訪問的、可枚舉的屬性,其中即包括存在于實例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性的實例屬性也會在 for-in 循環中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的——只有在 IE8 及更早版本中例外。
要取得對象上所有可枚舉的實例屬性,可以使用 ECMAScript5 的 Object.keys() 方法。這個方法接收一個對象作為參數,放回一個包含所有可枚舉屬性的字符串數組。
如果你想要得到所有實例屬性,無論它是否可枚舉,都可以使用 Object.getOwnPropertyNames() 方法
結果中包含了不可枚舉的 constructor 屬性。Object.keys() 和?Object.getOwnPropertyNames()? 方法都可以用來替代 for-in 循環。
更簡單的原型語法
為減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象
最終結果相同,但有一個例外:constructor 屬性不再指向 Person 了。前面曾經介紹過,每創建一個函數,就會同時創建它的 prototype 對象,這個對象也會自動獲得 constructor 屬性。而我們在這里使用的語法,本質上完全重寫了默認的 prototype 對象,因此 constructor 屬性也變成了新對象的 constructor 屬性(指向 Object 構造函數),不再指向 Person 函數。
可以像下面這樣特意將它設置回適當的值。
原型對象的問題
原型模式的最大問題是由其共享的本性所導致的。對于那些包含基本值的屬性倒也說得過去,然而,對于包含引用類型值的屬性來說,問題就比較突出了。
實例一般都是要有屬于自己的全部屬性的。
4. 組合使用構造函數模式和原型模式
創建自定義類型的最常見的方式,就是組合使用構造函數模式與原型模式。構造函數模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性。結果,每個實例都會有自己的一份實例屬性的副本,但同時又共享著對方法的引用,最大限度地節省了內存。
5. 動態原型模式
6. 寄生構造函數模式
通常在前述幾種模式都不適用的情況下,可以使用寄生構造函數模式。
除了使用 new 操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣。構造函數在不返回值的情況下,默認會返回新對象實例。而通過構造函數的末尾添加一個 return 語句,可以重寫調用構造函數時返回的值。
有一點需要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關系;也就是說,構造函數返回的對象與在構造函數外部創建的對象沒有什么不同。建議在可以使用其他模式的情況下,不要使用這種模式。
7. 穩妥構造函數模式
不使用 this 和 new,適合在安全的環境中使用。
三、繼承
1. 原型鏈
ECMAScript 中描述了原型鏈的概念,并將原型鏈作為實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。
那么,假如我們讓原型對象等于另一個類型的實例,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含著一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那么上述關系仍然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂的原型鏈的基本概念。
實現原型鏈有一種基本模式,其代碼大致如下。
最終結果:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。
通過實現原型鏈,本質上擴展了前面介紹的原型搜索機制。當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。如果沒有找到該屬性,則會繼續搜索實例的原型。在通過原型鏈實現繼續的情況下,搜索過程就得以沿著原型鏈繼續向上。在找不到屬性或方法的情況下,搜索過程總要一環一環地前行到原型鏈末端才會停下來。
我們知道所有引用類型默認都繼承了 Object,而這個繼承也是通過原型鏈實現的。大家要記住,所有函數的默認原型都是 Object 的實例。
原型鏈的構建是通過將一個類型的實例賦值給另一個構造函數的原型實現的。
原型鏈的問題
最主要的問題來自包含引用類型值的原型。在通過原型實現繼承時,原型實際上會變成另一個類型的實例。于是,原先的實例屬性也就順理成章地變成了現在的原型屬性了。第二個問題是,在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。
2. 借用構造函數
在子類型構造函數的內部調用超類型構造函數。
傳遞參數
借用構造函數的問題,方法都存在構造函數中定義,函數復用就無從談起了。
3. 組合繼承
指得是將原型鏈和借用構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式。其背后的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。是 JavaScript 中最常用的繼承模式。
4. 原型式繼承
ECMAScript5 通過新增 Object.create() 方法規范化鏈原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數的情況下,Object.create() 與上圖的 Object() 方法的行為相同。
其本質是執行對給定對象的淺復制。
5. 寄生式繼承
寄生式繼承是與原型式繼承緊密相關的一種思路。基于某個或某些信息創建一個對象,然后增強對象,最后返回對象。
6. 寄生組合式繼承
前面說過,組合繼承式 JavaScript 最常用的繼承模式;不過它也有自己的不足。組合繼承最大的問題就是無論什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次式在子類型構造函數內部。沒錯,子類型最終會包含超類型對象的全部實例屬性,但我們不得不在調用子類型構造函數時重寫這些屬性。
寄生組合式繼承模式
普遍認為寄生組合式繼承是引用類型最理想的繼承模式