本章內(nèi)容
- 理解對象屬性
- 理解并創(chuàng)建對象
- 理解繼承
面向?qū)ο笳Z言有一個標(biāo)志,那就是它們都有類的概念,而通過類可以創(chuàng)建任意多個具有相同屬性和方法的對象。ECMAScript 中沒有類的概念,因此它的對象也與基于類的語言中的對象有所不同。
我們可以把 ECMAScript 的對象想象成散列表:無非就是一組名值對,其中值可以是數(shù)據(jù)或函數(shù)。
每個對象都是基于一個引用類型創(chuàng)建的,這個引用類型可以是原生類型,也可以是開發(fā)人員定義的類型。
6.1 理解對象
創(chuàng)建自定義對象的最簡單方式就是創(chuàng)建一個Object
的實例,然后再為它添加屬性和方法。
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engine";
person.sayName = function () {
alert(this.name);
}
早期開發(fā)人員經(jīng)常使用這個模式創(chuàng)建新對象。幾年后,對象字面量成為創(chuàng)建這種對象的首選模式。
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
alert(this.name);
}
}
6.1.1 屬性類型
ECMAScript 5 再定義只有內(nèi)部才用的特性(attribute)時,描述了屬性(property)的各種特征。定義這些特性是為了實現(xiàn) JavaScript 引擎用的,因此在 JavaScript 中不能直接訪問它們。為了表示特性是內(nèi)部值,該規(guī)范把它們放在了兩對方括號中,例如[[Enumerable]]
。
ECMAScript 中有兩種屬性:數(shù)據(jù)屬性和訪問器屬性。
- 數(shù)據(jù)屬性
數(shù)據(jù)屬性包含一個數(shù)據(jù)值的位置。在這個位置可以讀取和寫入值。數(shù)據(jù)屬性有 4 個描述其行為的特性。
-
[[Configurable]]
:表示能否通過delete
刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。直接在對象上定義的屬性,默認(rèn)值為true
。 -
[[Enumerable]]
: 表示能否通過for-in
循環(huán)返回屬性。同上默認(rèn)為true
。 -
[[Writable]]
:表示能否修改屬性的值。同上默認(rèn)為true
。 -
[[Value]]
:包含這個屬性的數(shù)據(jù)值。讀取屬性值的時候,從這個位置讀;寫入屬性值得時候,把新值保存在這個位置。這個特性的默認(rèn)值為undefined
。
要修改屬性默認(rèn)的特性,必須使用Object.defineProperty()
方法。這個屬性接收三個參數(shù):屬性所在的對象,屬性的名字和一個描述符對象。其中描述符對象的屬性必須是:configurable
、enumerable
、writable
、value
。設(shè)置其中的一或多個值,可以修改對應(yīng)的特性值。
var person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
alert(person.name); //"NIcholas"
person.name = "Greg";
alert(person.name); //"Nicholas"
可以多次調(diào)用Object.defineProperty()
方法修改同一個屬性,但在把configurable
特性設(shè)置為false
之后就會有限制了。多數(shù)情況沒有必要使用。
- 訪問器屬性
訪問器屬性不包含數(shù)據(jù)值:它們包含一對getter
和setter
函數(shù)(不過,這兩個函數(shù)都不是必需的)。在讀取訪問器屬性時,會調(diào)用getter
函數(shù),這個函數(shù)負(fù)責(zé)返回有效的值;在寫入訪問器屬性時,會調(diào)用setter
函數(shù)并傳入新值,這個函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。訪問器屬性有如下 4 個特性。
-
[[Configurable]]
: 表示能否通過delete
刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數(shù)據(jù)屬性。 -
[[Enumerable]]
:表示能否通過for-in
循環(huán)返回屬性。對于直接在對象上定義的屬性,這個特性的默認(rèn)值為true
。 -
[[Get]]
:在讀取屬性時調(diào)用的函數(shù)。默認(rèn)值為undefined
。 -
[[Set]]
:在寫入屬性時調(diào)用的函數(shù)。默認(rèn)值為undefined
。
訪問其屬性不能直接定義,必須使用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
_year
前面的下劃線是一種常用的記號,用于表示只能通過對象方法訪問的屬性。
6.1.2 定義多個屬性
ECMAScript 5 又定義了一個Object.defineProperties()
方法。利用這個方法可以通過描述符一次定義多個屬性。這個方法接收兩個對象參數(shù):第一個對象是要添加和修改其屬性的對象,第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應(yīng)。
6.1.2 讀取屬性的特性
使用Object.getOwnPropertyDescriptor()
方法,可以取得給定屬性的描述符。這個方法接收兩個參數(shù):屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,有對應(yīng)的訪問器屬性的特性和數(shù)據(jù)屬性的特性。
6.2 創(chuàng)建對象
雖然Object
構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢杂脕韯?chuàng)建單個對象,但這些方式有個明顯的缺點:使用一個接口創(chuàng)建很多對象,會產(chǎn)生大量的重復(fù)代碼。為解決這個問題,人們開始使用工廠模式的一種變體。
6.2.1 工廠模式
考慮到ECMAScript 中無法創(chuàng)建類,開發(fā)人員就發(fā)明了一種函數(shù),用函數(shù)來封裝以特定接口創(chuàng)建對象的細(xì)節(jié)。
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;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
工廠模式雖然解決了創(chuàng)建多個相似對象的問題,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。隨著發(fā)展,又一個新模式出現(xiàn)了。
6.2.2 構(gòu)造函數(shù)模式
ECMAScript 中的構(gòu)造函數(shù)可用來創(chuàng)建特定類型的對象。像Object
和Array
這樣的原生構(gòu)造函數(shù),在運行時會自動出現(xiàn)在執(zhí)行環(huán)境中。此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對象類型的屬性和方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
與工廠模式的不同之處:
- 沒有顯式地創(chuàng)建對象;
- 直接將屬性和方法賦給了
this
對象; - 沒有
return
語句。
按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個大寫字母開頭,而非構(gòu)造函數(shù)則應(yīng)該以一個小寫字母開頭。
要創(chuàng)建Person
的新實例,必須使用new
操作符。以這種方式調(diào)用構(gòu)造函數(shù)實際上會經(jīng)歷以下 4 個步驟:
- 創(chuàng)建一個新對象;
- 將構(gòu)造函數(shù)的作用域賦給新對象(因此
this
就指向了這個新對象); - 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個新對象添加屬性);
- 返回新對象。
創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來可以將它的實例標(biāo)識為一種特定的類型;而這正是構(gòu)造函數(shù)模式勝過工廠模式的地方。
以這種方式定義的構(gòu)造函數(shù)是定義在
Global
對象(在瀏覽器中是window
對象)中的。
- 將構(gòu)造函數(shù)當(dāng)作函數(shù)
任何函數(shù),只要通過new
操作符來調(diào)用,那它就可以作為構(gòu)造函數(shù);而任何函數(shù),如果不通過new
操作符來調(diào)用,那它跟普通函數(shù)也不會有什么兩樣。
// 當(dāng)作構(gòu)造函數(shù)使用
var person = new person("Nicholas", 29, "Software Engineer");
// 作為普通函數(shù)調(diào)用
Person("Greg", 27, "Doctor"); //添加到 window
window.sayName(); //"Greg"
// 在另一個對象的作用域中調(diào)用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
- 構(gòu)造函數(shù)的問題
構(gòu)造函數(shù)的主要問題,就是每個方法都要在每個實例上重新創(chuàng)建一遍。可以通過把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外部來解決這個問題。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.syaName = sayName;
}
function sayName () {
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software English");
var person2 = new Person("Greg", 27, "Doctor");
新的問題:如果對象需要定義很多方法,那么就要定義很多個全局函數(shù),于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型模式來解決。
6.2.3 原型模式
我們創(chuàng)建的每個函數(shù)都有一個prototype
(原型)屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。按照字母意思來理解,prototype
就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個對象實例的原型對象。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。不必在構(gòu)造函數(shù)中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中。
function Person () {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
alert(this.name);
};
var person1 = new Person();
person.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
- 理解原型對象
只要創(chuàng)建了一個新函數(shù),就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個prototype
屬性,這個屬性指向函數(shù)的原型對象。在默認(rèn)情況下,所有原型對象都會自動獲得一個constructor
(構(gòu)造函數(shù))屬性,這個屬性包含一個指向prototype
屬性所在函數(shù)的指針。舉例講,Person.prototype. constructor
指向Person
。而通過這個構(gòu)造函數(shù),我們還可繼續(xù)為原型對象添加其他屬性和方法。
創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型對象默認(rèn)只會取得constructor
屬性;至于其他方法,則都是從Object
繼承而來的。
上圖展示了Person
構(gòu)造函數(shù)、Person
的原型屬性以及Person
現(xiàn)有的兩個實例之間的關(guān)系。
可以通過isPrototypeOf()
方法來確定對象之間是否存在這種關(guān)系。從本質(zhì)上講,如果[[Prototype]]
指向isPrototypeOf()
方法的對象(Person.prototype)
,那么這個方法就返回true
。
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
可以使用Object.getPrototypeOf()
方便取得一個對象的原型。
當(dāng)為對象實例添加一個屬性時,這個屬性就會屏蔽原型對象中保存的同名屬性。
使用hasOwnProperty
方法可以檢測一個屬性是存在于實例中,還是存在于原型中。
- 原型與 in 操作符
有兩種方式使用in
操作符:單獨使用和在for-in
循環(huán)中使用。單獨使用時,in
操作符會在通過對象能夠訪問給定屬性時返回true
,無論該屬性存在于實例中還是原型中。
alert("name" in person1); //true
alert("name" in person2); //true
同時使用hasOwnProperty()
方法和in
操作符,就可以確定該屬性到底是存在于對象中,還是存在于原型中。
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
只要in
操作符返回true
而hasOwnProperty()
返回false
就可以確定屬性是原型中的屬性。
function Person () {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false
要取得對象上所有可枚舉的實例屬性,可以使用 ECMAScript 5 的Object.keys()
方法。這個方法接收一個對象作為參數(shù),返回一個包含所有可枚舉屬性的字符串?dāng)?shù)組。
function Person() {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person.prototype);
alert(keys); //"name,age,job,sayName"
var p1 =new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"
如果你想要得到所有實例屬性,無論它是否可枚舉,都可以使用Object.getOwnPropertyNames()
方法。
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName"
注意結(jié)果中包含了不可枚舉的constructor
屬性。Object.keys()
和Object.getOwnPropertyNames()
方法都可以用來替代for-in
循環(huán)。
- 更簡單的原型語法
更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象。
function Person() {
}
Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName: function () {
alert(this.name);
}
};
這樣寫的話,constructor
屬性不再指向Person
了,本質(zhì)上完全重寫了默認(rèn)的prototype
對象,因此constructor
屬性也就變成了新對象的constructor
屬性(指向Object
構(gòu)造函數(shù)),不再指向Person
函數(shù)。可以特意將它設(shè)置回適當(dāng)?shù)闹怠?/p>
function Person() {
}
Person.prototype = {
constructor: Person,
name : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
注意,以這種方式重設(shè)`constructor`屬性會導(dǎo)致它的[[Enumerable]]特性被設(shè)置為true
。默認(rèn)情況下,原生的constructor
屬性是不可枚舉的,可以試試Object.defineProperty()
。
function Person() {
}
Person.prototype = {
};
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
- 原型的動態(tài)性
由于在原型中查找值得過程是一次搜索,因此對原型對象所做的任何修改都能夠立即從實例上反映出來--即使是先創(chuàng)建了實例后修改原型也照樣如此。
var friend = new Person();
Person.prototype.sayHi = function () {
alert("hi");
};
friend.sayHi(); //"hi"
如果重寫整個原型對象,會把原型修改為另一個對象,等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系。
function Person() {
}
var friend = new Person();
Person.prototype = {
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
- 原生對象的原型
原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創(chuàng)建的。所有原生引用類型(Object, Array, String, 等等)都在其構(gòu)造函數(shù)的原型上定義了方法。例如,在Array.prototype
中可以找到sort()
方法,而在String.prototype
中可以找到substring()
方法。
通過原聲對象的原型,不僅可以取得所有默認(rèn)方法的引用,而且也可以定義新方法。可以像修改自定義對象的原型一樣修改原生對象的原型,因此可以隨時添加方法。
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
不推薦在產(chǎn)品化的程序中修改原生對象的原型。如果因某個實現(xiàn)中缺少某個方法,就在原生對象的原型中添加這個方法,那么當(dāng)在另一個支持該方法的實現(xiàn)中運行代碼時,就可能會導(dǎo)致命名沖突。而且,這樣做也可能會意外地重寫原生方法。
- 原型對象的問題
原型模式也存在缺點。他省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),結(jié)果所有實例在默認(rèn)情況下都將取得相同的屬性值。雖然這會在某種程度上帶來一些不方便,但還不是原型的最大問題。原型模式的最大問題是由其共享的本性所導(dǎo)致的。
原型中所有屬性是被很多實例共享的,這種共享對于函數(shù)非常合適。對于那些包含基本值的屬性倒也說的過去,畢竟,通過在實例上添加一個同名屬性,可以隱藏原型中的對應(yīng)屬性。然而,對于包含引用類型值的屬性來說,問題就比較突出了。
function Person() {
}
Person.prototype = {
constructor: Person,
friends : ["shelby", "court"]
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("van");
alert(person1.friends); //"shelby,court,van"
alert(person2.friends); //"shelby,court,van"
alert(person1.friends === person2.friends); //true
修改了person1.friends
引用的數(shù)組,向數(shù)組中添加了一個字符串。由于friends
數(shù)組存在于Person.prototype
而非person1
中,所以剛剛提到的修改也會通過person2.friends
反映出來。這個問題正是我們很少看到有人單獨使用原型模式的原因所在。
6.2.4 組合使用構(gòu)造函數(shù)模式和原型模式
創(chuàng)建自定義類型的最常見方式,就是組合使用構(gòu)造函數(shù)模式與原型模式。構(gòu)造函數(shù)模式用于定義實例屬性,而原型模式用于定義方法和共享的屬性。
function Person(name, age, job) {
this.name = name;
this.name = age;
this.job = job;
this.friends = ["shelby", "court"];
}
Person.prototype = {
constructor : Person,
sayName : function() {
alert(this.name);
}
}
var person1 =new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("van");
alert(person1.friends); //"shelby, court, van"
alert(person2.friends); //"shelby,count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
這種構(gòu)造函數(shù)與原型混成的模式,是目前使用最廣泛,認(rèn)同度最高的一種創(chuàng)建自定義類型的方法。可以說,這是用來定義應(yīng)用類型的一種默認(rèn)模式。
6.2.5 動態(tài)原型模式
有其他 OO 語言經(jīng)驗的開發(fā)人員看到獨立的構(gòu)造函數(shù)和原型時,很可能會感到非常困惑。動態(tài)原型模式把所有信息都封裝在了構(gòu)造函數(shù)中,而通過在構(gòu)造函數(shù)中初始化原型(僅在必要的情況下),又保持了同時使用構(gòu)造函數(shù)和原型的優(yōu)點。換句話說,可以通過檢查某個應(yīng)該存在的方法是否有效,來決定是否需要初始化原型。
function Person(name, age, job) {
//屬性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
在sayName()
方法不存在的情況下,才會將它添加到原型中。這段代碼只會在初次調(diào)用構(gòu)造函數(shù)時才會執(zhí)行。此后,原型已經(jīng)完成初始化。這里對原型所做的修改,能夠立即在所有實例中得到反映。
使用動態(tài)原型模式時,不能使用對象字面量重寫原型。
6.2.6 寄生構(gòu)造函數(shù)模式
通常,在前述模式不適用的情況下,可以使用寄生構(gòu)造函數(shù)模式。這種模式的基本思想是創(chuàng)建一個函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建對象的代碼,然后再返回新創(chuàng)建的對象;但從表面上看,這個函數(shù)又很像是典型的構(gòu)造函數(shù)。
function Person (name, age, job) {
var o = new Object();
o.name = name;
o.job = job;
o.sayName = function () {
alert(this.name);
};
return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
除了使用new
操作符并把使用的包裝函數(shù)叫做構(gòu)造函數(shù)之外,這個模式跟工廠模式其實是一模一樣的。
通過在構(gòu)造函數(shù)的末尾添加一個return
語句,可以重寫調(diào)用函數(shù)時返回的值。
這個模式可以在特殊的情況下用來為對象創(chuàng)建構(gòu)造函數(shù)。假設(shè)我們想創(chuàng)建一個具有額外方法的特殊數(shù)組。由于不能直接修改Array
構(gòu)造函數(shù),因此可以使用這個模式。
function SpecialArray () {
//創(chuàng)建數(shù)組
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function () {
return this.join("|");
};
//返回數(shù)組
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
關(guān)于寄生構(gòu)造函數(shù)模式。返回的對象與構(gòu)造函數(shù)或者與構(gòu)造函數(shù)的原型屬性之間沒有關(guān)系;也就是說,構(gòu)造函數(shù)返回的對象與構(gòu)造函數(shù)外部創(chuàng)建的對象沒有什么不同。為此,不能依賴instanceof
操作符來確定對象類型。由于存在上述問題,建議使用其他模式。
6.2.7 穩(wěn)妥構(gòu)造函數(shù)模式
道格拉斯發(fā)明了穩(wěn)妥對象這個概念。其指的是沒有公共屬性,而且其方法也不引用this
的對象。穩(wěn)妥對象最適合在一些安全環(huán)境中,或者在防止數(shù)據(jù)被其他應(yīng)用程序(如 Mashup 程序)改動時使用。穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類似的模式,但有兩點不同:一是新創(chuàng)建對象的實例方法不引用this
;二是不使用new
操作符調(diào)用構(gòu)造函數(shù)。按照穩(wěn)妥構(gòu)造函數(shù)的要求,可以將前面的Person
構(gòu)造函數(shù)重寫如下。
function Person (name, age, job) {
//創(chuàng)建要返回的對象
var o = new Object();
//可以在這里定義私有變量和函數(shù)
//添加方法
o.sayName = function () {
alert(name);
};
//返回對象
return o;
}
在這種模式創(chuàng)建的對象中,除了使用sayName()
方法之外,沒有其他方法訪問name
的值。可以像下面使用穩(wěn)妥的Person
構(gòu)造函數(shù)。
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
與寄生構(gòu)造函數(shù)模式類似,使用穩(wěn)妥構(gòu)造函數(shù)模式創(chuàng)建的對象與構(gòu)造函數(shù)之間也沒有什么關(guān)系,因此
instanceof
操作符對這種對象也沒有意義。
6.3 繼承
ECMAScript 只支持實現(xiàn)繼承,而且其實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)的。
6.3.1 原型鏈
基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構(gòu)造函數(shù),原型和實例的關(guān)系:每個構(gòu)造函數(shù)都有一個原型對象,原型對象都包含一個指向構(gòu)造函數(shù)的指針,而實例都包含一個指向原型對象的內(nèi)部指針。那么,假如我們讓原型對象等于另一個類型的實例,此時的原型對象將包含一個指向另一個原型的指針,相應(yīng)地,另一個原型中也包含這一個指向另一個構(gòu)造函數(shù)的指針。假如另一個原型又是另一個類型的實例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。
實現(xiàn)原型鏈有一種基本模式,其代碼大致如下。
function SuperType () {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType () {
this.subproperty = false;
}
//繼承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
1.別忘記默認(rèn)的原型
事實上,前面例子中展示的原型鏈還少一環(huán)。我們知道,所有引用類型默認(rèn)都繼承了Object
,而這個繼承也是通過原型鏈實現(xiàn)的。大家要記住,所有函數(shù)的默認(rèn)原型都是Object
的實例,因此默認(rèn)原型都會包含一個內(nèi)部指針,指向Object.prototype
。這正是所有自定義類型都會繼承toString()
、valueOf()
等默認(rèn)方法的根本原因。
一句話,SubType 繼承了 SuperType, 而 SuperType 繼承了 Object。當(dāng)調(diào)用instance.toString()
時,實際上調(diào)用的是保存在Object.prototype
中的那個方法。
2.確定原型和實例的關(guān)系
可以通過兩種方式來確定原型和實例之間的關(guān)系。第一種方式是使用instanceof
操作符,只要用這個操作符來測試實例與原型鏈中出現(xiàn)過的構(gòu)造函數(shù),結(jié)果就會返回true
。
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
第二種方式是使用isPrototypeOf()
方法。同樣,只要是原型鏈中出現(xiàn)的原型,都可以說是該原型鏈所派生的實例的原型。
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
3.謹(jǐn)慎地定義方法
子類型有時候需要重寫超類型中的某個方法,或者需要添加超類型中不存在的某個方法。給原型添加方法的代碼一定要放在替換原型的語句之后。
還有一點,即在通過原型鏈實現(xiàn)繼承時,不能使用對象字面量創(chuàng)建原型方法。因為這樣做就會重寫原型鏈。
SubType.prototype = new SuperType();
SubType.prototype = {
getSubValue : function () {
return this.subproperty;
},
someOtherMethod: function () {
return false;
}
};
以上代碼展示了又將原型替換成一個對象字面量而導(dǎo)致的問題。由于現(xiàn)在的原型包含的是一個Object
的實例,而非SuperType
的實例,因此我們設(shè)想的原型鏈已經(jīng)被切斷。
4.原型鏈的問題
最主要的問題來自包含引用類型值的原型。前面介紹過包含引用類型值的原型屬性會被所有實例共享;而這也是為什么要在構(gòu)造函數(shù)中,而不是在原型對象中定義屬性的原因。在通過原型來實現(xiàn)繼承時,原型實際上會變成另一個類型的實例。
function SuperType () {
this.colors = {"red", "blue", "green"};
}
function SubType () {
}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"
原型鏈的第二個問題是:在創(chuàng)建子類型的實例時,不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。實際上,應(yīng)該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)。實踐中很少會單獨使用原型鏈。
6.3.2 借用構(gòu)造函數(shù)
在解決原型中包含引用類型所帶來問題的過程中,開發(fā)人員開始使用一種叫做借用構(gòu)造函數(shù)的技術(shù)(偽造對象或經(jīng)典繼承)。這種技術(shù)的基本思想相當(dāng)簡單,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)。通過使用apply()
和call()
方法也可以在新創(chuàng)建的對象上執(zhí)行構(gòu)造函數(shù)。
function SuperType () {
this.colors = {"red", "blue", "green"};
}
function SubType () {
//繼承了 SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red, blue, green, black"
var instance2 = new SubType();
alert(instance2.colors); //"red, blue, green"
1.傳遞參數(shù)
借用構(gòu)造函數(shù)可以在子類型構(gòu)造函數(shù)中向超類型構(gòu)造函數(shù)傳遞參數(shù)。
function SuperType (name) {
this.name = name;
}
function SubType () {
//繼承了 SuperType,同時還傳遞了參數(shù)
SuperType.call(this, "Nicholas");
//實例屬性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //"Nicholas"
alert(instance.age); //29
2.借用構(gòu)造函數(shù)的問題
無法避免構(gòu)造函數(shù)模式存在的問題--方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無從談起了。借用構(gòu)造函數(shù)的技術(shù)也是很少單獨使用的。
6.3.3 組合繼承
有時候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊。其背后的思路是使用原型鏈實現(xiàn)對原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承。這樣,既通過在原型上定義方法實現(xiàn)了函數(shù)復(fù)用,又能夠保證每個實例都有它自己的屬性。
function SuperType (name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
alert(this.name);
};
function SubType(name, age) {
//繼承屬性
SuperType.call(this, name);
this.age = age;
}
//繼承方法
SubType.prototype = new SuperType ();
SubType.prototype.sayAge = function () {
alert(this.age);
} ;
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red, blue, green, black"
instance1.sayName(); //"Nicholas"
instance1.sayAge(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red, blue, green"
instance2.sayName(); //"Greg"
instance2.sayAge(); //27
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,成為 JavaScript 中最常用的繼承模式。而且,instanceof
和isPrototypeOf()
也能夠用于識別基于組合繼承創(chuàng)建的對象。
6.3.4 原型式繼承
function object(o) {
function F () {}
F.prototype = o;
return new F();
}
在object()
函數(shù)內(nèi)部,先創(chuàng)建了一個臨時性的構(gòu)造函數(shù),然后將傳入的對象作為這個構(gòu)造函數(shù)的原型,最后返回了這個臨時類型的一個新實例。從本質(zhì)上講,object()
對傳入其中的對象執(zhí)行了一次淺復(fù)制。
這種原型繼承,要求你必須有一個對象可以作為另一個對象的基礎(chǔ)。
ECMAScript 5 通過新增Object.create()
方法規(guī)范化了原型式繼承。接收兩個參數(shù):一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象。在傳入一個參數(shù)的情況下,Object.create()
與object()
方法的行為相同。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"
6.3.5 寄生式繼承
寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。
使用寄生式繼承來為對象添加函數(shù),會由于不能做到函數(shù)復(fù)用而降低效率;這一點與構(gòu)造函數(shù)模式類似。
6.3.6 寄生組合式繼承
組合繼承最大的問題是會調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部。
寄生組合式繼承,即通過借用構(gòu)造函數(shù)來繼承屬性,通過原型鏈的混成形式來繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù)。
6.4 小結(jié)
ECMAScript 支持面向?qū)ο缶幊蹋皇褂妙惢蛘呓涌凇ο罂梢栽诖a執(zhí)行過程中創(chuàng)建和增強,因此具有動態(tài)性而非嚴(yán)格定義的實體。在沒有類的情況下,可以采用下列模式創(chuàng)建對象。
- 工廠模式,使用簡單的函數(shù)創(chuàng)建對象,為對象添加屬性和方法,然后返回對象。這個模式后來被構(gòu)造函數(shù)模式所取代。
- 構(gòu)造函數(shù)模式,可以創(chuàng)建自定義引用類型,可以像創(chuàng)建內(nèi)置對象實例一樣使用
new
操作符。不過,構(gòu)造函數(shù)模式也有缺點,即它的每個成員都無法得到復(fù)用,包括函數(shù)。由于函數(shù)可以不局限于任何對象(即與對象具有松散耦合的特點),因此沒有理由不在多個對象間共享函數(shù)。 - 原型模式,使用構(gòu)造函數(shù)的
prototype
屬性來指定那些應(yīng)該共享的屬性和方法。組合使用構(gòu)造函數(shù)模式和原型模式時,使用構(gòu)造函數(shù)定義實例屬性,而使用原型定義共享的屬性和方法。
JavaScript 主要通過原型鏈實現(xiàn)繼承。原型鏈的構(gòu)建是通過將一個類型的實例賦值給另一個構(gòu)造函數(shù)的原型實現(xiàn)的。這樣,子類型就能夠訪問超類型的所有屬性和方法,這一點與基于類的繼承很相似。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共享的屬性和方法,而通過借用構(gòu)造函數(shù)繼承實例屬性。
此外,還存在下列可供選擇的繼承模式。 - 原型式繼承,可以在不必預(yù)先定義構(gòu)造函數(shù)的情況下實現(xiàn)繼承,其本質(zhì)是執(zhí)行對給定對象的淺復(fù)制。而復(fù)制得到的副本還可以得到進(jìn)一步改造。
- 寄生式繼承,與原型式繼承非常相似,也是基于某個對象或某些信息創(chuàng)建一個對象,然后增強對象,最后返回對象。為了解決組合繼承模式由于多次調(diào)用超類型構(gòu)造函數(shù)而導(dǎo)致的低效率問題,可以將這個模式與組合繼承一起使用。
- 寄生組合式繼承,集寄生式繼承和組合繼承的優(yōu)點于一身,是實現(xiàn)基于類型繼承的最有效方式。