面向?qū)ο蟮恼Z言都有一個標(biāo)志答姥,那就是它們都有類的概念,而通過類可以創(chuàng)建任意多個具有相同屬性和方法的對象铣缠。但是ECMAScript中沒有類的概念,因此它的對象也與基于類的語言中的對象有所不同。
創(chuàng)建自定義對象的兩種方法:
// 創(chuàng)建一個Object實例
var person = new Object();
person.name = 'andy';
person.age = 29;
person.sayName = function () {
alert(this.name)
}
// 對象字面量
var person = {
name: 'andy',
age: 29,
sayName: function () {
console.log(this.name)
}
}
1. 工廠模式
這種模式抽象了創(chuàng)建具體對象的過程卿拴∏贸ぃ考慮到在ECMAScript中無法創(chuàng)建類,開發(fā)人員就發(fā)明了一種函數(shù)捡硅,用函數(shù)來封裝以特定接口創(chuàng)建對象的細(xì)節(jié)哮内。
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function () {
alert(o.name);
}
return o;
}
var person1 = createPerson('andy', 21);
函數(shù)createPerson()能夠根據(jù)接受的參數(shù)來構(gòu)建一個包含所有必要信息的person對象∽尘拢可以無數(shù)次地調(diào)用這個函數(shù)北发,而每次它會返回一個包含兩個屬性一個方法的對象。工程模式雖然解決了創(chuàng)建多個類似對象的問題喷屋,但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)琳拨。隨著JavaScript的發(fā)展,又一個新模式出現(xiàn)了屯曹。
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) {
this.name = name;
this.age = age;
this.sayName = function () {
alert(this.name);
}
}
var per = new Person('andy', 21);
per.sayName(); // andy
在該構(gòu)造函數(shù)中批什,有以下幾點不同:
- 沒有顯式地創(chuàng)建對象;
- 直接將屬性和方法賦給了this對象社搅;
- 沒有return語句驻债。
要創(chuàng)建Person對象的新實例乳规,必須使用new操作符。以這種方式調(diào)用構(gòu)造函數(shù)實際上會經(jīng)歷以下4個步驟:
- 創(chuàng)建一個新對象 合呐;
- 將構(gòu)造函數(shù)的作用域賦給新對象暮的;
- 執(zhí)行構(gòu)造函數(shù)中的代碼;
- 返回新對象淌实。
我們可以用instanceof操作符來進(jìn)行對象檢測:
console.log(per.constructor === Person); // true
console.log(per instanceof Object); // true
console.log(per instanceof Person); // true
console.log(per instanceof String); // false
構(gòu)造函數(shù)模式雖然好用冻辩,但也并非沒有缺點。使用構(gòu)造函數(shù)的主要問題拆祈,就是每個方法都要在每個實例上重新創(chuàng)建一遍恨闪。每定義一個函數(shù),也就實例化了一個對象放坏。以這種方式創(chuàng)建函數(shù)咙咽,會導(dǎo)致不同的作用域鏈和標(biāo)識符解析,但創(chuàng)建Function新實例的機制仍然是相同的淤年。因此钧敞,不同實例上的同名函數(shù)是不相等的。
console.log(per1.sayName === per2.sayName); // false
創(chuàng)建兩個完全同樣任務(wù)的Function實例的確沒有必要麸粮;況且有this對象在溉苛,根本不用再執(zhí)行代碼前就把函數(shù)綁定到特定對象上面。所以這就促使我們可以使用原型模式來自定義對象弄诲。
3. 原型模式
a. 原型語法
我們創(chuàng)建的每一個函數(shù)都有一個prototype屬性愚战,這個屬性是一個指針,指向一個對象威根。而這個對象的用途是包含由特定類型的所有實例共享的屬性和方法凤巨。使用原型對象的好處是可以讓所有對象實例共享它所包含的屬性和方法。換句話說洛搀,不必在構(gòu)造函數(shù)中定義對象實例的信息敢茁,而是可以將這些信息直接添加到原型對象中:
function Person() {
}
Person.prototype.name = 'andy';
Person.prototype.age = 13;
Person.prototype.sayName = function () {
console.log(this.name);
}
var per1 = new Person();
var per2 = new Person();
per1.sayName(); // andy
per2.sayName(); // andy
per1.name = 'qiqi';
per1.sayName(); // qiqi
在此,我們將sayName()方法的所有屬性直接添加到了Person的prototype屬性中留美,構(gòu)造函數(shù)編程了空函數(shù)彰檬。即使如此,也仍然可以通過調(diào)用構(gòu)造函數(shù)來創(chuàng)建新對象谎砾,而且新對象還會具有相同的屬性和方法逢倍。但與構(gòu)造函數(shù)模式不同的是,新對象的這些屬性和方法是由所有實例共享的景图。也就是說per1和per2訪問的都是同一組屬性和同一個sayName()函數(shù)较雕。
無論什么時候,只要創(chuàng)建了一個函數(shù),就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個prototype屬性亮蒋,這個屬性指向函數(shù)的原型對象扣典。在默認(rèn)情況下,所有原型對象都會自動獲得一個constructor屬性慎玖,這個屬性包含一個指向prototype屬性所在函數(shù)的指針贮尖。以前邊例子來說,Person.prototype.constructoy指向Person趁怔。通過這個構(gòu)造函數(shù)湿硝,我們還可繼續(xù)為原型對象添加其他屬性和方法。
幾種操作符:
- isPrototypeOf() 確定對象之間是否存在關(guān)系
console.log(Person.prototype.isPrototypeOf(person1)); // true
- getPrototypeof() 返回prototype的值
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person1.name)); // andy
- hasOwnProperty() 用來檢測一個屬性是存在于實例中润努,還是存在于原型中关斜。
console.log(per1.hasOwnProperty('name')); // false
per1.name = 'qiqi';
console.log(per1.hasOwnProperty('name')); // true
- in 只要通過對象能夠訪問到屬性就返回true,不管是在實例中還是在原型中
console.log('name' in per1); // true
console.log('name' in Person); // true
b. 更簡單的原型語法
在前面的例子中铺浇,每添加一個屬性和方法就要敲一遍prototype蚤吹,為了減少不必要的輸入可以采用以下的寫法:
function Person() {
}
Person.prototype = {
constructor: Person, // 不添加的話constructor會變化,此處為了使該指針指向原型
name: 'andy',
age: 21,
sayName: function() {
console.log(this.name);
}
}
原型模式并不是沒有缺點随抠。首先,它省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié)繁涂,結(jié)果所有實例在默認(rèn)情況下都取得相同的屬性值拱她。雖然這會在某種程度上帶來一些不方便,但這不是原型的最大問題扔罪。原型模式的最大問題是由其共享的本性導(dǎo)致的秉沼。
c. 組合使用構(gòu)造函數(shù)和原型模式
結(jié)合以上各種優(yōu)點缺點,在自定義對象的時候可以采用組合使用的方式!
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this..name);
}
}
4. 繼承
繼承是面向?qū)ο笳Z言中一個最為津津樂道的概念矿酵,許多面向?qū)ο笳Z言都支持兩種繼承方式:接口繼承和實現(xiàn)繼承唬复。接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法全肮。因為函數(shù)沒有簽名敞咧,所以ECMAScript中無法實現(xiàn)接口繼承,只支持實現(xiàn)繼承辜腺,而且其實實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)休建。
a. 原型鏈
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType(); // 原型鏈繼承
SubType.prototype.getSubValue = function() {
return this.subproperty;
}
var ins = new SubType();
console.log(ins.getSubValue());
以上代碼定義了兩個類型:Supertype和SubType。每個類型分別有一個屬性和一個方法评疗。他們的主要區(qū)別是SubType繼承了SuperType测砂,而繼承是通過創(chuàng)建SuperType的實例,并將該實例賦給SubType.prototype實現(xiàn)的百匆。實現(xiàn)的本質(zhì)是重寫原型對象砌些,代之以一個新類型的實例。換句話說加匈,原來存在于SuperType的實例中的所有屬性和方法存璃,現(xiàn)在也存在于SubType中了 仑荐。確立了繼承關(guān)系之后,給SubType.prototype添加了一個方法有巧。
b. 借用構(gòu)造函數(shù)
在解決原型鏈中包含引用類型值所帶來的問題中释漆,開發(fā)人員開始使用一種叫做借用構(gòu)造函數(shù)的技術(shù)。這種技術(shù)的基本思想相當(dāng)簡單篮迎,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù)男图。別忘了,函數(shù)只不過是在特定環(huán)境中執(zhí)行代碼的對象甜橱,因此通過使用apply()和call()方法也可以在新創(chuàng)建的對象上執(zhí)行構(gòu)造函數(shù):
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'black'];
}
function SubType() {
// 繼承了SuperType逊笆,還傳遞了參數(shù)
SuperType.call(this, 'andy');
}
c. 組合繼承
組合繼承,有時候也叫偽經(jīng)典繼承岂傲,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一起难裆,從而發(fā)揮二者之長的一種繼承方式。其背后的思路是使用原型鏈實現(xiàn)對原型屬性和方法的繼承镊掖,而通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承乃戈。這樣,既通過在原型上定義方法實現(xiàn)了函數(shù)的復(fù)用亩进,又能保證每個實例都有它自己的屬性:
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'black'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType() {
// 繼承屬性
SuperType.call(this, name);
this.age = age;
}
// 繼承方法
SubType.prototype = new SuperType();
SubType.prototype.sayName = function () {
console.log(this.name);
}