一文帶你徹底理解 JavaScript 原型對象

一、什么是原型

原型是Javascript中的繼承的基礎(chǔ),JavaScript的繼承就是基于原型的繼承。

1.1 函數(shù)的原型對象

在JavaScript中,我們創(chuàng)建一個函數(shù)A(就是聲明一個函數(shù)), 那么瀏覽器就會在內(nèi)存中創(chuàng)建一個對象B,而且每個函數(shù)都默認會有一個屬性 prototype 指向了這個對象( 即:prototype的屬性的值是這個對象 )。這個對象B就是函數(shù)A的原型對象,簡稱函數(shù)的原型。這個原型對象B 默認會有一個屬性 constructor 指向了這個函數(shù)A ( 意思就是說:constructor屬性的值是函數(shù)A )。

看下面的代碼:

<body><scripttype="text/javascript">? ? ? /*

? ? ? ? 聲明一個函數(shù),則這個函數(shù)默認會有一個屬性叫 prototype 。而且瀏覽器會自動按照一定的規(guī)則

? ? ? ? 創(chuàng)建一個對象,這個對象就是這個函數(shù)的原型對象,prototype屬性指向這個原型對象。這個原型對象

? ? ? ? 有一個屬性叫constructor 指向了這個函數(shù)


? ? ? 注意:原型對象默認只有屬性:constructor。其他都是從Object繼承而來,暫且不用考慮。

? ? */functionPerson(){? ? ? ? ? ? ? }</script></body>

下面的圖描述了聲明一個函數(shù)之后發(fā)生的事情:


1.2 使用構(gòu)造函數(shù)創(chuàng)建對象

當(dāng)把一個函數(shù)作為構(gòu)造函數(shù) (理論上任何函數(shù)都可以作為構(gòu)造函數(shù)) 使用new創(chuàng)建對象的時候,那么這個對象就會存在一個默認的不可見的屬性,來指向了構(gòu)造函數(shù)的原型對象。這個不可見的屬性我們一般用 [[prototype]] 來表示,只是這個屬性沒有辦法直接訪問到。

<body><scripttype="text/javascript">? ? ? functionPerson(){? ? ? ? ? ? ? }/*

? ? ? ? ? 利用構(gòu)造函數(shù)創(chuàng)建一個對象,則這個對象會自動添加一個不可見的屬性 [[prototype]], 而且這個屬性

? ? ? ? ? 指向了構(gòu)造函數(shù)的原型對象。

? ? ? ? */varp1 =newPerson();</script></body>

觀察下面的示意圖:


說明:

1.從上面的圖示中可以看到,創(chuàng)建p1對象雖然使用的是Person構(gòu)造函數(shù),但是對象創(chuàng)建出來之后,這個p1對象其實已經(jīng)與Person構(gòu)造函數(shù)沒有任何關(guān)系了,p1對象的[[ prototype ]]屬性指向的是Person構(gòu)造函數(shù)的原型對象。

2.如果使用new Person()創(chuàng)建多個對象,則多個對象都會同時指向Person構(gòu)造函數(shù)的原型對象。

3.我們可以手動給這個原型對象添加屬性和方法,那么p1,p2,p3…這些對象就會共享這些在原型中添加的屬性和方法。

4.如果我們訪問p1中的一個屬性name,如果在p1對象中找到,則直接返回。如果p1對象中沒有找到,則直接去p1對象的[[prototype]]屬性指向的原型對象中查找,如果查找到則返回。(如果原型中也沒有找到,則繼續(xù)向上找原型的原型—原型鏈。后面再講)。

5.如果通過p1對象添加了一個屬性name,則p1對象來說就屏蔽了原型中的屬性name。換句話說:在p1中就沒有辦法訪問到原型的屬性name了。

6.通過p1對象只能讀取原型中的屬性name的值,而不能修改原型中的屬性name的值。p1.name = “李四”; 并不是修改了原型中的值,而是在p1對象中給添加了一個屬性name。

看下面的代碼:

<body><scripttype="text/javascript">? ? ? functionPerson(){? ? ? ? ? ? }// 可以使用Person.prototype 直接訪問到原型對象//給Person函數(shù)的原型對象中添加一個屬性 name并且值是 "張三"Person.prototype.name ="張三";? ? ? Person.prototype.age =20;varp1 =newPerson();/*

? ? ? ? 訪問p1對象的屬性name,雖然在p1對象中我們并沒有明確的添加屬性name,但是

? ? ? ? p1的 [[prototype]] 屬性指向的原型中有name屬性,所以這個地方可以訪問到屬性name

? ? ? ? 就值。

? ? ? ? 注意:這個時候不能通過p1對象刪除name屬性,因為只能刪除在p1中刪除的對象。

? ? ? */alert(p1.name);// 張三varp2 =newPerson();? ? ? alert(p2.name);// 張三? 都是從原型中找到的,所以一樣。alert(p1.name === p2.name);// true// 由于不能修改原型中的值,則這種方法就直接在p1中添加了一個新的屬性name,然后在p1中無法再訪問到//原型中的屬性。p1.name ="李四";? ? ? alert("p1:"+ p1.name);// 由于p2中沒有name屬性,則對p2來說仍然是訪問的原型中的屬性。alert("p2:"+ p2.name);// 張三? </script></body>


二、與原型有關(guān)的幾個屬性和方法

2.1 prototype屬性

prototype 存在于構(gòu)造函數(shù)中 (其實任意函數(shù)中都有,只是不是構(gòu)造函數(shù)的時候prototype我們不關(guān)注而已) ,他指向了這個構(gòu)造函數(shù)的原型對象。

參考前面的示意圖。

2.2 constructor屬性

constructor屬性存在于原型對象中,他指向了構(gòu)造函數(shù)。

看下面的代碼:

<scripttype="text/javascript">? functionPerson(){? }? alert(Person.prototype.constructor === Person);// truevarp1 =newPerson();//使用instanceof 操作符可以判斷一個對象的類型。//typeof一般用來獲取簡單類型和函數(shù)。而引用類型一般使用instanceof,因為引用類型用typeof 總是返回object。alert(p1instanceofPerson);// true</script>

我們根據(jù)需要,可以Person.prototype 屬性指定新的對象,來作為Person的原型對象。

但是這個時候有個問題,新的對象的constructor屬性則不再指向Person構(gòu)造函數(shù)了。

看下面的代碼:

<scripttype="text/javascript">? functionPerson(){? ? ? }//直接給Person的原型指定對象字面量。則這個對象的constructor屬性不再指向Person函數(shù)Person.prototype = {name:"志玲",age:20};varp1 =newPerson();? alert(p1.name);// 志玲alert(p1instanceofPerson);// truealert(Person.prototype.constructor === Person);//false//如果constructor對你很重要,你應(yīng)該在Person.prototype中添加一行這樣的代碼:/*

? ? Person.prototype = {

? ? ? ? constructor : Person? //讓constructor重新指向Person函數(shù)

? ? }

? ? */</script>

2.3 __ proto __ 屬性(注意:左右各是2個下劃線)

用構(gòu)造方法創(chuàng)建一個新的對象之后,這個對象中默認會有一個不可訪問的屬性 [[prototype]] , 這個屬性就指向了構(gòu)造方法的原型對象。

但是在個別瀏覽器中,也提供了對這個屬性[[prototype]]的訪問(chrome瀏覽器和火狐瀏覽器。ie瀏覽器不支持)。訪問方式:p1.__ proto __

但是開發(fā)者盡量不要用這種方式去訪問,因為操作不慎會改變這個對象的繼承原型鏈。

<scripttype="text/javascript">? functionPerson(){? ? ? }//直接給Person的原型指定對象字面量。則這個對象的constructor屬性不再指向Person函數(shù)Person.prototype = {constructor: Person,name:"志玲",age:20};varp1 =newPerson();? alert(p1.__proto__ === Person.prototype);//true</script>

2.4 hasOwnProperty() 方法

大家知道,我們用去訪問一個對象的屬性的時候,這個屬性既有可能來自對象本身,也有可能來自這個對象的[[prototype]]屬性指向的原型。

那么如何判斷這個對象的來源呢?

hasOwnProperty方法,可以判斷一個屬性是否來自對象本身。

<scripttype="text/javascript">? functionPerson(){? ? ? }? Person.prototype.name ="志玲";varp1 =newPerson();? p1.sex ="女";//sex屬性是直接在p1屬性中添加,所以是truealert("sex屬性是對象本身的:"+ p1.hasOwnProperty("sex"));// name屬性是在原型中添加的,所以是falsealert("name屬性是對象本身的:"+ p1.hasOwnProperty("name"));//? age 屬性不存在,所以也是falsealert("age屬性是存在于對象本身:"+ p1.hasOwnProperty("age"));</script>

所以,通過hasOwnProperty這個方法可以判斷一個對象是否在對象本身添加的,但是不能判斷是否存在于原型中,因為有可能這個屬性不存在。

也即是說,在原型中的屬性和不存在的屬性都會返回fasle。

如何判斷一個屬性是否存在于原型中呢?

2.5 in 操作符

in操作符用來判斷一個屬性是否存在于這個對象中。但是在查找這個屬性時候,現(xiàn)在對象本身中找,如果對象找不到再去原型中找。換句話說,只要對象和原型中有一個地方存在這個屬性,就返回true。

<scripttype="text/javascript">? functionPerson(){? ? ? }? Person.prototype.name ="志玲";varp1 =newPerson();? p1.sex ="女";? alert("sex"inp1);// 對象本身添加的,所以truealert("name"inp1);//原型中存在,所以truealert("age"inp1);//對象和原型中都不存在,所以false</script>

回到前面的問題,如果判斷一個屬性是否存在于原型中?

如果一個屬性存在,但是沒有在對象本身中,則一定存在于原型中。

<scripttype="text/javascript">? functionPerson(){? }? Person.prototype.name ="志玲";varp1 =newPerson();? p1.sex ="女";//定義一個函數(shù)去判斷原型所在的位置functionpropertyLocation(obj, prop){if(!(propinobj)){? ? ? alert(prop +"屬性不存在");? ? }elseif(obj.hasOwnProperty(prop)){? ? ? alert(prop +"屬性存在于對象中");? ? }else{? ? ? alert(prop +"對象存在于原型中");? ? }? }? propertyLocation(p1,"age");? propertyLocation(p1,"name");? propertyLocation(p1,"sex");</script>

三、組合原型模型和構(gòu)造函數(shù)模型創(chuàng)建對象

3.1 原型模型創(chuàng)建對象的缺陷

原型中的所有的屬性都是共享的。也就是說,用同一個構(gòu)造函數(shù)創(chuàng)建的對象去訪問原型中的屬性的時候,大家都是訪問的同一個對象,如果一個對象對原型的屬性進行了修改,則會反映到所有的對象上面。

但是在實際使用中,每個對象的屬性一般是不同的。張三的姓名是張三,李四的姓名是李四。

但是,這個共享特性對方法(屬性值是函數(shù)的屬性)又是非常合適的。所有的對象共享方法是最佳狀態(tài)。這種特性在c#和Java中是天生存在的。

3.2 構(gòu)造函數(shù)模型創(chuàng)建對象的缺陷

在構(gòu)造函數(shù)中添加的屬性和方法,每個對象都有自己獨有的一份,大家不會共享。這個特性對屬性比較合適,但是對方法又不太合適。因為對所有對象來說,他們的方法應(yīng)該是一份就夠了,沒有必要每人一份,造成內(nèi)存的浪費和性能的低下。

<scripttype="text/javascript">? functionPerson(){this.name ="李四";this.age =20;this.eat =function(){? ? ? ? ? alert("吃完東西");? ? ? }? }varp1 =newPerson();varp2 =newPerson();//每個對象都會有不同的方法alert(p1.eat === p2.eat);//fasle</script>

可以使用下面的方法解決:

<scripttype="text/javascript">? functionPerson(){this.name ="李四";this.age =20;this.eat = eat;? }functioneat(){? ? ? alert("吃完東西");? ? }varp1 =newPerson();varp2 =newPerson();//因為eat屬性都是賦值的同一個函數(shù),所以是truealert(p1.eat === p2.eat);//true</script>

但是上面的這種解決方法具有致命的缺陷:封裝性太差。使用面向?qū)ο?,目的之一就是封裝代碼,這個時候為了性能又要把代碼抽出對象之外,這是反人類的設(shè)計。

3.3 使用組合模式解決上述兩種缺陷

原型模式適合封裝方法,構(gòu)造函數(shù)模式適合封裝屬性,綜合兩種模式的優(yōu)點就有了組合模式。

<scripttype="text/javascript">? //在構(gòu)造方法內(nèi)部封裝屬性functionPerson(name, age){this.name = name;this.age = age;? }//在原型對象內(nèi)封裝方法Person.prototype.eat =function(food){? ? alert(this.name +"愛吃"+ food);? }? Person.prototype.play =function(playName){? ? alert(this.name +"愛玩"+ playName);? }varp1 =newPerson("李四",20);varp2 =newPerson("張三",30);? p1.eat("蘋果");? p2.eat("香蕉");? p1.play("志玲");? p2.play("鳳姐");</script>

四、動態(tài)原型模式創(chuàng)建對象

前面講到的組合模式,也并非完美無缺,有一點也是感覺不是很完美。把構(gòu)造方法和原型分開寫,總讓人感覺不舒服,應(yīng)該想辦法把構(gòu)造方法和原型封裝在一起,所以就有了動態(tài)原型模式。

動態(tài)原型模式把所有的屬性和方法都封裝在構(gòu)造方法中,而僅僅在需要的時候才去在構(gòu)造方法中初始化原型,又保持了同時使用構(gòu)造函數(shù)和原型的優(yōu)點。

看下面的代碼:

<scripttype="text/javascript">? //構(gòu)造方法內(nèi)部封裝屬性functionPerson(name, age){//每個對象都添加自己的屬性this.name = name;this.age = age;/*

? ? ? ? 判斷this.eat這個屬性是不是function,如果不是function則證明是第一次創(chuàng)建對象,

? ? ? ? 則把這個funcion添加到原型中。

? ? ? ? 如果是function,則代表原型中已經(jīng)有了這個方法,則不需要再添加。

? ? ? ? perfect!完美解決了性能和代碼的封裝問題。

? ? ? */if(typeofthis.eat !=="function"){? ? ? ? Person.prototype.eat =function(){? ? ? ? ? alert(this.name +" 在吃");? ? ? ? }? ? ? }? }varp1 =newPerson("志玲",40);? p1.eat();</script>

說明:

組合模式和動態(tài)原型模式是JavaScript中使用比較多的兩種創(chuàng)建對象的方式。

建議以后使用動態(tài)原型模式。他解決了組合模式的封裝不徹底的缺點。

通過怎么樣的學(xué)習(xí),才能成為合格的WEB前端工程師?如果現(xiàn)在的你很迷茫,可以+web前端扣扣裙:956766604 免費網(wǎng)課在線學(xué)習(xí)以及問題解答、項目指導(dǎo)服務(wù),配合強大的學(xué)習(xí)工具,帶你完成九大實戰(zhàn)項目,經(jīng)歷從零基礎(chǔ)到專業(yè)前端工程師的完美蛻變。

歡迎初學(xué)和進階中的小伙伴。裙里有免費的學(xué)習(xí)資料,還有專業(yè)老師為你解惑,更有免費體驗課、免費直播課等福利不定期放送。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容