1. 程式人生 > >第6 章 面向物件的程式設計

第6 章 面向物件的程式設計

本章內容

  1.  理解物件屬性
  2.  理解並建立物件
  3.  理解繼承

我們可以把 ECMAScript 的物件想象成散列表:無非就是一組名值對,其中值可以是資料或函式。

1 理解物件   建立自定義物件的最簡單方式就是建立一個 Object 的例項,然後再為它新增 屬性和方法,如下所示。 var person = new Object(); person.name = "Nicholas"; person.age = 29; person.job = "Software Engineer"; person.sayName = function(){ alert(this.name); };

上面的例子建立了一個名為 person 的物件,併為它添加了三個屬性(name、 age 和 job)和一個方法(sayName())。其中, sayName()方法用於顯示 this.name(將被解析為 person.name)的值。早期的 JavaScript 開發人員經常使用這個模式建立新物件。幾年後,物件字面量成為建立這種物件的首選模式。前面的例子用物件字面量語法可以寫成這樣:

var person = { name: "Nicholas", age: 29, job: "Software Engineer", sayName: function(){ alert(this.name); } }; 這個例子中的 person 物件與前面例子中的 person 物件是一樣的,都有相同的屬性和方法。這些屬性在建立時都帶有一些特徵值(characteristic), JavaScript 通過這些特徵值來定義它們的行為。

屬性型別

1. 資料屬性  

資料屬性包含一個數據值的位置。在這個位置可以讀取和寫入值。資料屬性有 4 個描述其行為的 特性。  [[Configurable]]:表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特 性,或者能否把屬性修改為訪問器屬性。像前面例子中那樣直接在物件上定義的屬性,它們的 這個特性預設值為 true。  [[Enumerable]]:表示能否通過 for-in 迴圈返回屬性。像前面例子中那樣直接在物件上定 義的屬性,它們的這個特性預設值為 true。  [[Writable]]:表示能否修改屬性的值。像前面例子中那樣直接在物件上定義的屬性,它們的 這個特性預設值為 true。  [[Value]]:包含這個屬性的資料值。讀取屬性值的時候,從這個位置讀;寫入屬性值的時候, 把新值儲存在這個位置。這個特性的預設值為 undefined。 對於像前面例子中那樣直接在物件上定義的屬性,它們的[[Configurable]]、 [[Enumerable]] 和[[Writable]]特性都被設定為 true,而[[Value]]特性被設定為指定的值。例如: var person = { name: "Nicholas" }; 這裡建立了一個名為 name 的屬性,為它指定的值是"Nicholas"。也就是說, [[Value]]特性將 被設定為"Nicholas",而對這個值的任何修改都將反映在這個位置。  

要修改屬性預設的特性,必須使用 ECMAScript 5 的 Object.defineProperty()方法。這個方法接收三個引數:屬性所在的物件、屬性的名字和一個描述符物件。其中,描述符(descriptor)物件的屬性必須是: configurable、 enumerable、 writable 和 value。設定其中的一或多個值,可以修改對應的特性值。例如:

把 configurable 設定為 false,表示不能從物件中刪除屬性。如果對這個屬性呼叫 delete,則 在非嚴格模式下什麼也不會發生,而在嚴格模式下會導致錯誤。而且,一旦把屬性定義為不可配置的, 就不能再把它變回可配置了。此時,再呼叫 Object.defineProperty()方法修改除 writable 之外 的特性,都會導致錯誤:  

2. 訪問器屬性  

訪問器屬性不能直接定義,必須使用 Object.defineProperty()來定義。請看下面的例子。 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 以上程式碼建立了一個 book 物件,並給它定義兩個預設的屬性: _year 和 edition。 _year 前面 的下劃線是一種常用的記號,用於表示只能通過物件方法訪問的屬性。而訪問器屬性 year 則包含一個 getter 函式和一個 setter 函式。 getter 函式返回_year 的值, setter 函式通過計算來確定正確的版本。因此, 把 year 屬性修改為 2005 會導致_year 變成 2005,而 edition 變為 2。這是使用訪問器屬性的常見方 式,即設定一個屬性的值會導致其他屬性發生變化。          不一定非要同時指定 getter 和 setter。只指定 getter 意味著屬性是不能寫,嘗試寫入屬性會被忽略。在嚴格模式下,嘗試寫入只指定了 getter 函式的屬性會丟擲錯誤。類似地,只指定 setter 函式的屬性也不能讀,否則在非嚴格模式下會返回 undefined,而在嚴格模式下會丟擲錯誤。

2 定義多個屬性

定義多個屬性 由於為物件定義多個屬性的可能性很大, ECMAScript 5 又定義了一個 Object.defineProperties()方法。利用這個方法可以通過描述符一次定義多個屬性。這個方法接收兩個物件引數:第一個物件是要新增和修改其屬性的物件,第二個物件的屬性與第一個物件中要新增或修改的屬性一一對應。例如: 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; } } } }); MultiplePropertiesExample01.htm 以上程式碼在 book 物件上定義了兩個資料屬性(_year 和 edition)和一個訪問器屬性(year)。最終的物件與上一節中定義的物件相同。唯一的區別是這裡的屬性都是在同一時間建立的。

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

建立物件

雖然 Object 建構函式或物件字面量都可以用來建立單個物件,但這些方式有個明顯的缺點:使用同一個介面建立很多物件,會產生大量的重複程式碼。為解決這個問題,人們開始使用工廠模式的一種變體。

1. 工廠模式

函式 createPerson()能夠根據接受的引數來構建一個包含所有必要資訊的 Person 物件。可以無 數次地呼叫這個函式,而每次它都會返回一個包含三個屬性一個方法的物件。工廠模式雖然解決了建立 多個相似物件的問題,但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)。隨著 JavaScript 的發展,又一個新模式出現了。  

2 建構函式模式

在這個例子中, Person()函式取代了 createPerson()函式。我們注意到, Person()中的程式碼 除了與 createPerson()中相同的部分外,還存在以下不同之處:  沒有顯式地建立物件;  直接將屬性和方法賦給了 this 物件;  沒有 return 語句。 此外,還應該注意到函式名 Person 使用的是大寫字母 P。按照慣例,建構函式始終都應該以一個 大寫字母開頭,而非建構函式則應該以一個小寫字母開頭。這個做法借鑑自其他 OO 語言,主要是為了 區別於 ECMAScript 中的其他函式;因為建構函式本身也是函式,只不過可以用來建立物件而已。  

要建立 Person 的新例項,必須使用 new 操作符。以這種方式呼叫建構函式實際上會經歷以下 4 個步驟: (1) 建立一個新物件; (2) 將建構函式的作用域賦給新物件(因此 this 就指向了這個新物件); (3) 執行建構函式中的程式碼(為這個新物件新增屬性); (4) 返回新物件。  

1. 將建構函式當作函式 建構函式與其他函式的唯一區別,就在於呼叫它們的方式不同。不過,建構函式畢竟也是函式,不存在定義建構函式的特殊語法。任何函式,只要通過 new 操作符來呼叫,那它就可以作為建構函式;而任何函式,如果不通過 new 操作符來呼叫,那它跟普通函式也不會有什麼兩樣。例如,前面例子中定義的 Person()函式可以通過下列任何一種方式來呼叫。 // 當作建構函式使用 var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); //"Nicholas" // 作為普通函式呼叫 Person("Greg", 27, "Doctor"); // 新增到 window window.sayName(); //"Greg" // 在另一個物件的作用域中呼叫 var o = new Object(); Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); //"Kristen