JavaScript不是一門真正的面向對象語言,因為它連最基本的類的概念都沒有,因此它的對象和基於類的語言中的對象也會有所不同。ECMA-262把對象定義為:“無序屬性的集合,其屬性可以包含基本值、對象或者函數。” 嚴格來講,這就相當於說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都映射到一個值。我們可以把ECMAScript的對象想象成散列表:無非就是一組名值對,其中的值可以是數據或函數。每個對象都是基於一個引用類型創建的,這個引用類型可以是原生類型,也可以是開發人員自己定義的。
一、理解對象
創建自定義對象的最簡單方式就是創建一個Object的實例,然后為其添加屬性和方法。代碼如下:
1 var person = new Object(); 2 person.name = "Tom"; 3 person.age = 29; 4 person.job = "CEO"; 5 6 person.sayName = function(){ 7 alert(this.name); 8 }
簡單點,可以用對象字面量創建對象。代碼如下:
1 var person = { 2 person.name = "Tom"; 3 person.age = 29; 4 person.job = "CEO"; 5 6 person.sayName = function(){ 7 alert(this.name); 8 } 9 }
1、屬性類型
ECMA-262第5版在定義只有內部才用的特性(attribute)時,描述了屬性(property)的各種特征。定義這些特性是為了實現JavaScript引擎用的,因此在JavaScript中不能直接訪問它們。為了表示特性是內部值,該規范把它們放在了倆對兒方括號里,例如[[Enumerable]]。
ECMAScript中有兩種屬性:數據屬性和訪問器屬性。
1.1 數據屬性:數據屬性包含一個數據值的位置。在這個位置可以讀取和寫入值。數據屬性有4個描述其行為的特性。
[[Configurable]] : 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。
[[Enumerable]] : 表示能否通過for...in循環返回屬性。
[[Writable]] : 表示能否修改屬性的值。
[[Value]] : 包含這個屬性的數據值。
要修改屬性默認的特性,必須使用ECMAScript 5的Object.defineProperty()方法。這個方法接收三個參數:屬性所在的對象,屬性的名字和一個描述付對象,其中描述符(descriptor)對象的屬性必須是:configurable, enumerable, writable 和value。設置其中的一個或多個值,可以修改對應的特性值。調用Object.defineProperty()方法時,如果不顯式指定,configurable
、enumerable
和writable
特性的默認值都是false
。
1.2 訪問器屬性:訪問器屬性不包含數據值;訪問器屬性包含一對getter和setter函數(這兩個函數都不是必需的)。在讀取訪問器屬性時,會調用getter函數,這個函數負責返回有效的值;在寫入訪問器屬性時,會調用setter函數並傳入新值,這個函數負責決定如何處理數據。訪問器有如下4個特性:
[[Configurable]] : 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。
[[Enumerable]] : 表示能否通過for...in循環返回屬性。
[[Get]]:在讀取屬性時調用的函數。默認值為undefined。
[[Set]]:在寫入屬性時調用的函數。默認值為undefined。
數據屬性和訪問器屬性詳情請參見 :《JS高程》——數據屬性和訪問器屬性.
二、創建對象
雖然Object構造函數或對象字面量都可以用來創建單個對象,但這些方法有個明顯的缺點:使用同一個接口創建很多對象,會產生大量的重復代碼。下面來一步一步完善:
1、工廠模式
這種模式抽象了創建對象的過程,考慮到JavaScript中無法創建類,開發人員就發明了一種函數,用函數來封裝以特定接口創建對象的細節,如下:
1 function createPerson(name, age, job){ 2 var o = new Object(); 3 o.name = name; 4 o.age = age; 5 o.job = job; 6 o.sayName = function(){ 7 alert(this.name); 8 }; 9 return o; 10 } 11 var person1 = createPerson('pretty', 29, "FE"); 12 var person2 = createPerson('Grey', 27, "DT");
工廠模式雖然解決了多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。於是,新的模式出現了...
2、構造函數模式
ECMAScript中的構造函數可用來創建特定類型的對象。像Object和Array這樣的原生構造函數,在運行時會自動出現在執行環境中。此外,也可以創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。例如:
1 function Person(name, age, job){ 2 // 默認 var this = new Object(); 3 this.name= name; 4 this.age = age; 5 this.job = job; 6 this.sayName = function(){ 7 alert(this.name); 8 } 9 10 // 默認 return this; 11 } 12 var person1 = new Person('pretty', 29, "FE"); 13 var person2 = new Person('Grey', 27, 'Dt');
在調用部分,var person1=new Person("nicole",24); 經歷了以下4個步驟(即new操作符都做了啥):
(1)創建一個新對象 // var this = new Object();
(2)將構造函數的作用域賦給新對象(this指向新對象) // this._proto_ = Base.prototype;
(3)執行構造函數中的代碼(為新對象添加屬性) // Base.call(this);
(4)返回新對象 // return this;
以上兩個實例person1&person2分別保存着Person的不同實例,這兩個對象都有一個constructor屬性(不可枚舉,enumerable=false),該屬性指向Person
1 console.log(person1.constructor==Person) //true 2 console.log(person2.constructor==Person); //true 3 console.log(person2.constructor==person1.constructor); //true
提到檢測對象類型,instanceof 操作符要更可靠一些,我們在這個例子中創建的所有對象既是Object的實例,同時也是Person的實例
1 alert(person1 instanceof Person) //true 2 alert(person1 instanceof Object) //true
person1和person2之所以同時是Object的實例,是因為所有對象均繼承自Object
2.1 將構造函數當作函數
構造函數與其他函數的唯一區別,就在於調用它們的方式不同。不過,構造函數也是函數,不存在定義構造函數的特殊語法。任何函數,只要通過new操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過new操作符來調用,那它跟普通的函數也不會有什么倆樣。

1 //當作構造函數使用 2 var person = new Person('pretty', 29, 'Fe'); 3 person.sayName(); // 'pretty' 4 5 //作為普通函數調用 6 Person('Greg', 27, "Dt"); 7 window.sayName(); //'Greg' 8 9 //在另一個對象的做用域中調用 10 var o = new Object(); 11 person.call(o, "Kristen" ,25, "Te"); 12 o.sayName(); //'Kristen
當作構造函數時好理解,關鍵看看作為普通函數調用時發生了什么:屬性和方法都被添加到window對象。因為在全局作用域調用一個函數時,this對象總是指向Global對象(在瀏覽器中就是window對象)。因此,在調用完函數之后,可以通過window對象來調用sayName()方法,並且還返回了Greg。最后,也可以使用call()(或者apply())在某個特殊對象的的作用域中調用Person()。這里是在對象o的作用域中調用的,因此,調用后o就擁有了所有的屬性和sayName()方法。
2.2 構造函數的問題
使用構造函數的主要問題就是每個方法都要在每個實例上重新創建一遍。在前面的例子中,person1和person2都有一個名為sayName()的方法。但那兩個方法不是同一個function的實例。不要忘了--ECMAScript 中的函數就是對象,因為每定義一個函數,也就是實例化了一個對象。從邏輯上講,此時的構造函數也可以這樣定義:

1 function Person(name, age, job){ 2 this.name = name; 3 this.age = age; 4 this.job = job; 5 this.sayName = new Function('alert(this.name)'); //與聲明函數在邏輯上是等價的 6 }
3、原型模式
我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途就是包含可以由特定類型的所有實例共享的屬性和方法。

1 function Person(){} 2 Person.prototype.name = "pretty"; 3 Person.prototype.age = 18; 4 Person.prototype.job = "fe"; 5 Person.prototype.sayName = function(){ 6 alert(this.name); 7 }; 8 var person1 = new Person(); 9 person1.sayName(); //'pretty' 10 var person2 = new Person(); 11 person2.sayName() //''pretty" 12 13 alert(person1.sayName == person2.sayName); //true
3.1 理解原型對象
無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象,在默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這個屬性包含一個指向prototype屬性所在函數的指針。Person.prototype.constructor指向Person. 而通過這個屬性我們可以繼續為原型對象添加屬性和方法
創建了自定義的構造函數之后,其原型對象默認只會取得constructor屬性;至於其他方法,則都是從Object繼承而來.當調用構造函數創建一個新實例后,該實例的內部將包含一個指針(內部屬性)指向構造函數的原型對象,ECMA-262第五版中稱這個指針為[[Prototype]],沒有標准的方式來訪問[[Prototype]],但是在Firefox、Safari、Chrome瀏覽器中每個對象都支持屬性__proto__,而在其它實現中該屬性是完全不可見的。不過,要明確的真正重要的一點就是,這個連接存在於實例與構造函數原型對象之間,而不是存在於實例與構造函數之間。關系如圖:
雖然所有的實現都無法訪問[[Prototype]],但是可以通過原型對象的isPrototypeOf()方法來確定實例與原型對象之間是否存在這種關系,如果實例的[[Prototype]]指向了調用isPrototypeOf()方法的原型對象(Person.prototype),則該返回true.
1 Person.prototype.isPrototypeOf(person1); //true 2 Person.prototype.isPrototypeOf(person2); //true
在ECMAScript5中有個方法叫做Object.getPrototypeOf(),在所有支持的實現中,該方法返回[[Prototype]]的值:
alert(Object.getPrototypeOf(person) == Person.prototype); //true
支持該方法的瀏覽器有IE9+、Firefox 3.5+、Safari 5+、Opera 12+、Chrome。
每當讀取某個對象的屬性時,都會進行一次搜索,首先從對象實例本身開始,找到了就返回屬性值,如果沒有找到,則繼續搜索指針指向的原型對象,這就是多個對象實例共享原型對象所保存的屬性和方法的基本原理。
雖然可以通過對象實例訪問保存在原型對象中的值,但是不能通過對象實例重寫原型對象中的值。如果在某個實例中添加了一個屬性,且該屬性與原型對象中的某個屬性同名,那么是在實例中創建該屬性,該屬性將會屏蔽掉原型對象中的那個同名屬性。
同時也可以使用delete操作符來刪除某個實例屬性,從而能夠重新訪問原型對象中的同名屬性。
方法hasOwnProperty()(從Object對象繼承而來的)可以用來檢測一個屬性是存在於實例本身還是存在於原型對象中,只有給定屬性存在於實例中時,才返回true。
3.2 原型與in操作符
使用in操作符的兩種方式,如下:
-
- 單獨使用:通過對象能夠訪問指定屬性時返回true,無論該屬性存在於實例對象中還是存在於原型對象中,使用方式為:"屬性名" in 對象。結合hasOwnProperty方法使用就能確定一個屬性是否存在且存在什么對象中。
/*判斷實例屬性是否在原型中*/ function hasPrototypePrototype(object, name){ return !object.hasOwnProperty(name) && (name in object); }
- 在for-in循環中使用:返回的是所有能夠通過對象訪問、可枚舉(enumerated)的屬性,既包括存在於實例中的屬性,也包括存在於原型對象中的屬性;屏蔽了原型對象中的不可不枚舉屬性([[Enumerable]]標記的屬性)的實例屬性也會在該循環中返回,因為根據規定,所有開發人員定義的屬性都是可枚舉的(IE8--例外)。此外,要取得對象上所有可枚舉的的實例屬性,可以使用ECMAScript5中的Object.keys()方法,返回一個包含所有可枚舉屬性的字符串數組:Object.keys(Person.prototype)。如果是要取得所有實例屬性,而無論該屬性是否可枚舉,則可以使用Object.getOwnPropertyNames(對象)。支持這兩個方法的瀏覽器包括:IE9+、Firefox4+、Safari5+、Opera12+、Chrome。
- 單獨使用:通過對象能夠訪問指定屬性時返回true,無論該屬性存在於實例對象中還是存在於原型對象中,使用方式為:"屬性名" in 對象。結合hasOwnProperty方法使用就能確定一個屬性是否存在且存在什么對象中。
要想取得對象上所有可枚舉的實例屬性,可以使用ECMAScript5的Object.key()方法。這個方法接受一個對象作為參數,返回一個包含所有可枚舉屬性的字符串數組。例如:

1 function Person() {} 2 3 Person.prototype.name = "Tom"; 4 Person.prototype.age = 22; 5 Person.prototype.job = "CEO"; 6 Person.prototype.sayName = function() { 7 alert(this.name); 8 } 9 10 var keys = Object.keys(Person.prototype); 11 alert(keys); //"name,age,job,sayName" 12 13 var p1 =new Person(); 14 p1.name = "Rob"; 15 p1.age = 32; 16 17 var p1keys = Object.keys(p1); 18 alert(pekeys); //"name,aeg";
3.3 原型的動態性
由於在原型中查找值的過程是一次搜索,因此我們對原型所做的任何修改都能夠立即從實例上反映出來——即使是先創建了實例后修改原型也照樣如此。比如:

1 var friend = new Person(); 2 3 Person.prototype.sayHi = function(){ 4 alert("Hi"); 5 } 6 7 friend.sayHi(); //"Hi" (沒有問題)
原因是實例與原型之間的松散連接關系。當我們調用person.sayHi()時,首先會在實例中搜索名為sayHi的屬性,在沒有找到的情況下,會繼續搜索原型。因為實例與原型之間的連接只不過是一個指針,而非一個副本,因此就可以在原型中找到新的sayHi屬性並返回保存在那里的函數。
盡管可以隨時為原型添加屬性和方法,並且修改能夠立即在所有對象實例中反映出來,但如果是重寫整個原型對象,那么情況就變的糟糕了。我們知道,調用構造函數時會為實例添加一個指向最初原型的[[Prototype]]指針,而把原型修改為另一個對象就等於切斷了構造函數與最初原型之間的聯系。切記:實例中的指針僅指向最初的原型,而不指向構造函數。

1 function Person(){} 2 3 var friend = new Person(); 4 5 Person.prototype = { 6 constructor: Person, 7 name: "Tom", 8 age: 22, 9 sayName: function(){ 10 alert(this.name); 11 } 12 }; 13 14 friend.sayName(); /* error 此時的friend實例指向還是最初的原型Person.prototype(里面只有一個constructor屬性),因此,先寫原型后實例化 */
3.4 原型對象的問題
由於原型對象中的所有屬性和方法都是被所有實例共享的,這種共享對於方法來說很適合,對於基本數據類型的屬性來說也是適合的,因為通過在實例上添加同名的屬性可以隱藏掉原型對象中的對應屬性,但是對於引用類型的屬性來說,要格外注意,如下:

1 function Person() {} 2 3 Person.prototype = { 4 constructor: Person, 5 name: "Tom", 6 friend: ["Tom", "Jake"] 7 }; 8 9 var p1 = new Person(); 10 var p2 = new Person(); 11 12 p1.books.push("Mary"); 13 14 alert(person1.friend); //"Tom,Jake,Mary" 15 alert(person2.friend); //"Tom,Jake,Mary" 16 alert(person1.friend === person2.friend); //true
可見,這樣的操作不會屏蔽同名屬性,而會修改同名屬性。因為,friend屬性是個引用類型,實例調用friend時調用的是它的指針,通過指針找到相應內存地址,然后修改其屬性值。注意:原型中引用類型可能會被改寫,但構造函數中的任何類型都不會被改寫!
4、組合使用構造函數模式和原型模式
組合使用構造函數模式和原型模式的方式是創建自定義對象最常見的方式,構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。所以,每個實例都會有自己的一份實例屬性的副本,但同時又共享對方法的引用,好處就是最大限度的節省了內存,如下:
1 function Person(name, age, job) { 2 this.name = name; 3 this.age = age; 4 this.job = job;
5 this.friend = ["Tom", "Jake"];
6 } 7 Person.prototype = { 8 constructor: Person, 9 getName: function() { 10 return this.name; 11 } 12 }; 13 14 var person1 = new Person("Tom",22,"A"); 15 var person2 = new Person("Jake",23,"B"); 16 17 person1.friend.push("Van"); 18 alert(person1.friends); // "Tom,Jake,Van" 19 alert(person2.friends); // "Tomo,Jake" 20 alert(person1.friends === person2.friends); //false 21 alert(person1.sayName === person2.sayName); //true
5、動態原型模式
動態原型模式把所有信息都封裝在構造函數中,而通過在構造函數中初始化原型對象(僅在必要的情況下),且保持了同時使用構造函數和原型的優點。也就是說,可以通過檢查某個應該存在的方法是否有效來決定是否需要初始化原型對象。如下:
1 function Person(name, age, job) { 2 //屬性 3 this.name = name; 4 this.age = age; 5 this.job = job; 6 //方法 7 if (typeof this.sayName != "function") { 8 Person.prototype.sayName = function() { 9 return this.name; 10 }; 11 } 12 } 13 14 var friend = new Person("Tom",22,"A"); 15 friend.sayName();
注意加粗部分,只在sayName()方法不存在的情況下,才會將其添加到原型對象中,所以只會在第一次調用構造函數創建實例時才會添加該方法到原型對象中。這段代碼只會在初次調用構造函數時才會執行。此后,原型已經完成初始化,不需要再做什么修改了。不過要記住,這里對原型所做的修改,能夠立即在所有實例中得到反映。因此,這種方法可以說是完美。
不過在使用這種方式時,需要注意的是不能使用對象字面量來重寫原型,因為如果在已經創建了實例的情況下重寫原型,那么就會切斷現有實例與新原型之間的關系。
6、寄生構造函數模式
通常,在前述的幾種模式都不適用的情況下,可以使用寄生(parasitic)構造函數模式。這種模式的基本思想是創建一個函數,該函數的作用僅僅是封裝對象的代碼,然后再返回新創建對象:
1 function Person(name, age, job) { 2 var o = new Object(); 3 o.name = name; 4 o.age = age;
o.job = job; 5 o.getName = function() { 6 return this.name; 7 }; 8 9 return o; 10 } 11 12 var person = new Person("Tom", 21, "A"); 13 friend.sayName(); // "Tom"
咋一看,這與工廠模式一模一樣,但是注意函數名定義時首字母是大寫的,說明把該函數當做構造函數來使用,而不是普通函數,而且使用的是new操作符來創建對象,並不是普通的函數調用。在該例子中,Person函數中創建了一個新對象,並以相應的屬性和方法來初始化該對象,然后返回了該對象。構造函數在不返回值的情況下,默認會返回新對象實例,而通過在構造函數的末尾添加返回語句,就可以重寫調用構造函數時返回的值。
該模式可以在特殊的情況下用來為對象創建構造函數。比如,要創建一個具有額外方法的特殊數組,由於不能直接修改Array的構造函數,所以可以使用該模式,如下:
1 function SpecialArray() { 2 // 創建數組對象 3 var arr = new Array(); 4 5 // 添加元素 6 arr.push.apply(arr, arguments); 7 8 // 添加方法 9 arr.toPipedString = function() { 10 return this.join("|"); 11 }; 12 13 // 返回數組對象 14 return arr; 15 } 16 17 var colors = new SpecialArray("red", "blue", "green"); 18 colors.toPipedString(); // "red|blue|green"
對於寄生構造函數模式,返回的對象與構造函數或者與構造函數的原型屬性之間沒有任何關系;也就是說,構造函數返回的對象與在構造函數外部創建的對象沒有什么不同,所以不能依賴instanceof操作符來確定對象類型。
7、穩妥構造函數模式
1、穩妥對象:所謂穩妥對象指的是沒有公共屬性,且其方法也不引用this的對象。穩妥對象最適合在一些安全的環境中(這些環境禁止使用this和new),或者在防止數據被其它應用程序改動時使用。
2、穩妥構造函數模式與寄生構造函數模式類似,但是有兩點不同:
-
- 新創建對象的實例方法不引用this
- 不使用new操作符調用構造函數
1 function Person(name, age, job) { 2 // 創建要返回的對象 3 var o = new Object(); 4 5 // 可以在這里定義私有變量和函數 6 7 o.sayName = function() { 8 return name; 9 }; 10 //返回對象 11 return o; 12 } 13 14 var person = Person("Tom", 21, "CEO"); 15 person.getName(); // "Tom"
姊妹篇:JS之繼承的常用方法