當前位置:
首頁 > 知識 > 簡單粗暴地理解 JS 原型鏈

簡單粗暴地理解 JS 原型鏈

作者:茄果


www.cnblogs.com/qieguo/archive/2016/05/03/5451626.html


原型鏈理解起來有點繞了,網上資料也是很多,每次晚上睡不著的時候總喜歡在網上找點原型鏈和閉包的文章看,效果極好。


不要糾結於那一堆術語了,那除了讓你腦筋擰成麻花,真的不能幫你什麼。簡單粗暴點看原型鏈吧,想點與代碼無關的事,比如人、妖以及人妖。


1)人是人他媽生的,妖是妖他媽生的。人和妖都是對象實例,而人他媽和妖他媽就是原型。原型也是對象,叫原型對象。

簡單粗暴地理解 JS 原型鏈



2)人他媽和人他爸啪啪啪能生出一堆人寶寶、妖他媽和妖他爸啪啪啪能生出一堆妖寶寶,啪啪啪就是構造函數,俗稱造人。

簡單粗暴地理解 JS 原型鏈



3)人他媽會記錄啪啪啪的信息,所以可以通過人他媽找到啪啪啪的信息,也就是說能通過原型對象找到構造函數。


4)人他媽可以生很多寶寶,但這些寶寶只有一個媽媽,這就是原型的唯一性。


5)人他媽也是由人他媽他媽生的,通過人他媽找到人他媽他媽,再通過人他媽他媽找到人他媽他媽……,這個關係叫做原型鏈。

簡單粗暴地理解 JS 原型鏈


6)原型鏈並不是無限的,當你通過人他媽一直往上找,最後發現你會發現人他媽他媽他媽……的他媽都不是人,也就是原型鏈最終指向null。


7)人他媽生的人會有人的樣子,妖他媽生的妖會有妖的醜陋,這叫繼承。

簡單粗暴地理解 JS 原型鏈



8)你繼承了你媽的膚色,你媽繼承了你媽他媽的膚色,你媽他媽……,這就是原型鏈的繼承。


9)你談對象了,她媽讓你帶上房產證去提貨,你若沒有,那她媽會問你媽有沒有,你媽沒有那她媽會問你媽她媽有沒有……這就是原型鏈的向上搜索。


10)你會繼承你媽的樣子,但是你也可以去染髮洗剪吹,就是說對象的屬性可以自定義,會覆蓋繼承得到的屬性。

簡單粗暴地理解 JS 原型鏈



11)雖然你洗剪吹了染成黃毛了,但你不能改變你媽的樣子,你媽生的弟弟妹妹跟你的黃毛洗剪吹沒一點關係,就是說對象實例不能改動原型的屬性。

12)但是你家被你玩火燒了的話,那就是說你家你媽家你弟們家都被燒了,這就是原型屬性的共享。


13)你媽外號阿珍,鄰居大娘都叫你阿珍兒,但你媽頭髮從飄柔做成了金毛獅王后,隔壁大嬸都改口叫你包租仔,這叫原型的動態性。

簡單粗暴地理解 JS 原型鏈



14)你媽愛美,又跑到韓國整形,整到你媽他媽都認不出來,即使你媽頭髮換回飄柔了,但隔壁鄰居還是叫你金毛獅王子。因為沒人認出你媽,整形後的你媽已經回爐再造了,這就是原型的整體重寫。

簡單粗暴地理解 JS 原型鏈



尼瑪!你特么也是夠了! Don』t BB! Show me the code!


function Person (name) { this.name = name; }


function Mother () { }

Mother.prototype = { //Mother的原型


age: 18,


home: [ Beijing , Shanghai ]


};


Person.prototype = new Mother(); //Person的原型為Mother


//用chrome調試工具查看,提供了__proto__介面查看原型,這裡有兩層原型,各位還是直接看chrome好一點。


var p1 = new Person( Jack ); //p1: Jack ; __proto__:


var p2 = new Person( Mark ); //p2: Mark ; __proto__:


p1.age = 20;


/* 實例不能改變原型的基本值屬性,正如你洗剪吹染黃毛跟你媽無關

* p1:下面多了個屬性age,而__proto__跟 Mother.prototype一樣,age=18。


* p2:只有屬性name,__proto__跟 Mother.prototype一樣


*/


p1.home[0] = Shenzhen ;


/* 原型中引用類型屬性的共享,正如你燒了你家,就是燒了你全家的家


* 這個先過,下文再仔細嘮叨一下可好?


* p1: Jack ,20; __proto__:


* p2: Mark ; __proto__:


*/


p1.home = [ Hangzhou , Guangzhou ];

/* 其實跟p1.age=20一樣的操作。換成這個理解: var o={}; o.home=[ big , house ]


* p1: Jack ,20,[ Hangzhou , Guangzhou ]; __proto__:


* p2: Mark ; __proto__:


*/


delete p1.age;


/* 刪除實例的屬性之後,原本被覆蓋的原型值就重見天日了。正如你剃了光頭,遺傳的迷人小捲髮就長出來了。


* 這就是向上搜索機制,先搜你,然後你媽,再你媽他媽,所以你媽的改動會動態影響你。


* p1: Jack ,[ Hangzhou , Guangzhou ]; __proto__:


* p2: Mark ; __proto__:


*/

/* 改寫原型,動態反應到實例中。正如你媽變新潮了,鄰居提起你都說你媽真潮。


* 注意,這裡我們改寫的是Person的原型,就是往Mother里加一個lastName屬性,等同於Mother.lastName= Jin


* 這裡並不是改Mother.prototype,改動不同的層次,效果往往會有很大的差異。


* p1: Jack ,[ Hangzhou , Guangzhou ]; __proto__:{ jin ,__proto__:18,[ Shenzhen , Shanghai ]}


* p2: Mark ; __proto__:{ jin ,__proto__:18,[ Shenzhen , Shanghai ]}


*/


Person.prototype = {


age: 28,


address: { country: USA , city: Washington }


};

var p3 = new Person( Obama );


/* 重寫原型!這個時候Person的原型已經完全變成一個新的對象了,也就是說Person換了個媽,叫後媽。


* p1: Jack ,[ Hangzhou , Guangzhou ]; __proto__:{ jin ,__proto__:18,[ Shenzhen , Shanghai ]}


* p2: Mark ; __proto__:{ jin ,__proto__:18,[ Shenzhen , Shanghai ]}


* p3: Obama ;__proto__: 28


*/


/* 改寫原型的原型,動態反應到實例中。正如你媽他媽變新潮了,鄰居提起你都說你丫外婆真潮。


* 注意,這裡我們改寫的是Mother.prototype,p1p2會變,但上面p3跟親媽已經了無瓜葛了,不影響他。


* p1: Jack ,[ Hangzhou , Guangzhou ]; __proto__:{ jin ,__proto__:18,[ Shenzhen , Shanghai ],9527}


* p2: Mark ; __proto__:{ jin ,__proto__:18,[ Shenzhen , Shanghai ],9527}

* p3: Obama ;__proto__: 28


*/


Mother.prototype = {


car: 2,


hobby: [ run , walk ]


};


var p4 = new Person( Tony );


/* 重寫原型的原型!這個時候Mother的原型已經完全變成一個新的對象了!人他媽換了個後媽!


* 由於上面Person與Mother已經斷開聯繫了,這時候Mother怎麼變已經不影響Person了。


* p4: Tony ;__proto__: 28


*/


Person.prototype = new Mother(); //再次綁定


var p5 = new Person( Luffy );


// 這個時候如果需要應用這些改動的話,那就要重新將Person的原型綁到mother上了


// p5: Luffy ;__proto__:


p1.__proto__.__proto__.__proto__.__proto__ //null,你說原型鏈的終點不是null?


Mother.__proto__.__proto__.__proto__ //null,你說原型鏈的終點不是null?


看完基本能理解了吧?


現在再來說說 p1.age = 20、p1.home = [『Hangzhou』, 『Guangzhou』] 和 p1.home[0] = 『Shenzhen』 的區別。 p1.home[0] = 『Shenzhen』; 總結一下是 p1.object.method,p1.object.property 這樣的形式。


p1.age = 20; p1.home = [『Hangzhou』, 『Guangzhou』];這兩句還是比較好理解的,先忘掉原型吧,想想我們是怎麼為一個普通對象增加屬性的:


varobj=newObject();


obj.name= xxx ;


obj.num=[100,200];


這樣是不是就理解了呢?一樣一樣的呀。


那為什麼 p1.home[0] = 『Shenzhen』 不會在 p1 下創建一個 home 數組屬性,然後將其首位設為 『Shenzhen』呢? 我們還是先忘了這個,想想上面的obj對象,如果寫成這樣: var obj.name = 『xxx』, obj.num = [100, 200],能得到你要的結果嗎? 顯然,除了報錯你什麼都得不到。因為obj還未定義,又怎麼能往裡面加入東西呢?同理,p1.home[0]中的 home 在 p1 下並未被定義,所以也不能直接一步定義 home[0] 了。如果要在p1下創建一個 home 數組,當然是這麼寫了:


p1.home=[];


p1.home[]= Shenzhen ;


這不就是我們最常用的辦法嗎?


而之所以 p1.home[0] = 『Shenzhen』 不直接報錯,是因為在原型鏈中有一個搜索機制。當我們輸入 p1.object 的時候,原型鏈的搜索機制是先在實例中搜索相應的值,找不到就在原型中找,還找不到就再往上一級原型中搜索……一直到了原型鏈的終點,就是到null還沒找到的話,就返回一個 undefined。當我們輸入 p1.home[0] 的時候,也是同樣的搜索機制,先搜索 p1 看有沒有名為 home 的屬性和方法,然後逐級向上查找。最後我們在Mother的原型裡面找到了,所以修改他就相當於修改了 Mother 的原型啊。


由上面的分析可以知道,原型鏈繼承的主要問題在於屬性的共享,很多時候我們只想共享方法而並不想要共享屬性,理想中每個實例應該有獨立的屬性。因此,原型繼承就有了下面的兩種改良方式:


1)組合繼承


functionMother(age){


this.age=age;


this.hobby=[ running , football ]


}


console.log(this.age);


};


functionPerson(name,age){


Mother.call(this,age);//第二次執行


this.name=name;


}


Person.prototype=newMother();//第一次執行


console.log(this.name);


}


varp1=newPerson( Jack ,20);


p1.hobby.push( basketball );//p1: Jack ; __proto__:20,[ running , football ]


varp2=newPerson( Mark ,18);//p2: Mark ; __proto__:18,[ running , football ]


結果是醬紫的:

簡單粗暴地理解 JS 原型鏈


簡單粗暴地理解 JS 原型鏈



這裡第一次執行的時候,得到 Person.prototype.age = undefined, Person.prototype.hobby = [『running』,』football』],第二次執行也就是 var p1 = new Person(『Jack』, 20) 的時候,得到 p1.age =20, p1.hobby = [『running』,』football』],push後就變成了 p1.hobby = [『running』,』football』, 『basketball』]。其實分辨好 this 的變化,理解起來也是比較簡單的,把 this 簡單替換一下就能得到這個結果了。 如果感覺理解起來比較繞的話,試著把腦子裡面的概念扔掉吧,把自己當瀏覽器從上到下執行一遍代碼,結果是不是就出來了呢?


通過第二次執行原型的構造函數 Mother(),我們在對象實例中複製了一份原型的屬性,這樣就做到了與原型屬性的分離獨立。細心的你會發現,我們第一次調用 Mother(),好像什麼用都沒有呢,能不調用他嗎?可以,就有了下面的寄生組合式繼承。


2)寄生組合式繼承


functionobject(o){


functionF(){}


F.prototype=o;


returnnewF();


}


functioninheritPrototype(Person,Mother){


varprototype=object(Mother.prototype);


prototype.constructor=Person;


Person.prototype=prototype;


}


functionMother(age){


this.age=age;


this.hobby=[ running , football ]


}


console.log(this.age);


};


functionPerson(name,age){


Mother.call(this,age);


this.name=name;


}


inheritPrototype(Person,Mother);


console.log(this.name);


}


varp1=newPerson( Jack ,20);


p1.hobby.push( basketball );//p1: Jack ; __proto__:20,[ running , football ]


varp2=newPerson( Mark ,18);//p2: Mark ; __proto__:18,[ running , football ]


結果是醬紫的:

簡單粗暴地理解 JS 原型鏈


簡單粗暴地理解 JS 原型鏈



原型中不再有 age 和 hobby 屬性了,只有兩個方法,正是我們想要的結果!


關鍵點在於 object(o) 裡面,這裡借用了一個臨時對象來巧妙避免了調用new Mother(),然後將原型為 o 的新對象實例返回,從而完成了原型鏈的設置。很繞,對吧,那是因為我們不能直接設置 Person.prototype = Mother.prototype 啊。


小結


說了這麼多,其實核心只有一個:屬性共享和獨立的控制,當你的對象實例需要獨立的屬性,所有做法的本質都是在對象實例裡面創建屬性。若不考慮太多,你大可以在Person裡面直接定義你所需要獨立的屬性來覆蓋掉原型的屬性。總之,使用原型繼承的時候,要對於原型中的屬性要特別注意,因為他們都是牽一髮而動全身的存在。


下面簡單羅列下js中創建對象的各種方法,現在最常用的方法是組合模式,熟悉的同學可以跳過到文章末尾點贊了。


1)原始模式


//1.原始模式,對象字面量方式


varperson={


name: Jack ,


age:18,


sayName:function(){alert(this.name);}


};


//1.原始模式,Object構造函數方式


varperson=newObject();


person.name= Jack ;


person.age=18;


person.sayName=function(){


alert(this.name);


};


顯然,當我們要創建批量的person1、person2……時,每次都要敲很多代碼,資深copypaster都吃不消!然後就有了批量生產的工廠模式。


2)工廠模式


//2.工廠模式,定義一個函數創建對象


functioncreatPerson(name,age){


varperson=newObject();


person.name=name;


person.age=age;


person.sayName=function(){


alert(this.name);


};


returnperson;


}


工廠模式就是批量化生產,簡單調用就可以進入造人模式(啪啪啪……)。指定姓名年齡就可以造一堆小寶寶啦,解放雙手。但是由於是工廠暗箱操作的,所以你不能識別這個對象到底是什麼類型、是人還是狗傻傻分不清(instanceof 測試為 Object),另外每次造人時都要創建一個獨立的temp對象,代碼臃腫,雅蠛蝶啊。


3)構造函數


//3.構造函數模式,為對象定義一個構造函數


functionPerson(name,age){


this.name=name;


this.age=age;


this.sayName=function(){


alert(this.name);


};


}


varp1=newPerson( Jack ,18);//創建一個p1對象


Person( Jack ,18);//屬性方法都給window對象,window.name= Jack ,window.sayName()會輸出Jack


構造函數與C++、JAVA中類的構造函數類似,易於理解,另外Person可以作為類型識別(instanceof 測試為 Person 、Object)。但是所有實例依然是獨立的,不同實例的方法其實是不同的函數。這裡把函數兩個字忘了吧,把sayName當做一個對象就好理解了,就是說張三的 sayName 和李四的 sayName是不同的存在,但顯然我們期望的是共用一個 sayName 以節省內存。


4)原型模式


//4.原型模式,直接定義prototype屬性


functionPerson(){}


//4.原型模式,字面量定義方式


functionPerson(){}


Person.prototype={


name: Jack ,


age:18,


sayName:function(){alert(this.name);}


};


varp1=newPerson();//name= Jack


varp2=newPerson();//name= Jack


這裡需要注意的是原型屬性和方法的共享,即所有實例中都只是引用原型中的屬性方法,任何一個地方產生的改動會引起其他實例的變化。


5)混合模式(構造+原型)


//5. 原型構造組合模式,


functionPerson(name,age){


this.name=name;


this.age=age;


}


Person.prototype={


hobby:[ running , football ];


sayName:function(){alert(this.name);},


sayAge:function(){alert(this.age);}


};


varp1=newPerson( Jack ,20);


//p1: Jack ,20; __proto__: [ running , football ],sayName,sayAge


varp2=newPerson( Mark ,18);


//p1: Mark ,18;__proto__: [ running , football ],sayName,sayAge


做法是將需要獨立的屬性方法放入構造函數中,而可以共享的部分則放入原型中,這樣做可以最大限度節省內存而又保留對象實例的獨立性。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序源 的精彩文章:

一個獨立開發者總結的App 迭代設計思路
架構師是否應該寫代碼?
Windows的十大常見故障和解決方法
Docker Mesos在生產環境的應用

TAG:程序源 |

您可能感興趣

區塊鏈的簡單理解
簡單理解Hadoop架構原理
簡單易懂教你理解數字貨幣
繪畫上色的簡單理解
五張圖理解一致性Hash原理
冷棕色原理解密 公式拆解
架構拆分原理解析
圓頓大法對「空相」的理解原來如此簡單!
深度理解PLC的工作原理
Derek:剖析源碼,一文理解比原鏈
JSONP淺顯理解
如何簡單理解AI與機器學習
理解權益證明安全模型的原理
理解焦慮,原地不爆炸
如何直觀地理解條件隨機場,並通過PyTorch簡單地實現
從語義上理解卷積核行為,UCLA朱松純等人使用決策樹量化解釋CNN
JCI:靶向作用炎症或能理解危險血凝塊形成的分子機制
簡單幾句話理解受眾與用戶的區別
回調函數的簡單理解及應用
深入理解單例模式 ( 下 )