一、工廠模式
所謂的工廠模式就是,把創(chuàng)建具體對(duì)象的過(guò)程抽象成了一個(gè)函數(shù),每次調(diào)用這個(gè)函數(shù)都會(huì)返回一個(gè)相似的對(duì)象。
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");
person1.sayName(); //"Nicholas"
person2.sayName(); //"Greg"
工廠模式雖然解決了創(chuàng)建多個(gè)相似對(duì)象的問(wèn)題,但卻沒(méi)有解決對(duì)象識(shí)別的問(wèn)題(即怎樣知道一個(gè)對(duì)象的類型)。
二、構(gòu)造函數(shù)模式
js里經(jīng)常如此寫(xiě)var obj=new Object();var arr=new Array();,Object和Array就是構(gòu)造函數(shù),使用new操作符可以創(chuàng)建相應(yīng)類型的對(duì)象,使用instanceof可以驗(yàn)證對(duì)象的類型,例如:
alert(arr instance Array); //true
構(gòu)造函數(shù)模式就是,自定義像Array和Object等這樣的構(gòu)造函數(shù),并使用new操作符調(diào)用它來(lái)創(chuàng)建自定義類型對(duì)象的方法。
例如:
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");
person1.sayName(); //"Nicholas"
person2.sayName(); //“Greg”
- new操作符
使用new操作符調(diào)用,Person就是一個(gè)構(gòu)造函數(shù)
要?jiǎng)?chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用構(gòu)造函數(shù)實(shí)際上會(huì)經(jīng)歷一下4個(gè)步驟:
(1)創(chuàng)建一個(gè)新對(duì)象
(2)將構(gòu)造函數(shù)的作用域賦給新對(duì)象,即把構(gòu)造函數(shù)的this指向這個(gè)新對(duì)象
(3)執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性)
(4)返回新對(duì)象
如果不使用new,Person就是一個(gè)普通的函數(shù),可以正常調(diào)用。例如:
//作為普通函數(shù)在全局作用域下調(diào)用
Person("Greg", 27, "Doctor"); //adds to window
window.sayName(); //“Greg"
//作為普通函數(shù)在另一個(gè)對(duì)象中調(diào)用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
- 檢測(cè)類型
alert(person1 instanceof Object); //true
alert(person1 instanceof Person);//true
綜上,創(chuàng)建自定義的構(gòu)造函數(shù),意味著將來(lái)可以將它的實(shí)例標(biāo)識(shí)為一種特定的類型(類似于Array類型,Number類型);而這正是構(gòu)造函數(shù)模式勝過(guò)工廠模式的地方。但是構(gòu)造函數(shù)模式也存在缺點(diǎn)。
- 構(gòu)造函數(shù)模式的問(wèn)題
使用構(gòu)造函數(shù)的主要問(wèn)題就是,每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍(實(shí)例化一次Function對(duì)象),浪費(fèi)內(nèi)存。例如,person1和person2都有一個(gè)sayName()的方法,但創(chuàng)建person1和person2時(shí)候,定義sayName這個(gè)方法時(shí)都實(shí)例化了一個(gè)函數(shù)對(duì)象,因此person1.sayName和person2.sayName是不相等的,而事實(shí)上它們又是做的同樣的事情。或者也可以這么說(shuō),person1和person2的sayName()方法做同樣的事情,但卻在創(chuàng)建對(duì)象時(shí)被實(shí)例化了兩次,也就占用了兩倍內(nèi)存。
雖然可以解決,但并不完美,例如:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor”);
alert(person1.sayName == person2.sayName); //true
但是如果共享方法有很多,就需要定義很多個(gè)全局函數(shù),那么我們的自定義的引用類型就絲毫沒(méi)有封裝性可言了。好在,這些問(wèn)題可以通過(guò)使用原型模式解決。
3、原型模式
(1)理解原型對(duì)象
無(wú)論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性就是該函數(shù)的原型對(duì)象。每個(gè)函數(shù)都有一個(gè)原型對(duì)象,所有原型對(duì)象都會(huì)自動(dòng)獲得constructor屬性,constructor指向該函數(shù)(擁有該prototype屬性的函數(shù))。
例如,Person.prototype.constructor指向Person。
創(chuàng)建構(gòu)造函數(shù)后,其原型對(duì)象默認(rèn)只會(huì)取得constructor屬性;至于其他的方法都是從Object繼承來(lái)的(__proto__)。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例內(nèi)部將包含一個(gè)指針(__proto__),指向構(gòu)造函數(shù)的原型對(duì)象。(ECMA-262第5版中管這個(gè)指針叫[[Prototype]],但在腳本中沒(méi)有標(biāo)準(zhǔn)的方式訪問(wèn)它。在chrome,safari和firefox中都支持一個(gè)屬性__proto__,但在其他實(shí)現(xiàn)中__proto__對(duì)腳本是不可見(jiàn)的)。所以和實(shí)例有直接關(guān)系的是構(gòu)造函數(shù)的原型對(duì)象,而不是構(gòu)造函數(shù)。

上圖展示了Person構(gòu)造函數(shù)、Person的原型對(duì)象和Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。
(2)實(shí)例屬性和原型屬性:
原型屬性即構(gòu)造函數(shù)的原型對(duì)象的屬性;實(shí)例屬性即在實(shí)例對(duì)象上直接添加的屬性。
例如:person1.name=“Jone”。
通過(guò)點(diǎn)運(yùn)算符可以訪問(wèn)到實(shí)例的實(shí)例屬性和原型屬性。實(shí)例訪問(wèn)屬性時(shí),腳本會(huì)先搜索實(shí)例屬性,如果找到了,則停止搜索返回實(shí)例屬性的值;如果沒(méi)找到就繼續(xù)搜索原型屬性。所以如果實(shí)例屬性和原型屬性同名,那么原型屬性就會(huì)被屏蔽掉,無(wú)法訪問(wèn)到。
需要注意的是:實(shí)例無(wú)法修改他的原型屬性的值,也無(wú)法修改原型對(duì)象(即不能修改、刪除和增加一個(gè)原型屬性)
(注意:實(shí)例不能修改的是原型屬性的值,但是如果原型屬性指向一個(gè)引用類型,原型屬性的值是存儲(chǔ)這個(gè)引用類型的地址,即不能修改原型屬性指向另一個(gè)對(duì)象,但卻能修改原型屬性指向的對(duì)象里的屬性。下面原型對(duì)象的問(wèn)題里還會(huì)再講到)。
如果person1.name=“Jone”這樣寫(xiě),腳本只會(huì)在實(shí)例屬性里創(chuàng)建或修改一個(gè)name=“Jone”的屬性,delete person1.name只會(huì)刪除person1的實(shí)例屬性name(就算實(shí)例沒(méi)有name的實(shí)例屬性,也不會(huì)刪除實(shí)例的原型屬性)。
(3)和原型對(duì)象有關(guān)的幾個(gè)方法
isPrototypeOf()
alert(Person.prototype.isPrototypeOf(person1)); //true
如果person1的[[prototype]](即__proto__)指向調(diào)用isPrototypeOf的對(duì)象即Person.prototype就會(huì)返回true。
即判斷Person.prototype是否是person1的[[prototype]]Object.getPrototypeOf()
alert(Object.getPrototypeOf(person1)==Person.prototype); //true
返回person1這個(gè)對(duì)象的原型[[prototype]]hasOwnProperty()
person1.hasOwnProperty(“name”);如果person1.name是來(lái)自于person1的實(shí)例屬性,返回true;如果來(lái)自于person1的原型屬性,則返回false。
(4)原型與in操作符
有兩種方式使用in操作符:
單獨(dú)使用in:alert(“name” in person1); //true
在通過(guò)person1能夠訪問(wèn)給定屬性是返回true,無(wú)論屬性是實(shí)例屬性還是原型屬性。
在for-in循環(huán)中使用:返回的是所有能夠通過(guò)對(duì)象訪問(wèn)的、可枚舉的屬性,其中包括實(shí)例屬性也包括原型屬性。
- Object.keys()
接受一個(gè)對(duì)象作為參數(shù),返回一個(gè)包含對(duì)象的所有可枚舉屬性的字符串?dāng)?shù)組。
如果對(duì)象是一個(gè)實(shí)例,則只返回實(shí)例的實(shí)例屬性而不包含原型屬性 - Object.getOwnPropertyNames()
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName”
得到對(duì)象的所有實(shí)例屬性,無(wú)論它是否可枚舉
(5)更簡(jiǎn)單的原型語(yǔ)法
所謂的更簡(jiǎn)單的原型寫(xiě)法就是用字面量的形式來(lái)定義構(gòu)造函數(shù)的原型對(duì)象,如下:
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
這樣定義完了之后,Person.prototype這個(gè)對(duì)象就被重寫(xiě)了,導(dǎo)致它的constructor這個(gè)屬性的指向變成了Object,而不是Person
(解釋:Person.prototype是Object的一個(gè)實(shí)例,所以它有一個(gè)原型屬性constructor指向Object。Person被創(chuàng)建時(shí),它的原型對(duì)象Person.prototype自動(dòng)獲得了一個(gè)constructor的屬性,指向Person,這個(gè)屬性是對(duì)象的實(shí)例的實(shí)例屬性,所以會(huì)屏蔽掉對(duì)象的原型屬性,所以說(shuō)Person.prototype.constructor是指向Person的。但是用字面量重寫(xiě)了Person.prototype后,Person.prototype仍是Object的一個(gè)實(shí)例,所以它有一個(gè)原型屬性constructor指向Object,但它沒(méi)有了指向Person的實(shí)例屬性constructor,所以在訪問(wèn)Person.prototype.constructor時(shí),就是訪問(wèn)了Person.prototype對(duì)象的原型屬性,指向了Object)。
但我們可以再把它定義進(jìn)這個(gè)對(duì)象字面量里手動(dòng)指向Person,即給Person.prototype這個(gè)對(duì)象的實(shí)例加一個(gè)實(shí)例屬性constructor,指向Person。如下:
function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
我們知道如此定義對(duì)象,對(duì)象的屬性的[[enumerable]]特性默認(rèn)是true。而默認(rèn)情況下,原聲的原型對(duì)象的constructor屬性是不可枚舉的,因此如果你使用兼容ES5的javaScript引擎,可以使用Object.defineProperty()來(lái)設(shè)置constructor屬性。如下:
//重設(shè)構(gòu)造函數(shù),只適用于ES5兼容的瀏覽器
Object.difineProperty(Person.prototype,”constructor”,{
enumerable:false,
value:Person
});
(6)原型的動(dòng)態(tài)性
簡(jiǎn)單點(diǎn)來(lái)說(shuō),就是實(shí)例的[[prototype]]是指向構(gòu)造函數(shù)的原型對(duì)象,而不是構(gòu)造函數(shù)。只要你明白這一點(diǎn),原型的動(dòng)態(tài)性就好理解了。
第一種情況:Person.prototype可以在任意地方增加修改或刪除屬性,實(shí)例可以實(shí)時(shí)的訪問(wèn)最新的原型屬性。因?yàn)槊看螌?shí)例訪問(wèn)屬性,都是一次搜索的過(guò)程,搜索原型屬性時(shí)是到實(shí)例的[[prototype]]指向的對(duì)象里查找。實(shí)例的[[prototype]]是一個(gè)指針,Person.prototype也是一個(gè)指針,指向的是同一個(gè)地址,也就是說(shuō)修改和查找都在同一個(gè)地方,那么查找到的值自然就是最新實(shí)時(shí)的了。
function Person(){
}
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"
第二種情況:在實(shí)例被創(chuàng)建之后,Person.prototype被重寫(xiě)了
function Person(){
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
這種情況是因?yàn)椋簩?shí)例一旦被創(chuàng)建,實(shí)例的[[prototype]]存儲(chǔ)的地址就確定了,指向的對(duì)象地址就確定了,如果你改變這個(gè)地址里的對(duì)象,實(shí)例都可以訪問(wèn)的到。但是如果在實(shí)例被創(chuàng)建之后,重寫(xiě)Person.prototype,就相當(dāng)于是把Person.prototype指向了一個(gè)新的對(duì)象,而實(shí)例的[[prototype]]還是指向原來(lái)的對(duì)象,所以實(shí)例訪問(wèn)的原型屬性還是要在原來(lái)的對(duì)象里查找,原來(lái)的對(duì)象里并沒(méi)有sayName這個(gè)方法,因此會(huì)報(bào)錯(cuò)。
(7)原生對(duì)象的原型
我們用原型模式創(chuàng)建自定義類型,讓自定義類型和原生類型一樣使用。其實(shí)所有的原生的對(duì)象(Object、Array、String,等等)也是采用的原型模式創(chuàng)建的。所有原生的引用類型都在其構(gòu)造函數(shù)的原型上定義了方法。
例如,在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法。
通過(guò)原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,也可以定義新的方法??梢韵裥薷淖远x對(duì)象的原型一樣修改原生對(duì)象的原型,因此可以隨時(shí)添加方法。但是不建議如此做(在支持該方法的實(shí)現(xiàn)中運(yùn)行代碼時(shí)會(huì)導(dǎo)致命名沖突,或者意外重寫(xiě)了原生方法)。
(7)原型對(duì)象的問(wèn)題
首先,原型模式省略了為構(gòu)造函數(shù)傳遞參數(shù),初始化實(shí)例的環(huán)節(jié),使得所有實(shí)例默認(rèn)時(shí)都是一樣的。
其次,原型模式的共享本性使得所有的實(shí)例都能共享它的屬性。
如果屬性值是函數(shù)或者是基本值時(shí),實(shí)例不能修改原型屬性的值,只會(huì)為該實(shí)例增加一個(gè)同名屬性,然后屏蔽掉同名原型屬性,這樣其它的實(shí)例都不會(huì)受到影響,使用的仍然是原型屬性原來(lái)的值。
如果屬性值是引用類型,實(shí)例雖不能修改原型屬性的值(這個(gè)值就是指向的對(duì)象的地址),即實(shí)例不能讓這個(gè)原型屬性重新指向另一個(gè)對(duì)象,但是卻可以修改指向的對(duì)象的屬性,這就會(huì)導(dǎo)致其它實(shí)例再訪問(wèn)這個(gè)對(duì)象時(shí),對(duì)象已被修改了。
例如:
function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
sayName : function () {
alert(this.name);
}
};
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
這樣就違反了我們希望實(shí)例擁有屬于自己的全部屬性的初衷
4、組合使用構(gòu)造函數(shù)模式和原型模式
綜合前面所說(shuō)的,我們發(fā)現(xiàn)構(gòu)造函數(shù)模式優(yōu)點(diǎn)在于能向構(gòu)造函數(shù)傳遞,定義屬于實(shí)例自己的實(shí)例屬性。原型模式優(yōu)點(diǎn)在于共享著對(duì)方法的引用,原型屬性是所有實(shí)例所共享的。
所以創(chuàng)建自定義類型的最常見(jiàn)方式,就是組合使用構(gòu)造函數(shù)模式與原型模式
例如:
function Person(name, age, job){
this.name = name;
this.age = 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,Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
5、動(dòng)態(tài)原型模式
這一小節(jié),私以為了解了解就好,只要你理解了上面所說(shuō)的構(gòu)造函數(shù)模式和原型模式的原理,那么原型屬性的定義你可以隨心所欲,只要符合你的預(yù)期就好。你高興就好,代碼高興就好。
6、寄生構(gòu)造函數(shù)模式
與工廠模式的區(qū)別是使用new 調(diào)用。不使用new調(diào)用,它就是工廠模式。
這一小節(jié),私以為了解了解就好。
7、穩(wěn)妥構(gòu)造函數(shù)模式
與工廠模式的區(qū)別是對(duì)象定義的方法不使用this,構(gòu)造函數(shù)傳進(jìn)來(lái)的參數(shù)不向外直接暴露。
這一小節(jié),私以為了解了解就好。
好了,封裝類的幾種方式已經(jīng)介紹完了。我的觀點(diǎn)是理解了對(duì)象和構(gòu)造函數(shù)模式以及原型模式,就可以隨機(jī)應(yīng)變了。不需要記住什么什么各種模式的,無(wú)非就是使用對(duì)象的場(chǎng)景不同。要理解對(duì)象和構(gòu)造函數(shù)以及原型對(duì)象,靈活變換,無(wú)招勝有招才好。
這是我讀《javaScript高級(jí)程序設(shè)計(jì)》這本書(shū)的第6章面向?qū)ο蟮某绦蛟O(shè)計(jì),做的筆記,在本篇之前還有一篇理解對(duì)象的筆記,后面還有一篇繼承的筆記。發(fā)現(xiàn)問(wèn)題的小伙伴歡迎指出。