## 6.2 創建對象
Object構造函數或對象字面量創建對象有明顯的缺點:使用同一個接口創建很多對象,會產很更大量的重復代碼。
### 6.2.1 工廠模式
**工廠模式**在JavaScript中是指**用函數來封裝以特定接口創建對象的細節**。
這種模式抽象了創建具體對象的過程。例:
~~~
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;
}
~~~
工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別的問題(即**怎么知道一個對象的類型**)。
### 6.2.2 構造函數模式
**構造函數模式**通過創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。
~~~
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name)
};
}
~~~
Person()函數與createPerson()函數之間的不同之處在于:
* 沒有顯式地創建對象;
* 直接將屬性和方法賦給了this對象;
* 沒有return語句;
*按照慣例,構造函數始終都應該以一個大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。這個做法借鑒自其他OO語言,主要為了區別于ECMAScript中的其他函數;因為構造函數本身也是函數,只不過可以用來創建對象而已。*
~~~
var person1 = new Person("Tom", 18, "Student");
~~~
要創建Person對象的新實例,必須使用**new 操作符**。以這種方式調用構造函數實際上會經歷以下四個步驟:
1. 創建一個新對象;
2. 將構造函數的作用域賦給新對象(因此this就指向了這個新對象)
3. 執行構造函數中的代碼(為這個新對象添加屬性);
4. 返回新對象。
創建自定義的構造函數意味著將來可以將它的實例標識為一種特定的類型。
**1.將構造函數當做函數**
任何函數,只要通過new操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過new操作符來調用,那它跟普通函數沒有什么區別。當一個原意用作構造函數的函數不通過new操作符調用,那函數體內的this對象就指向全局作用域window。
~~~
//當作構造函數使用
var person1 = new Person("Tom", 18, "Student");
person1.sayName(); //"Tom"
//作為普通函數調用
Person("Tom", 18, "Student"); //添加到window
window.sayName(); //"Tom"
//在另一個對象的作用域中調用
var o = new Object();
Person.call(0, "Ken" ,28, "Teacher");
o.sayName(); //"Ken"
~~~
**2. 構造函數的問題**
構造函數的主要問題在于,每個方法都要在每個實例上重新創建一遍。可以通過把函數定義轉移到構造函數外面來解決這個問題:
~~~
……
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
~~~
但這樣的做法又帶來了新問題,全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那么就要定義很多個全局函數,于是我們這個自定義的引用類型就絲毫沒有封裝性可言。
### 6.2.3 原型模式
每個創建的函數都有一個**prototype屬性**,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。換言之,**prototype就是通過調用構造函數而創建的那個對象實例的原型對象**。使用原型對象的好處是可以就讓所有對象實例共享它所包含的屬性和方法。例:
~~~
function Person(){
}
Person.prototype.name = "ken";
Person.prototype.age = 28;
Person.prototype.job = "Teacher";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true (不加括號,否則對比的是運行值)
~~~
**1. 理解原型對象**
無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個**prototype屬性,這個屬性指向函數的原型對象。**
在默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性是一個指向prototype屬性所在函數的指針。例如Person.prototype.constructor指向Person。

當調用構造函數創建一個新實例后,該實例的內部將包含一個[[prototype]]指針(主流瀏覽器中的proto屬性)。要明確的重要一點是,這個連接存在于實例與原型對象之間,而不是存在于實例與構造函數之間。
雖然在所有實現中都無法訪問[[prototype]],但可以通過isPrototypeOf()方法來確定對象之間是否存在這種關系。
~~~
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
~~~
ECMAScript5新增了`Ojbect.getPrototypeOf()`方法返回[[prototype]]的值。
~~~
alert(Object.getPrototypeOf(person1)) //Person.prototype
alert(Object.getPrototypeOf(person1).name) //ken
~~~
雖然可以通過對象實例訪問保存在原型中的值,但卻不能通過對象實例重寫原型中的值。
當為對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性,但不會修改那個屬性。可以使用delete操作符完全刪除實例屬性,重新訪問原型中的屬性。
使用`hasOwnProperty()`方法可以檢測一個屬性存在于實例還是原型中,存在于實例中返回true。
~~~
var person1 = new Person();
person1.name = 'jason';
delete person1.name;
alert(person1.name); //ken
~~~
**2. 原型與in操作符**
* 單獨使用`in`操作符,會在通過對象能夠訪問給定屬性時返回true,無論該屬性存在于實例中還是原型中。
~~~
alert('name' in person1); //true;
~~~
* 使用for-in循環時,返回的是所有能夠通過對象訪問的,可枚舉的屬性,其中既包含存在于實例中的屬性,也包括存在原型中的屬性。屏蔽了原型中不可枚舉屬性的實例屬性也會在for-in循環中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的(除了IE8及更早版本)。
要取得對象上所有可枚舉的實例屬性,可以使用ECMAScript5中的`Object.keys()`方法。這個方法接收一個對象作為參數,返回一個**包含所有可枚舉屬性的字符串數組**。
如果想要得到所有實例屬性,無論它是否可枚舉,都可以使用`Object.getOwnPropertyNames()`。
總結:
~~~
for-in 實例+原型、可枚舉;
Object.keys() 實例、可枚舉;
Object.getOwnPropertyNames() 實例、可枚舉+不可枚舉
~~~
**3. 更簡單的原型語法**
~~~
function Person(){
}
Person.prototype = {
name:'Ken',
age:28,
job:'FrontEnd Engineer',
sayName:function(){
alert(this.name);
}
};
~~~
使用這樣的語法創建原型對象,**constructor屬性不再指向Person**了。前面曾經介紹過,每創建一個函數,就會同時創建他的prototype對象。在這里使用的語法,本質上完全重寫了默認的prototype對象,因此,constructor屬性也就變成了新對象的constructor屬性(指向Object構造函數),不再指向Person函數。此時,盡管instanceof操作符還能返回正確的結果,但通過constructor已經無法確立對象的類型了。
如果constructor的值真的很重要,那么在使用上面的語法重寫原型對象時,可以手動將construtor的屬性值設為Person。注意,以這種方式重設constructor屬性會導致他的[[Enumerable]]特性被置為true。可以通過Object.defineProperty()方法將屬性置為默認的不可枚舉:
~~~
Object.defineProperty(Person.prototype,'constructor',{
enumerable: false,
value: Person
});
~~~
**4. 原型的動態性**
由于在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例后修改原型也照樣如此。
盡管可以隨時為原型添加屬性和方法,并且修改能夠立即在所有對象實例中反映出來,但如果是重寫整個原型對象,那么情況就不一樣了。我們知道,調用構造函數時會為實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改為另一個對象就等于切斷了構造函數與最初原型之間的聯系。
**實例中的指針僅指向原型,而不指向構造函數。**
~~~
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor:person,
name:"Ken",
age:28,
job: "Teacher",
sayName:function(){
alert(this.name);
}
};
friend.sayName(); //error
~~~
**5. 原生對象的原型**
原型模式的重要性不僅體現在創建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創建的。
通過原生對象的原型,不僅可以取得所有默認方法的引用,而且也可以定義新方法。
**注意:不推薦在產品化的程序中修改原生對象的原型。**
**6. 原形對象的問題**
* 省略了為構造函數傳遞初始化參數這一環節;
* 原型中所有屬性是被很多實例共享的,對于包含引用類型值的屬性來說,存在問題。
### 6.2.4 組合使用構造函數模式和原型模式
~~~
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friend = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name);
}
}
var person1 = new Person("Ken", 29, "Teacher");
var person2 = new Person("Tom", 18, "Student");
person1.friends.push("Van");
alert(person1.friends); //"Shelby","Court","van"
alert(person2.friends); //"Shelby","Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
~~~
上例中,實例屬性都在構造函數中定義,由所有實例共享的屬性constructor和方法sayName()則在原型中定義。
### 6.2.5 動態原型模式
動態原型模式解決了構造函數與原型分別獨立、沒有封裝在一起的問題。
~~~
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['wesley','fox'];
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function(){
alert(this.name)
}
}
}
~~~
### 6.2.6 寄生構造函數模式
基本思想:創建一個函數,該函數的作用僅僅是封裝創建對象的代碼,然后再返回新創建的對象。
寄生構造函數模式解決了這樣一個場景下的問題:假設想創建一個具有額外方法的特殊數組,又不能直接修改Array構造函數,就可以使用這個模式。除了使用new操作符并把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一模一樣的。
~~~
function SpecialArray(){
var values = new Array();
values.push.apply(values,arguments); //apply可以方便地把arguments添加到values數組。
values.toPipedString = function(){
return this.join('|');
}
return values;
}
var colors = new SpecialArray('red','blue','green');
alert(colors.toPipeString()); //'red|blue|green'
~~~
關于寄生構造函數模式,有一點需要說明:首先,返回的對象與構造函數或者與構造函數的原型屬性之間沒有關系;也就是說,構造函數返回的對象與構造函數外部創建的對象沒什么不同。為此,不能依賴instanceof操作符來確定對象類型。由于存在上述問題,我們建議在在可以使用其他模式的情況下,不要使用這種模式。
~~~
alert(SpecialArray.prototype.isPrototypeOf(colors)); //false
alert(colors instanceof SpecialArray); //false
~~~
### 6.2.7 穩妥構造函數模式
所謂穩妥對象,指的是沒有公共屬性,而且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境會禁止使用this和new),或者防止數據被其他應用程序改動時使用。穩妥構造函數模式遵循與寄生構造函數模式類似的模式,但有兩點不同:一是新創建對象實例方法不引用this;二是不使用new操作符調用構造函數。
~~~
function Person(name,age,job){
//var name = name,age = age,job = job;
var o = new Object();
o.sayName = function(){
alert(name);
};
return o;
}
var friend = Person('ken','28','FrontEnd Engineer');
friend.sayName(); //ken
~~~
這樣,變量friend中保存的是一個穩妥對象,而除了調用sayName()方法外,沒有別的方式可以訪問其數據成員。即使有其他代碼會給這個對象添加方法和數據成員,但也不可能有別的辦法訪問傳入到構造函數中的原始數據。與寄生構造函數模式類似,使用穩妥構造函數模式創建的對象與構造函數之間也沒有什么關系,因此instanceof操作符對這種對象也沒有意義。
- 前言
- 第一章 JavaScript簡介
- 第三章 基本概念
- 3.1-3.3 語法、關鍵字和變量
- 3.4 數據類型
- 3.5-3.6 操作符、流控制語句(暫略)
- 3.7函數
- 第四章 變量的值、作用域與內存問題
- 第五章 引用類型
- 5.1 Object類型
- 5.2 Array類型
- 5.3 Date類型
- 5.4 基本包裝類型
- 5.5 單體內置對象
- 第六章 面向對象的程序設計
- 6.1 理解對象
- 6.2 創建對象
- 6.3 繼承
- 第七章 函數
- 7.1 函數概述
- 7.2 閉包
- 7.3 私有變量
- 第八章 BOM
- 8.1 window對象
- 8.2 location對象
- 8.3 navigator、screen與history對象
- 第九章 DOM
- 9.1 節點層次
- 9.2 DOM操作技術
- 9.3 DOM擴展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件處理程序
- 10.3 事件對象
- 10.4 事件類型
- 第十一章 JSON
- 11.1-11.2 語法與序列化選項
- 第十二章 正則表達式
- 12.1 創建正則表達式
- 12.2-12.3 模式匹配與RegExp對象
- 第十三章 Ajax
- 13.1 XMLHttpRequest對象
- 你不知道的JavaScript
- 一、作用域與閉包
- 1.1 作用域
- 1.2 詞法作用域
- 1.3 函數作用域與塊作用域
- 1.4 提升
- 1.5 作用域閉包
- 二、this與對象原型
- 2.1 關于this
- 2.2 全面解析this
- 2.3 對象
- 2.4 混合對象“類”
- 2.5 原型
- 2.6 行為委托
- 三、類型與語法
- 3.1 類型
- 3.2 值
- 3.3 原生函數