JavaScript中的prototype原型學(xué)習(xí)指南
原型是什么
Function 類型有一個屬性 prototype,直接翻譯過來就是原型。這個屬性就是一個指針,指向一個對象,這個對象包含一些屬性和方法,這些屬性和方法會被當(dāng)前函數(shù)生成的所有實(shí)例(對象)所共享。
這句話根據(jù)前面所說的,細(xì)細(xì)琢磨下來,就可以得到下面代碼:
function Person(){ ... } Person.prototype = { country : 'china', sayName : function(){ ... } }
先創(chuàng)建了一個 Function 類型的實(shí)例 person,然后 person 的方法 prototype 是一個對象,就聲明指向了一個對象。這個對象里面的屬性和方法,會被當(dāng)前 person 函數(shù)生成的實(shí)例所共享。也就是說:
person1 = new Person(); person2 = new Person();
person1 和 person2 都是通過 Person 這個 Function 類型實(shí)例,再次生成的實(shí)例,它們倆都有共同的屬性 country 和方法 sayName,因?yàn)樗鼈兌加心硞€指針(__proto__),直接指向 Person.prototype 所指向的對象。不過要注意 __proto__ 這個指針是不標(biāo)準(zhǔn)的,只有 Chrome 和 Firefox 等瀏覽器自己定義的,實(shí)際中,也不會用到這個屬性,只是作為理解 prototype 來用:
關(guān)于原型等用法,后面會更具體的講到。
創(chuàng)建對象的模式
下面,我們就來看下創(chuàng)建對象的方法和常用模式,以及它們之間的優(yōu)缺點(diǎn)。
1.工廠模式
就像工廠一樣,抽象了創(chuàng)建具體對象的過程,用函數(shù)來封裝以特定接口創(chuàng)建對象的細(xì)節(jié)。通過使用函數(shù)代替部分重復(fù)工作,代碼如下:
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("jiangshui","22","engineer");
這樣就創(chuàng)建出來了一個人,工廠模式解決了多個相似對象重復(fù)創(chuàng)建問題,但是沒有解決對象識別問題。只是單純的創(chuàng)建了一個對象,而不管這個對象是從人類模版還是動物模版創(chuàng)建的,無法區(qū)分這個對象的類型。
2.構(gòu)造函數(shù)模式
創(chuàng)建一個自定義的構(gòu)造函數(shù),從而定義自定義對象類型的屬性和方法。
function Person(name, age, job){ this.name = name; this.age = age; this.job = jpb; this.sayName = function(){ alert(this.name); }; }; var person1 = new Person(...);
3.構(gòu)造函數(shù)模式與工廠模式區(qū)別:
- 沒有顯式的創(chuàng)建對象。
- 直接將屬性和方法賦值 this 對象。
- 沒有 return 語句。
Person 是 Function 類型的對象,new 之后,會繼續(xù)產(chǎn)生一個對象,但這個新產(chǎn)生的對象,由于在函數(shù)中傳遞進(jìn)去參數(shù),并賦值給了 this 指針,那么傳遞進(jìn)去的內(nèi)容,就變成了新產(chǎn)生對象的屬性或方法。
構(gòu)造函數(shù)默認(rèn)習(xí)慣是首字母大寫,上面代碼執(zhí)行經(jīng)歷了下面幾個步驟:
- 創(chuàng)建一個新對象
- 將構(gòu)造函數(shù)作用域賦值給新對象
- 執(zhí)行構(gòu)造函數(shù)中的代碼
- 返回新對象
這樣生成的實(shí)例中,都默認(rèn)包含一個 constructor 屬性指向構(gòu)造函數(shù),例如:
alert(person1.constructor == Person);
所以用構(gòu)造函數(shù)模式,有類型的區(qū)分,可以將它的實(shí)例標(biāo)識為一種特定的類型。
此外,構(gòu)造函數(shù)就是普通的函數(shù),因?yàn)橐答伒玫叫聦ο螅杂?new 來調(diào)用。如果不用的話,直接執(zhí)行就跟普通函數(shù)一樣,例如上面,執(zhí)行 Person.sayName() 會彈出 window.name,因?yàn)楹瘮?shù)在 window 下面執(zhí)行,所以 this 指向 window。
構(gòu)造函數(shù)模式也是有缺陷的,構(gòu)造函數(shù)模式里面的方法,在每個實(shí)例上都重新創(chuàng)建了一遍,因此不同實(shí)例上的同名函數(shù)是不相等的。例如:
person1.sayName == person2.sayName; //false
也就是說,由構(gòu)造函數(shù)生成的每個對象實(shí)例,屬性和方法都是獨(dú)有的,都是復(fù)制了一遍。屬性獨(dú)有是必須的,因?yàn)檫@正是對象之間不同的地方,但是很多方法功能和代碼都是一樣的,重復(fù)復(fù)制多次,顯然就會浪費(fèi)資源。
所以我們可以把函數(shù)放在外面,然后在構(gòu)造函數(shù)里面,用指針指向這個函數(shù),那么生成的實(shí)例中,方法存儲的就是一個指向某函數(shù)的指針,也就共用一個函數(shù)了:
function Person(name, age){ this.name = name; this.age = age; this.sayName = sayName; } function sayName(){ alert(this.name); }
但是這樣,這個函數(shù)就變成了全局函數(shù),而且與 Person 構(gòu)造函數(shù)關(guān)聯(lián)性不強(qiáng),沒有封裝性可言。
下面有請原型模式登場。
原型模式
前面已經(jīng)介紹了一部分關(guān)于原型的基礎(chǔ)知識。簡單的說,就是每個函數(shù)都有一個 prototype 屬性,指向一個對象(原型對象),這個對象里面可以放一些屬性或者方法。然后這個函數(shù)生成的實(shí)例,會有一個不規(guī)范的屬性(__proto__)指向原型。
由此來看,你應(yīng)該可以理解:prototype 產(chǎn)生的屬性和方法是所有實(shí)例共享的。
這樣正好解決了上面構(gòu)造函數(shù)模式中,實(shí)例中函數(shù)的共用問題。例如下面代碼:
function Person(){ .... } Person.prototype.name = "jiangshui"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); //jiangshui
或者
Person.prototype = { constructor : Person, name : "jiangshui", sayName : function(){ alert(this.name); } };
第二種方法覆蓋了整個 prototype 對象,所以需要手動指定 constructor 屬性,指向構(gòu)造函數(shù)否則會指向 Object。
梳理一下它們的關(guān)系:
- Person.prototype -》 原型對象,可以定義一些屬性或者參數(shù),被所有實(shí)例共用。
- Person.prototype.constructor == Person —》 原型對象有個默認(rèn)的屬性 constructor 指向該原型對象所屬的構(gòu)造函數(shù)(注意另一種寫法會覆蓋掉這個屬性,需要重新指定)。
- person1 = new Person() -》構(gòu)造函數(shù)生成實(shí)例,實(shí)例包含了構(gòu)造函數(shù)的內(nèi)容和原型對象的內(nèi)容。
- person1.__proto__ -》指向創(chuàng)建這個實(shí)例的原型對象(不規(guī)范,不要用)。
使用 isPrototypeOf() 可以確定對象之間的關(guān)系。例如:
Person.prototype.isPrototypeOf(person1);
當(dāng)代碼讀取某個對象的某個屬性,會執(zhí)行搜索。先從當(dāng)前對象開始,如果沒有,則搜索指針指向的原型對象,而不會搜索構(gòu)造函數(shù)。對象實(shí)例可以訪問但是不能重寫原型對象的值。如果實(shí)例中設(shè)置了與原型對象同名的屬性,則搜索過程,在實(shí)例中結(jié)束而不會訪問原型對象,所以達(dá)到覆蓋的目的。因此即使這個屬性設(shè)置為 null,也表示在實(shí)例中已經(jīng)存在該屬性,而不會取消掉這個屬性,從而可以訪問原型對應(yīng)屬性。
所以需要使用 delete 操作符,完全刪除實(shí)例屬性,從而可以重新訪問原型。
原型是動態(tài)的,對原型對象所做的任何修改,都能立即從實(shí)例上反映出來。原因是實(shí)例與原型之間的松散鏈接關(guān)系,每次調(diào)用實(shí)例的屬性方法,都會進(jìn)行一次查詢,如果原型變了,查詢結(jié)果也就變了。
了解原型之后,我們也可以對原生對象添加新方法或?qū)傩?。Object、Array、String 等原生引用類型,與上面構(gòu)造函數(shù)類似,我們可以用 prototype 擴(kuò)充它們的方法。例如:
String.prototype.startsWith = function(text){ return this.indexOf(text) == 0; }; var msg = "Hello World"; msg.startsWith("Hello");
這段代碼為 String 這個原生引用類型,增加了一個 startsWith 方法,功能就是傳遞進(jìn)去一個參數(shù),看看要測試的字符串是否以參數(shù)開始。由于原型的動態(tài)性,所以只要執(zhí)行一下,所有字符串類型的變量全都獲得了這個方法。
但是不推薦使用這個方法,如果用的太多,代碼太多,會導(dǎo)致維護(hù)困難、代碼混亂等情況。一般情況下,會先繼承某個原生引用類型,然后再在新自定義的類型上創(chuàng)建。關(guān)于繼承,后面會再總結(jié)。
原型模式也不是萬能的,原型中的所有屬性和方法是被所有實(shí)例共享的,所以對于函數(shù)之類非常合適,而對于包含引用類型的屬性來說,就會產(chǎn)生一些沖突。例如:
function Person(){} Person.prototype = { constructor : Person, friends : ["greg","jack"] }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("tom"); console.log(person2.friends);
你會在 console 中看到,person2 的 friends 多了一個 tom,這并不是我想要的,但是對 person1 定義他的朋友時,的確影響到了實(shí)例 person2。
所以我們要結(jié)合原型模式和構(gòu)造函數(shù)模式來使用。
組合使用構(gòu)造函數(shù)模式和原型模式
這就是最常用的模式,構(gòu)造函數(shù)用來定義實(shí)例屬性,通過傳遞參數(shù)實(shí)現(xiàn)自定義;原型用來定義方法或者需要所有實(shí)例共享的屬性。這樣,既實(shí)現(xiàn)了自定義,又保證了共用,還避免了問題。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["greg","jack"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } }; var jiangshui = new Person("jiangshui","22","engineer");
實(shí)際應(yīng)用示例
OK,到了這里,你可能會看懂原型是啥,以及如何創(chuàng)建對象,可是,這些又有什么用?確實(shí),我之前的工作,一直也就是用 jQuery 寫一些代碼就可以了,根本用不到封裝然后生成對象實(shí)現(xiàn)功能等。那這些究竟有什么用?
這種開發(fā)方式主要用于模塊化和組建化的開發(fā)。比如你常用的彈窗功能,你當(dāng)然可以把彈窗有關(guān)代碼,每次都粘貼復(fù)制,然后修改一下就可以用在項(xiàng)目里面了。更好的選擇是把你的彈窗功能代碼,抽象封裝成這樣的一個組件,這樣當(dāng)你需要用彈窗的時候,只需要傳遞參數(shù)生成一個彈窗實(shí)例,就可以調(diào)用了。
原型對象和原型鏈
在Javascript中,萬物皆對象,但對象也有區(qū)別,大致可以分為兩類,即:普通對象(Object)和函數(shù)對象(Function)。
一般而言,通過new Function產(chǎn)生的對象是函數(shù)對象,其他對象都是普通對象。
舉例說明:
function f1(){ //todo } var f2 = function(){ //todo }; var f3 = new Function('x','console.log(x)'); var o1 = {}; var o2 = new Object(); var o3 = new f1(); console.log( typeof f1,//function typeof f2,//function typeof f3,//function typeof o1,//object typeof o2,//object typeof o3 //object ); >> function function function object object object
f1屬于函數(shù)的聲明,最常見的函數(shù)定義方式,f2實(shí)際上是一個匿名函數(shù),把這個匿名函數(shù)賦值給了f2,屬于函數(shù)表達(dá)式,f3不常見,但也是一種函數(shù)對象。
Function是JS自帶的對象,f1,f2在創(chuàng)建的時候,JS會自動通過new Function()的方式來構(gòu)建這些對象,因此,這三個對象都是通過new Function()創(chuàng)建的。
在Javascript中創(chuàng)建對象有兩種方式:對象字面量和使用new表達(dá)式,o1和o2的創(chuàng)建恰好對應(yīng)了這兩種方式,重點(diǎn)講一下o3, 如果用Java和C#的思路來理解的話,o3是f1的實(shí)例對象,o3和f1是同一類型,至少我以前這么認(rèn)為,其實(shí)不然…
那么怎么理解呢? 很簡單,看o3是不是通過new Function產(chǎn)生的, 顯然不是,既然不是函數(shù)對象,那就是普通對象 。
通過對函數(shù)對象和普通對象的簡單理解之后,我們再來了解一下Javascript中的原型和原型鏈:
在JS中,每當(dāng)創(chuàng)建一個函數(shù)對象f1 時,該對象中都會內(nèi)置一些屬性,其中包括prototype和__proto__, prototype即原型對象,它記錄著f1的一些屬性和方法。
需要注意的是,prototype 對f1是不可見的,也就是說,f1不會查找prototype中的屬性和方法。
function f(){} f.prototype.foo = "abc"; console.log(f.foo); //undefined
那么,prototype有什么用呢? 其實(shí)prototype的主要作用就是繼承。 通俗一點(diǎn)講,prototype中定義的屬性和方法都是留給自己的“后代”用的,因此,子類完全可以訪問prototype中的屬性和方法。
想要知道f1是如何把prototype留給“后代”,我們需要了解一下JS中的原型鏈,此時,JS中的 __proto__ 入場了,這哥們長的很奇特,隱藏的也很深,以致于你經(jīng)常見不到它,但它在普通對象和函數(shù)對象中都存在, 它的作用就是保存父類的prototype對象,JS在通過new 表達(dá)式創(chuàng)建一個對象的時候,通常會把父類的prototype賦值給新對象的__proto__屬性,這樣,就形成了一代代傳承…
function f(){} f.prototype.foo = "abc"; var obj = new f(); console.log(obj.foo); //abc
現(xiàn)在我們知道,obj中__proto__保存的是f的prototype, 那么f的prototype中的__proto__中保存的是什么呢? 看下圖:
如圖所示,f.prototype的__proto__中保存的是Object.prototype,Object.prototype對象中也有__proto__,而從輸出結(jié)果看,Object.prototype.__proto__ 是null,表示obj對象原型鏈的終結(jié)。如下圖所示:
obj對象擁有這樣一個原型鏈以后,當(dāng)obj.foo執(zhí)行時,obj會先查找自身是否有該屬性,但不會查找自己的prototype,當(dāng)找不到foo時,obj就沿著原型鏈依次去查找…
在上面的例子中,我們在f的prototype上定義了foo屬性,這時obj就會在原型鏈上找到這個屬性并執(zhí)行。
相關(guān)文章
JavaScript數(shù)組的快速克隆(slice()函數(shù))和數(shù)組的排序、亂序和搜索(sort()函數(shù))
JavaScript數(shù)組的快速克隆(slice()函數(shù))和數(shù)組的排序、亂序和搜索(sort()函數(shù))...2006-12-12IE網(wǎng)頁js語法錯誤2行字符1、FF中正常的解決方法
使用模態(tài)窗體則會先彈出此錯誤然后再顯示新打開的界面,經(jīng)搜索找到不錯的解決方法,有類似問題的朋友可以參考下,希望對大家有所幫助2013-09-09關(guān)于不同頁面之間實(shí)現(xiàn)參數(shù)傳遞的幾種方式討論
下面小編就為大家?guī)硪黄P(guān)于不同頁面之間實(shí)現(xiàn)參數(shù)傳遞的幾種方式討論。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02