1.工廠模式
考慮到ECMAScript中無法創(chuàng)建類,開發(fā)人員就發(fā)明了一種函數(shù),用函數(shù)來封裝以特定接口創(chuàng)建對象的細(xì)節(jié),如下所示:
function createPerson(name,age,job){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function(){
console.log(this.name);
}
return obj;
}
var person1 = createPerson('Tom',20,'teacher');
var person2 = createPerson('Jone',27,'Doctor');
函數(shù)createPerson()能夠根據(jù)接受的參數(shù)來構(gòu)建一個(gè)包含所有必要信息的Person對象??梢詿o數(shù)次的調(diào)用這個(gè)函數(shù),而每次他都會返回一個(gè)包含三個(gè)屬性一個(gè)方法的對象。工廠模式雖然解決了創(chuàng)建多個(gè)相似對象的問題,但卻沒有解決對象識別的問題。
2.構(gòu)造函數(shù)模式
我們可以使用ECMAScript中的構(gòu)造函數(shù)來創(chuàng)建特定類型的對象,此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對象類型的屬性和方法。可以使用構(gòu)造函數(shù)模式將前面的例子重寫。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name)
}
}
var person1 = new Person('Tom',20,'Teacher');
var person2 = new Person('Jone',27,'Doctor');
在這個(gè)例子中,Person
函數(shù)取代了createPerson
函數(shù),兩者存在以下的不同之處:
- 沒有顯示的創(chuàng)建對象
- 直接將屬性和方法賦值給 this
- 沒有 return 語句
此外,還應(yīng)該注意到函數(shù)名Person使用的是大寫字母P。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個(gè)大寫字母開頭,而非構(gòu)造函數(shù)應(yīng)該以一個(gè)小寫字母開頭。
要?jiǎng)?chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用構(gòu)造函數(shù)實(shí)際上會經(jīng)歷以下4個(gè)步驟:
(1)創(chuàng)建一個(gè)新對象;
(2)將構(gòu)造函數(shù)的作用域賦給新對象(因此this就指向了這個(gè)新對象);
(3)執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對象添加屬性);
(4)返回新對象。
其中 person1
和person2
分別著Person
的一個(gè)不同的實(shí)例,它們都有一個(gè)constructor
(構(gòu)造器)屬性,且該屬性都指向Person
console.log(person1.constructor == Person); //true
console.log(person2.constructor == Person); //true
對象的constructor
屬性最初是用來標(biāo)示對象類型的。但是,提到檢測對象類型,還是instanceof
操作符要更可靠一些。
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //true
將構(gòu)造函數(shù)當(dāng)作函數(shù)
構(gòu)造函數(shù)與其他函數(shù)的唯一區(qū)別,就在于調(diào)用他們的方式不同。任何函數(shù),只要通過new
操作符來調(diào)用,那它就可以作為構(gòu)造函數(shù);反之,就是普通函數(shù)。
//當(dāng)作構(gòu)造函數(shù)調(diào)用
var person = new Person('Tom',20,'Teacher');
person.sayName();
//作為普通函數(shù)調(diào)用
Person('Jone',27,'Doctor');
window.sayName();
//在另一個(gè)對象的作用域中調(diào)用
var obj = new Object();
Person.call(obj,'Grey',25,'Nurse');
obj.sayName();
構(gòu)造函數(shù)的問題
當(dāng)我們每創(chuàng)建一個(gè)對象,那么對象中的方法也會被重新創(chuàng)建一遍,這樣就會導(dǎo)致不同的作用域鏈和標(biāo)識解析。
我們可以像下面這樣,通過把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外部來解決這個(gè)問題。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
console.log(this.name)
}
var person = new Person('Tom',20,'Teacher');
如果對象需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),于是我們這個(gè)自定義的應(yīng)用類型就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型模式來解決。
3.原型模式
我們創(chuàng)建的每一個(gè)函數(shù)都會有一個(gè) prototype
(原型)屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對象,這個(gè)對象的作用就是包含由特定類型的所有實(shí)例所共享的屬性和方法。使用原型對象的好處是可以讓所有對象實(shí)例共享它所包含的屬性和方法。
function Person(){
}
Person.prototype.name = 'Grey';
Person.prototype.age = 20;
Person.prototype.job = 'Teacher';
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); //'Grey"
var person2 = new Person();
person2.sayName(); //'Grey"
console.log(person1.sayName == person2.sayName); //true
在這我們將name
、age
、job
、sayName()
直接添加到了 Person
中的prototype
屬性中,person
中prototype
中的屬性都被 Person 的實(shí)例化對象所共享。
理解原型對象
每當(dāng)我們創(chuàng)建一個(gè)函數(shù),就會根據(jù)一種特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè) prototype 屬性,這個(gè)屬性指向函數(shù)的原型對象。在默認(rèn)情況下,所有的原型對象都會有一個(gè) constructor 屬性,這個(gè)屬性指向 prototype 屬性所在的函數(shù)的指針。Person.prototype.constructor 就指向 Person。而通過這個(gè)構(gòu)造函數(shù),我們還可以繼續(xù)為原型對象添加其他屬性和方法。
以前面使用Person
構(gòu)造函數(shù)和Person.prototype
創(chuàng)建實(shí)例的代碼為例,下圖展示了各個(gè)對象之間的關(guān)系:
每當(dāng)讀取某個(gè)屬性時(shí),都會進(jìn)行一次搜索。首先會搜索對象實(shí)例本身,如果搜索到了該具體名字的屬性,就會返回該屬性的值,如果沒有搜索到,那么就會繼續(xù)搜索指針指向的原型對象,在原型對象中如果搜索到的話就會返回屬性值。
可以發(fā)現(xiàn),在搜索的時(shí)候是先搜索的是對象實(shí)例本身,然后才是原型對象。如果在對象本身和原型對象含有相同的屬性,那么原型對象中的屬性和方法就會被對象實(shí)例中相應(yīng)的屬性和方法所覆蓋。如下所示:
function Person(){
}
Person.prototype.name = 'Grey';
Person.prototype.age = 20;
Person.prototype.job = 'Teacher';
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
person1.name = 'Tom';
person1.sayName(); //'Tom'
var person2 = new Person();
person2.sayName(); //'Grey'
當(dāng)為對象實(shí)例添加一個(gè)屬性時(shí),這個(gè)屬性就會屏蔽原型對象中保存的同名屬性,換居話說,添加這個(gè)屬性只會阻止我們訪問原型中的那個(gè)屬性,但不會修改那個(gè)屬性(使用delete
操作符則可以完全刪除實(shí)例屬性,從而讓我們能夠重新訪問原型中的屬性)。
更簡單的原型語法
前面的例子中每添加一個(gè)屬性和方法就要前一遍Person.prototype
。為了減少不必要的輸入,更常見的做法是用一個(gè)包含所有屬性和方法的對象字面量來重寫整個(gè)原型對象。
function Person(){
}
Person.prototype = {
name:'Grey',
age:20,
job:'Teacher',
sayName:function(){
console.log(this.name);
}
}
在上面的代碼中,我們將Person.prototype設(shè)置為等于一個(gè)以字面量形式創(chuàng)建的新對象。最終結(jié)果相同,但有一個(gè)例外:constructor屬性不再指向Person了。
如果constructor的值真的很重要,可以像下面這樣特意將它設(shè)置回適當(dāng)?shù)闹怠?/p>
function Person(){
}
Person.prototype = {
constructor:Person,
name:'Grey',
age:20,
job:'Teacher',
sayName:function(){
console.log(this.name);
}
}
原型對象的問題
原型對象省略了構(gòu)造函數(shù)傳遞參數(shù)初始化的步驟,結(jié)果導(dǎo)致所有的實(shí)例都會共享相同的屬性,這是非常不方便的。如果創(chuàng)建的屬性而引用類型的話,那么會造成不同的實(shí)例的混亂。
function Person(){
}
Person.prototype = {
constructor:Person,
name:'Grey',
age:20,
job:'Teacher',
friends:['Jone','Tom'],
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person();
person1.friends.push('Van');
console.log(person1.friends); //["Jone", "Tom", "Van"]
var person2 = new Person();
console.log(person2.friends); //["Jone", "Tom", "Van"]
4.組合使用構(gòu)造函數(shù)模式和原型模式
構(gòu)造函數(shù)模式用于定義實(shí)例屬性,而原型模式用于定義方法和共享的屬性。這樣每個(gè)實(shí)例都會有一份實(shí)例屬性副本,又同時(shí)含有一份共享的屬性和方法,這樣最大限度的節(jié)省了內(nèi)存。另外,這種混合模式還支持向構(gòu)造函數(shù)傳遞參數(shù)。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Jone','Tom'];
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person('Grey',20,'Teacher');
person1.friends.push('Van');
console.log(person1.friends); //["Jone", "Tom", "Van"]
var person2 = new Person('Nicholas',27,'Doctor');
console.log(person2.friends); //["Jone", "Tom"]
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true
動態(tài)原型模式
動態(tài)原型模式將所有信息都封裝在了構(gòu)造函數(shù)中,而通過在構(gòu)造函數(shù)中初始化原型,又保持了同時(shí)使用構(gòu)造函數(shù)和原型的優(yōu)點(diǎn)。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Jone','Tom'];
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
console.log(this.name);
}
}
}
var person1 = new Person('Grey',20,'Teacher');
person1.sayName();
5.寄生構(gòu)造函數(shù)模式
function Person(name,age,job){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function(){
console.log(this.name);
}
return obj;
}
var person = new Person('Grey',20,'Teacher');
person.sayName(); //'Grey'
這個(gè)模式可以在特殊的情況下用來為對象構(gòu)建構(gòu)造函數(shù)。假設(shè)我們想創(chuàng)建一個(gè)具有額外方法的特殊數(shù)組。由于不能直接修改Array構(gòu)造函數(shù),因此可以使用這個(gè)模式。
function SpecialArray(){
var newArr = new Array();
newArr.push.apply(newArr,arguments);
newArr.toPipedString = function(){
return this.join('|')
}
return newArr;
}
var colors = new SpecialArray('red','pink','greey');
console.log(colors.toPipedString()); //"red|pink|greey"
關(guān)于寄生構(gòu)造函數(shù)模式,有一點(diǎn)需要說明:首先,返回的對象與構(gòu)造函數(shù)或者構(gòu)造函數(shù)的原型屬性之間沒有關(guān)系;也就是說,構(gòu)造函數(shù)返回的對象與在構(gòu)造函數(shù)外部創(chuàng)建的對象沒有什么不同。
6.穩(wěn)妥構(gòu)造函數(shù)模式
所謂穩(wěn)妥對象,指的是沒有公共屬性,而且其方法也不引用this的對象。
穩(wěn)妥構(gòu)造函數(shù)遵循與寄生構(gòu)造函數(shù)類似的模式,但有兩點(diǎn)不同:一是新創(chuàng)建對象的實(shí)例方法不引用this;二是不使用new操作符調(diào)用構(gòu)造函數(shù)。
function Person(name,age,job){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function(){
console.log(name);
}
return obj;
}
var person1 = Person('Grey',20,'Teacher');
person1.sayName();