(1)、工廠模式:封裝一個函數createPerson,這個函數可以創造一個人對象,包含三個屬性和一個方法,然後利用這個函數分別創建了2個對象p1,p2. 工廠模式下解決了創建多個相似對象的問題,但是卻沒有解決對象識別問題(不知道這個對象的類型是數組或函數或正則等)alert(p1 instance
(1)、工廠模式:
封裝一個函數createPerson,這個函數可以創造一個人對象,包含三個屬性和一個方法,然後利用這個函數分別創建了2個對象p1,p2.
function createPerson(name,age,job){ var p=new Object(); p.name=name; p.age=age; p.job=job; p.showName=function(){ alert(this.name); }; return p; } var p1=createPerson('jxj1',24,'student'); var p2=createPerson('jxj2',25,'teacher');
工廠模式下解決了創建多個相似對象的問題,但是卻沒有解決對象識別問題(不知道這個對象的類型是數組或函數或正則等)
alert(p1 instanceof Object); //true
alert(p1 instanceof createPerson); //false
alert(p2 instanceof Object); //true
alert(p2 instanceof createPerson); //false
(2)、構造函數模式
創建一個構造函數,習慣上構造函數的首字母大寫,非構造函數第一個字母小寫,同樣也包含三個屬性和一個方法,然後利用這個函數分別創建了2個實例對象p1,p2.(構造函數也可以當作普通函數來使用,只有使用了new來調用,才作為構造函數使用)
我們先用構造函數從寫上面工廠模式下的函數
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.showName=fucntion(){ alert(this.name); }; }; var p1=new Person('jxj1',24,'student'); var p2=new Person('jxj2',25,'teacher');
這個構造函數的例子取代了前面的普通函數,二者之間到底有什麼區別呢
相對工廠模式構造函數
1、沒有顯式的創建對象,就是沒有在函數裡面var p=new Object();
2、直接將屬性和方法直接的付給了this對象(理解this的朋友知道,這樣的構造函數在沒有new新對象時,this指向全局對象window);
3、沒有return語句
再次解釋構造函數中的this,構造函數的作用就是為了創建對象,這裡我們有new 了二個新的對象 p1、p2,此時再調用p1和p2時this就指向了自己,而不是window(不懂得去查看this的作用域)
p1和p2是Person的不同實例,這二個對象都有一個屬性constructor(構造函數的屬性),該屬性指向Person這個構造函數
alert(p1.constructor==Person); //true
alert(p2.constructor==Person); //true
好了,之所以介紹構造函數,還沒有說它的優點呢,前面說了工廠方式不能夠解決對象的識別問題,那麼構造函數就可以識別
alert(p1 instanceof Object); //true
alert(p1 instanceof Person); //true
alert(p2 instanceof Object); //true
alert(p2 instanceof Person); //true
其實說了這麼多,構造函數還是不完美的,有缺點,有沒有註意到構造函數中有個showName的方法,該方法有個function函數,問題就是出現在這裡
p1,p2是Person的二個不同的實例,p1和p2中的showName方法是不一樣的(不同實例的同名函數不相等)
alert(p1.showName == p2.showName); //false
換句話說,每次的實例化的構造函數都是下麵這樣的(只是為了理解,不可以這樣寫)
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.showName=new fucntion(){ alert(this.name); }; };
總之,每次實例化時,都產生一個新方法,這就是缺點
好吧,想辦法解決:
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.showName=showName; }; fucntion showName(){ alert(this.name); }; var p1=new creatPerson('jxj1',24,'student'); var p2=new creatPerson('jxj2',25,'teacher');
在這個例子中,我們把showName()函數的定義轉移到了構造函數外部。而在構造函數內部,我們將sayName 屬性設置成等於全局的sayName 函數。這樣一來,由於sayName 包含的是一個指向函數
的指針,因此p1 和p2 對象就共用了在全局作用域中定義的同一個sayName()函數。這樣做確實解決了兩個函數做同一件事的問題,可是showName就成了全局函數,如果該構造函數有很多方法,呢麽會不會瘋啊,那麼這樣的解決辦法可以解決,但是不好,好吧繼續找方法
(3)、原型
我們創建的每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象。。。。接下來先不解釋含義了,因為不容易理解,舉個例子:
function Person(name,age,job){ this.prototype.name='jxj1'; this.prototype.age=24; this.prototype.job='student'; this.prototype.showName=fucntion(){ alert(this.name); }; }; var p1=new Person(); p1.showName();//jxj1 var p2=new Person(); p2.showName();//jxj1 alert(p1.showName==p2.showName);//true
通過上面的代碼,有沒有覺得,工廠模式和構造函數模式的缺點,都不會在這裡出現啊,這就是原型的強大之處,好吧,接下來我們來理解原型到底是什麼
直接說概念太抽象,對著圖說吧
我們用原型寫的構造函數,將所有屬性和方法都添加到Person的prototype屬性中,此時的構造函數變成了空函數。即使如此,也仍然可以通過調用構造函數來創建新對象,而且新對象還會具有相同的屬
性和方法。但與構造函數模式不同的是,新對象的這些屬性和方法是由(讓)所有實例共用的。換句話說,p1 和p2 訪問的都是同一組屬性和同一個sayName()函數。
看著圖。。。。。
只要創建一個新函數(Person),就會為這個函數創建一個prototype的屬性,這個屬性指向行原型對象(Person protype),而原型對象會獲得一個屬性constructor,該屬性指向原型屬性所在的函數指針Person。當調用構造函數創建新實例以後(person1,person2),該實例內部將包含一個指針[[prototype]],指向構造函數的原型對象.可以看出來,person1和person2與構造函數沒有直接的關係,它們操作的是構造函數的對象原型(Person protype)。請記住:實例中的指針僅指向原型,而不指向構造函數。
看看下麵的二句:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
這2句表明瞭,person1和person2內部都有一個指針[[prototype]]指向原型對象(Person.prototype)。
在ECMAScript5中新增加了一個方法Object.getPrototypeof(),該方法可以返回實例對象(person1,person2)中的指針值
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
下麵是是書上對原型中屬性和方法訪問的過程還是很容易理解的,所以我就copy了:
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性。搜索首先從對象實例本身開始。如果在實例中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性。如果在原型對象中找到了這個屬性,則返回該屬性的值。
也就是說,在我們調用person1.sayName()的時候,會先後執行兩次搜索。首先,解析器會問:“實例person1 有sayName 屬性嗎?”答:“沒有。”然後,它繼續搜索,再問:“person1 的原型有sayName 屬性嗎?”答:“有。”於是,它就讀取那個保存在原型對象中的函數。當我們調用person2.sayName()時,將會重現相同的搜索過程,得到相同的結果。而這正是多個對象實例共用原型所保存的屬性和方法的基本原理。
看了上面的原理,下麵我們舉個例子:
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(); var person2 = new Person(); person1.name = "Greg"; alert(person1.name); //"Greg"——來自實例 alert(person2.name); //"Nicholas"——來自原型
該例子可以看出,當代碼在讀取對象屬性時,先查找本身自己對象實例person1,當實例中找到了name,就不會再找對象原型中的相應屬性了;若是沒有找到,就繼續搜索原型對象。。。。
換句話說,當實例中存在和原型對象中同名的屬性,那麼會優先選擇實例中的屬性和屬性值。。。。
這裡要想person1.name顯示Nicholas,就需要使用delete.person1.name刪除實例中的同名屬性才可以。。。。。
接下來問題來了,有時候我們需要檢測一個屬性到底是在實例中還是在原型對象中,我們該怎麼辦
hasOwnProperty()是從Object繼承來的,hasOwnProperty()方法可以檢測到一個屬性是存在於原型中還是對象實例中,只有當存在對象對象實例中返回true(其實存在原型中,或是其它非對象實例中都會返回false)
in :當通過對象能夠訪問指定的屬性時就返回true(只要能訪問,不管存在於實例中還是原型中)
可以封裝一個函數來確定,屬性到底是存在於對象中還是原型中:
function hasPrototypePropery(obj,name){
return !obj.hasOwnProperty(name)&&(name in obj);
};
有些人肯定會疑惑,hasprototypepropery()方法就足夠了啊,幹麼還要in呢?我一開始也是這麼疑惑,逆向思維就知道了。。。
假設現在我要確定一個屬性是存在於原型對象中的,而hasPrototypePropery()只能確定存在對象實例中和非對象實例中,所以只有當在非實例對象中 !obj.hasOwnProperty(name)且能夠通過對象訪問到該屬性時,才能確定一定是在對象原型中,,,,,(不懂得慢慢想)
前面我們用原型的方法解決了工廠和構造函數的缺點,但是,原型寫的有點負責代碼又多,有那麼多重覆的對象.prototype所以我們要改寫一下了
function Person(){ }; Person.prototype(){ name:'jxj1', age:24, job:'student', showName:function(){ alert(this.name); } };
這種通過對象字面量的方式來寫原型對象和原來的寫法產生的效果是一樣的,但是有一個變了,前面說過每創建一個函數,這個函數就會產生一個prototype屬性,指向對象原型,而對象原型中
的constructor屬性會指向創建的那個函數,現在constructor屬性不再指向Person了,改編成指向Object構造函數
如果你需要constructor屬性很重要,可以改寫一下:
function Person(){ }; Person.prototype(){ constructor:Person, name:'jxj1', age:24, job:'student', showName:function(){ alert(this.name); } };
原生模式的重要性並僅僅體現在上面的哪些自定義類型方面,就連原生的引用類都是採用這種模式創建的。原生引用類型(Object、Array、String等)都在其構造函數的原型上定義了方法
例如,在Array.prototype 中可以找到sort()方法,而在String.prototype 中可以找到substring()方法,如下所示。
alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"
同樣的我們也可以修改原生對象的原型
下麵的代碼就給基本包裝類型
String 添加了一個名為startsWith()的方法。
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
這裡新定義的startsWith()方法會在傳入的文本位於一個字元串開始時返回true。既然方法被
添加給了String.prototype,那麼當前環境中的所有字元串就都可以調用它。由於msg 是字元串,
而且後臺會調用String 基本包裝函數創建這個字元串,因此通過msg 就可以調用startsWith()方法。
當然,非特殊情況下,不推薦在程式中修改原生對象的原型,修改的方法可以會在其它方法中產生命名衝突
原型的缺點:
對比你下原型和構造函數發現了,原型模式省略了初始化參數這一環節,結果導致所有的實例都共用取得相同的屬性值,其實我們並不像看到這樣的結果,這還不是最大的問題。
原生最大的問題就是由共用的本性所導致的,原型中所有屬性被實例共用,這沒有什麼不妥,況且,實例中同名的屬性優先順序更高。然而,對於包含引用類型值的屬性來說,問題就大了。
例如:
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
當實例person1對象修改的屬性中包含引用類型(比如數組)的值,會反映到person2對象中,我既然創建的是二個實例,怎麼會想她們共用一個數組呢,oh 。。。ON
現在問題又來了,原型也有缺點,繼續想辦法吧:
(4)、構造函數與原型的混合模式
我們需要將二者的優點都結合起來,拋棄她們的缺點,分析一下吧
首先,構造函數中的方法每次實例化都會是不一樣的,但是原型可以改正這個缺點,所以用原型模式來定義方法
其次,當其中一個實例改變原型中引用類型的值,同時另外一個實例在原型中的相應值也會跟著改變,但是構造函數可以改掉這個缺點,所以,用構造函數模式定義引用類型值的屬性
總結,構造函數用於定義實例的屬性(包括基本數據類型和引用數據類型),而原型模式用於定義方法和共用的屬性。
例子:這個例子也就是前面我們舉的例子
function Person(name,age,job){ this.name=name; this.age=age; this.job=job; this.friend=['a1','a2']; }; Person.prototype={ constructor:Person, showName:function(){ alert(this.name); } }; var p1=new Person('jxj1',24,'student'); var p2=new Person('jxj2',25,'student'); p1.friend.push('a3'); alert(p1.friend);//'a1,a2,a3' alert(p2.friend);//'a1,a2' alert(p1.friend==p2.friend);//false alert(p1.showName==p2.showName);//true
從例子中可以看出來,現在引用類型的數據值沒有共用,函數方法變成了共用,所以好像是沒有問題了。。。。。
(5)、動態原型模式
動態原型模式,它把所有信息都封裝在了構造函數中,而通過在構造函數中初始化原型(僅在必要的情況下),又保持了同時使用構造函數和原型的優點。換句話說,可以通過
檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。來看一個例子:
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()方法不存在的情況下,才會將它添加到原型中。這段代碼只會在初次調用構造函數時才會執行。此後,原型已經完成初始化,不需要再做什麼修改了。
不過要記住,這裡對原型所做的修改,能夠立即在所有實例中得到反映。因此,這種方法確實可以說非常完美。其中,if 語句檢查的可以是初始化之後應該存在的任何屬性或方法——不必用一大堆
if 語句檢查每個屬性和每個方法;只要檢查其中一個即可。對於採用這種模式創建的對象,還可以使用instanceof 操作符確定它的類型。
使用動態原型模式時,不能使用對象字面量重寫原型。如果在已經創建了實例的情況下重寫原型,那麼就會切斷現有實例與新原型之間的聯繫。