JS原形與原型鏈深入詳解
本文實(shí)例講述了JS原形與原型鏈。分享給大家供大家參考,具體如下:
前言
在JS中,我們經(jīng)常會(huì)遇到原型。字面上的意思會(huì)讓我們認(rèn)為,是某個(gè)對象的原型,可用來繼承。但是其實(shí)這樣的理解是片面的,下面通過本文來了解原型與原型鏈的細(xì)節(jié),再順便談?wù)劺^承的幾種方式。
原型
在講到原型之前,我們先來回顧一下JS中的對象。在JS中,萬物皆對象,就像字符串、數(shù)值、布爾、數(shù)組
等。ECMA-262把對象定義為:無序?qū)傩缘募?,其屬性可包含基本值、對象或函?shù)。對象是擁有屬性和方法的數(shù)據(jù),為了描述這些事物,便有了原型的概念。
無論何時(shí),只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向該函數(shù)的原型對象。所有原型對象都會(huì)獲得一個(gè)constructor屬性,這個(gè)屬性包含一個(gè)指向prototype屬性所在函數(shù)的指針。
這段話摘自《JS高級程序設(shè)計(jì)》,很好理解,以創(chuàng)建實(shí)例的代碼為例。
function Person(name, age) { this.name = name; this.age = age; this.sayName = function() { alert(this.name); }; } const person1 = new Person("gali", 18); const person2 = new Person("pig", 20);
上面例子中的person1跟person2都是構(gòu)造函數(shù)Person()
的實(shí)例,Person.prototype指向了Person函數(shù)的原型對象,而Person.prototype.constructor又指向Person。Person的每一個(gè)實(shí)例,都含有一個(gè)內(nèi)部屬性__proto__
,指向Person.prototype,就像上圖所示,因此就有下面的關(guān)系。
console.log(Person.prototype.constructor === Person); // true console.log(person1.__proto__ === Person.prototype); // true console.log(person2.__proto__ === Person.prototype); // true
繼承
JS是基于原型的語言,跟基于類的面向?qū)ο笳Z言有所不同,JS中并沒有類這個(gè)概念,有的是原型對象這個(gè)概念,原型對象作為一個(gè)模板,新對象可從原型對象中獲得屬性。那么JS具體是怎樣繼承的呢?
在講到繼承這個(gè)話題之前,我們先來理解原型鏈這個(gè)概念。
原型鏈
構(gòu)造函數(shù),原型和實(shí)例的關(guān)系已經(jīng)很清楚了。每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對象,原型對象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例對象都包含一個(gè)指向與原型對象的指針。這樣的關(guān)系非常好理解,但是如果我們想讓原型對象等于另一個(gè)類型的實(shí)例對象呢?那么就會(huì)衍生出相同的關(guān)系,此時(shí)的原型對象就會(huì)含有一個(gè)指向另一個(gè)原型對象的指針,而另一個(gè)原型對象會(huì)含有一個(gè)指向另一個(gè)構(gòu)造函數(shù)的指針。如果另一個(gè)原型對象又是另一個(gè)類型的實(shí)例對象呢?這樣就構(gòu)成了原型鏈。文字可能有點(diǎn)難理解,下面用代碼舉例。
function SuperType() { this.name = "張三"; } SuperType.prototype.getSuperName = function() { return this.name; }; function SubType() { this.subname = "李四"; } SubType.prototype = new SuperType(); SubType.prototype.getSubName = function() { return this.subname; }; const instance = new SubType(); console.log(instance.getSuperName()); // 張三
上述例子中,SubType的原型對象作為SuperType構(gòu)造函數(shù)
的實(shí)例對象,此時(shí),SubType的原型對象
就會(huì)有一個(gè)__proto__
屬性指向SuperType的原型對象
,instance作為SubType的實(shí)例對象,必然能共享SubType的原型對象的屬性,又因?yàn)?code>SubType的原型對象又指向SuperType原型對象
的屬性,因此可得,instance繼承了SuperType原型的所有屬性。
我們都知道,所有函數(shù)的默認(rèn)原型都是Object的實(shí)例,所以也能得出,SuperType的默認(rèn)原型必然有一個(gè)__proto__
指向Object.prototype。
圖中由__proto__
屬性組成的鏈子,就是原型鏈,原型鏈的終點(diǎn)就是null。
上圖可很清晰的看出原型鏈的結(jié)構(gòu),這不禁讓我想到JS的一個(gè)運(yùn)算符instanceof,instanceof可用來判斷一個(gè)實(shí)例對象是否屬于一個(gè)構(gòu)造函數(shù)。
A instanceof B; // true
實(shí)現(xiàn)原理其實(shí)就是在A的原型鏈上尋找是否有原型等于B.prototype,如果一直找到A原型鏈的頂端null,仍然找不到原型等于B.prototype,那么就可返回false。下面手寫一個(gè)instanceof
,這個(gè)也是很多大廠常用的手寫面試題。
function Instance(left, right) { left = left.__proto__; right = right.prototype; while (true) { if (left === null) return false; if (left === right) return true; // 繼續(xù)在left的原型鏈向上找 left = left.__propo__; } }
原型鏈繼承
上面例子中,instance繼承了SuperType原型的屬性,其繼承的原理其實(shí)就是通過原型鏈實(shí)現(xiàn)的。原型鏈很強(qiáng)大,可用來實(shí)現(xiàn)繼承。可是單純的原型鏈繼承也是有問題存在的。
- 實(shí)例屬性變成原型屬性,影響其他實(shí)例
- 創(chuàng)建子類型的實(shí)例時(shí),不能向超類型的構(gòu)造函數(shù)傳遞參數(shù)
function SuperType() { this.colorArr = ["red", "blue", "green"]; } function SubType() {} SubType.prototype = new SuperType(); const instance1 = new SubType(); instance1.colorArr.push("black"); console.log(instance1.colorArr); // ["red", "blue", "green", "black"] const instance2 = new SubType(); console.log(instance2.colorArr); // ["red", "blue", "green", "black"]
當(dāng)SubType的原型作為SuperType的實(shí)例時(shí),此時(shí)SubType的實(shí)例對象通過原型鏈繼承到colorArr屬性,當(dāng)修改了其中一個(gè)實(shí)例對象從原型鏈中繼承到的原型屬性時(shí),便會(huì)影響到其他實(shí)例。對instance1.colorArr的修改,在instance2.colorArr便能體現(xiàn)出來。
組合繼承
組合繼承指的是組合原型鏈和構(gòu)造函數(shù)的技術(shù)
,通過原型鏈實(shí)現(xiàn)對原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)實(shí)現(xiàn)對實(shí)例屬性的繼承。
function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { // 繼承屬性,借用構(gòu)造函數(shù)實(shí)現(xiàn)對實(shí)例屬性的繼承 SuperType.call(this, name); this.age = age; } // 繼承原型屬性及方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }; const instance1 = new SubType("gali", 18); instance1.colors.push("black"); console.log(instance1.colors); // ["red", "blue", "green", "black"] instance1.sayName(); // gali instance1.sayAge(); // 18 const instance2 = new SubType("pig", 20); console.log(instance2.colors); // ["red", "blue", "green"] instance2.sayName(); // pig instance2.sayAge(); // 20
上述例子中,借用構(gòu)造函數(shù)繼承實(shí)例屬性,通過原型繼承原型屬性與方法。這樣就可讓不同的實(shí)例分別擁有自己的屬性,又可共享相同的方法。而不會(huì)像原型繼承那樣,對實(shí)例屬性的修改影響到了其他實(shí)例。組合繼承是JS最常用的繼承方式。
寄生組合式繼承
雖然說組合繼承是最常用的繼承方式,但是有沒有發(fā)現(xiàn),就上面的例子中,組合繼承中調(diào)用了2次SuperType函數(shù)?;貞浺幌?,在第一次調(diào)用SubType時(shí)。
SubType.prototype = new SuperType();
這里調(diào)用完之后,SubType.prototype會(huì)從SuperType繼承到2個(gè)屬性:name和colors。這2個(gè)屬性存在SubType的原型中。而在第二次調(diào)用時(shí),就是在創(chuàng)造實(shí)例對象時(shí),調(diào)用了SubType構(gòu)造函數(shù),也就會(huì)再調(diào)用一次SuperType構(gòu)造函數(shù)。
SuperType.call(this, name);
第二次調(diào)用之后,便會(huì)在新的實(shí)例對象上創(chuàng)建了實(shí)例屬性:name和colors。也就是說,這個(gè)時(shí)候,實(shí)例對象跟原型對象擁有2個(gè)同名屬性。這樣實(shí)在是浪費(fèi),效率又低。
為了解決這個(gè)問題,引入了寄生組合繼承方式。重點(diǎn)就在于,不需要為了定義SubType的原型而去調(diào)用SuperType構(gòu)造函數(shù),此時(shí)只需要SuperType原型的一個(gè)副本,并將其賦值給SubType的原型即可。
function InheritPrototype(subType, superType) { // 創(chuàng)建超類型原型的一個(gè)副本 const prototype = Object(superType.prototype); // 添加constructor屬性,因?yàn)橹貙懺蜁?huì)失去constructor屬性 prototype.constructor = subType; subType.prototype = prototype; }
將組合繼承中的:
SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType;
替換成:
InheritPrototype(SubType, SuperType);
寄生組合繼承的優(yōu)點(diǎn)在于,只需要調(diào)用一次SuperType構(gòu)造函數(shù)。避免了在SubType的原型上創(chuàng)建多余的不必要的屬性。
總結(jié)
溫故而知新,再次看回《JS高級程序設(shè)計(jì)》這本書的原型與原型鏈部分,發(fā)現(xiàn)很多以前忽略掉的知識(shí)點(diǎn)。而這次回看這個(gè)知識(shí)點(diǎn),并輸出了一篇文章,對我來說受益匪淺。寫文章往往不是為了寫出怎樣的文章,其實(shí)中間學(xué)習(xí)的過程才是最享受的。
感興趣的朋友可以使用在線HTML/CSS/JavaScript代碼運(yùn)行工具:http://tools.jb51.net/code/HtmlJsRun測試上述代碼運(yùn)行效果。
更多關(guān)于JavaScript相關(guān)內(nèi)容感興趣的讀者可查看本站專題:《javascript面向?qū)ο笕腴T教程》、《JavaScript錯(cuò)誤與調(diào)試技巧總結(jié)》、《JavaScript數(shù)據(jù)結(jié)構(gòu)與算法技巧總結(jié)》、《JavaScript遍歷算法與技巧總結(jié)》及《JavaScript數(shù)學(xué)運(yùn)算用法總結(jié)》
希望本文所述對大家JavaScript程序設(shè)計(jì)有所幫助。
相關(guān)文章
js實(shí)現(xiàn)隨機(jī)數(shù)小游戲
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)隨機(jī)數(shù)小游戲,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-06-06JS實(shí)現(xiàn)拖拽元素時(shí)與另一元素碰撞檢測
這篇文章主要為大家詳細(xì)介紹了JS實(shí)現(xiàn)拖拽元素時(shí)與另一元素碰撞檢測,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-08-08PHP配置文件php.ini中打開錯(cuò)誤報(bào)告的設(shè)置方法
這篇文章主要介紹了PHP配置文件php.ini中打開錯(cuò)誤報(bào)告的設(shè)置方法,需要的朋友可以參考下2015-01-01常見Ajax下載文件方式以及報(bào)錯(cuò)解決辦法
AJAX(Asynchronous JavaScript and XML)是一種用于創(chuàng)建快速、動(dòng)態(tài)和交互式網(wǎng)頁的技術(shù),它的主要優(yōu)勢在于能夠在不刷新整個(gè)網(wǎng)頁的情況下與服務(wù)器進(jìn)行數(shù)據(jù)交互,這篇文章主要給大家介紹了關(guān)于常見Ajax下載文件方式以及報(bào)錯(cuò)解決辦法的相關(guān)資料,需要的朋友可以參考下2024-01-01用循環(huán)或if語句從json中取數(shù)據(jù)示例
倘若想將id和pid數(shù)據(jù)依次取出,就只能用循環(huán),若想有選擇性的輸出時(shí),需要添加if條件2014-08-08JS遍歷ul下的li點(diǎn)擊彈出li的索引的實(shí)現(xiàn)方法
這篇文章主要介紹了JS遍歷ul下的li點(diǎn)擊彈出li的索引的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09JS.getTextContent(element,preformatted)使用介紹
JS.getTextContent獲取標(biāo)簽的文字想必大家并不陌生吧,下面為大家介紹下具體的使用方法,感興趣的朋友可以參考下2013-09-09