摘要:原型繼承基本模式這種是最簡單實現(xiàn)原型繼承的方法,直接把父類的對象賦值給子類構(gòu)造函數(shù)的原型,這樣子類的對象就可以訪問到父類以及父類構(gòu)造函數(shù)的中的屬性。
真正意義上來說Javascript并不是一門面向?qū)ο蟮恼Z言,沒有提供傳統(tǒng)的繼承方式,但是它提供了一種原型繼承的方式,利用自身提供的原型屬性來實現(xiàn)繼承。Javascript原型繼承是一個被說爛掉了的話題,但是自己對于這個問題一直沒有徹底理解,今天花了點時間又看了一遍《Javascript模式》中關(guān)于原型實現(xiàn)繼承的幾種方法,下面來一一說明下,在最后我根據(jù)自己的理解提出了一個關(guān)于繼承比較完整的實現(xiàn),如果大家有不同意見,歡迎建議。
原型與原型鏈說原型繼承之前還是要先說說原型和原型鏈,畢竟這是實現(xiàn)原型繼承的基礎(chǔ)。
在Javascript中,每個函數(shù)都有一個原型屬性prototype指向自身的原型,而由這個函數(shù)創(chuàng)建的對象也有一個__proto__屬性指向這個原型,而函數(shù)的原型是一個對象,所以這個對象也會有一個__proto__指向自己的原型,這樣逐層深入直到Object對象的原型,這樣就形成了原型鏈。下面這張圖很好的解釋了Javascript中的原型和原型鏈的關(guān)系。
每個函數(shù)都是Function函數(shù)創(chuàng)建的對象,所以每個函數(shù)也有一個__proto__屬性指向Function函數(shù)的原型。這里需要指出的是,真正形成原型鏈的是每個對象的__proto__屬性,而不是函數(shù)的prototype屬性,這是很重要的。
原型繼承 基本模式var Parent = function(){ this.name = "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(){ this.name = "child" ; } ; Child.prototype = new Parent() ; var parent = new Parent() ; var child = new Child() ; console.log(parent.getName()) ; //parent console.log(child.getName()) ; //child
這種是最簡單實現(xiàn)原型繼承的方法,直接把父類的對象賦值給子類構(gòu)造函數(shù)的原型,這樣子類的對象就可以訪問到父類以及父類構(gòu)造函數(shù)的prototype中的屬性。 這種方法的原型繼承圖如下:
這種方法的優(yōu)點很明顯,實現(xiàn)十分簡單,不需要任何特殊的操作;同時缺點也很明顯,如果子類需要做跟父類構(gòu)造函數(shù)中相同的初始化動作,那么就得在子類構(gòu)造函數(shù)中再重復(fù)一遍父類中的操作:
var Parent = function(name){ this.name = name || "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ this.name = name || "child" ; } ; Child.prototype = new Parent() ; var parent = new Parent("myParent") ; var child = new Child("myChild") ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild
上面這種情況還只是需要初始化name屬性,如果初始化工作不斷增加,這種方式是很不方便的。因此就有了下面一種改進的方式。
借用構(gòu)造函數(shù)var Parent = function(name){ this.name = name || "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; Child.prototype = new Parent() ; var parent = new Parent("myParent") ; var child = new Child("myChild") ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild
上面這種方法在子類構(gòu)造函數(shù)中通過apply調(diào)用父類的構(gòu)造函數(shù)來進行相同的初始化工作,這樣不管父類中做了多少初始化工作,子類也可以執(zhí)行同樣的初始化工作。但是上面這種實現(xiàn)還存在一個問題,父類構(gòu)造函數(shù)被執(zhí)行了兩次,一次是在子類構(gòu)造函數(shù)中,一次在賦值子類原型時,這是很多余的,所以我們還需要做一個改進:
var Parent = function(name){ this.name = name || "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; Child.prototype = Parent.prototype ; var parent = new Parent("myParent") ; var child = new Child("myChild") ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild
這樣我們就只需要在子類構(gòu)造函數(shù)中執(zhí)行一次父類的構(gòu)造函數(shù),同時又可以繼承父類原型中的屬性,這也比較符合原型的初衷,就是把需要復(fù)用的內(nèi)容放在原型中,我們也只是繼承了原型中可復(fù)用的內(nèi)容。上面這種方式的原型圖如下:
上面借用構(gòu)造函數(shù)模式最后改進的版本還是存在問題,它把父類的原型直接賦值給子類的原型,這就會造成一個問題,就是如果對子類的原型做了修改,那么這個修改同時也會影響到父類的原型,進而影響父類對象,這個肯定不是大家所希望看到的。為了解決這個問題就有了臨時構(gòu)造函數(shù)模式。
var Parent = function(name){ this.name = name || "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; var F = function(){} ; F.prototype = Parent.prototype ; Child.prototype = new F() ; var parent = new Parent("myParent") ; var child = new Child("myChild") ; console.log(parent.getName()) ; //myParent console.log(child.getName()) ; //myChild
該方法的原型繼承圖如下:
很容易可以看出,通過在父類原型和子類原型之間加入一個臨時的構(gòu)造函數(shù)F,切斷了子類原型和父類原型之間的聯(lián)系,這樣當子類原型做修改時就不會影響到父類原型。
《Javascript模式》中到圣杯模式就結(jié)束了,可是不管上面哪一種方法都有一個不容易被發(fā)現(xiàn)的問題。大家可以看到我在"Parent"的prototype屬性中加入了一個obj對象字面量屬性,但是一直都沒有用。我們在圣杯模式的基礎(chǔ)上來看看下面這種情況:
var Parent = function(name){ this.name = name || "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; var F = function(){} ; F.prototype = Parent.prototype ; Child.prototype = new F() ; var parent = new Parent("myParent") ; var child = new Child("myChild") ; console.log(child.obj.a) ; //1 console.log(parent.obj.a) ; //1 child.obj.a = 2 ; console.log(child.obj.a) ; //2 console.log(parent.obj.a) ; //2
在上面這種情況中,當我修改child對象obj.a的時候,同時父類的原型中的obj.a也會被修改,這就發(fā)生了和共享原型同樣的問題。出現(xiàn)這個情況是因為當訪問child.obj.a的時候,我們會沿著原型鏈一直找到父類的prototype中,然后找到了obj屬性,然后對obj.a進行修改。再看看下面這種情況:
var Parent = function(name){ this.name = name || "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : 1} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; var F = function(){} ; F.prototype = Parent.prototype ; Child.prototype = new F() ; var parent = new Parent("myParent") ; var child = new Child("myChild") ; console.log(child.obj.a) ; //1 console.log(parent.obj.a) ; //1 child.obj.a = 2 ; console.log(child.obj.a) ; //2 console.log(parent.obj.a) ; //2
這里有一個關(guān)鍵的問題,當對象訪問原型中的屬性時,原型中的屬性對于對象來說是只讀的,也就是說child對象可以讀取obj對象,但是無法修改原型中obj對象引用,所以當child修改obj的時候并不會對原型中的obj產(chǎn)生影響,它只是在自身對象添加了一個obj屬性,覆蓋了父類原型中的obj屬性。而當child對象修改obj.a時,它先讀取了原型中obj的引用,這時候child.obj和Parent.prototype.obj是指向同一個對象的,所以child對obj.a的修改會影響到Parent.prototype.obj.a的值,進而影響父類的對象。AngularJS中關(guān)于$scope嵌套的繼承方式就是模范Javasript中的原型繼承來實現(xiàn)的。
根據(jù)上面的描述,只要子類對象中訪問到的原型跟父類原型是同一個對象,那么就會出現(xiàn)上面這種情況,所以我們可以對父類原型進行拷貝然后再賦值給子類原型,這樣當子類修改原型中的屬性時就只是修改父類原型的一個拷貝,并不會影響到父類原型。具體實現(xiàn)如下:
var deepClone = function(source,target){ source = source || {} ; target = target || {}; var toStr = Object.prototype.toString , arrStr = "[object array]" ; for(var i in source){ if(source.hasOwnProperty(i)){ var item = source[i] ; if(typeof item === "object"){ target[i] = (toStr.apply(item).toLowerCase() === arrStr) ? [] : {} ; deepClone(item,target[i]) ; }else{ target[i] = item; } } } return target ; } ; var Parent = function(name){ this.name = name || "parent" ; } ; Parent.prototype.getName = function(){ return this.name ; } ; Parent.prototype.obj = {a : "1"} ; var Child = function(name){ Parent.apply(this,arguments) ; } ; Child.prototype = deepClone(Parent.prototype) ; var child = new Child("child") ; var parent = new Parent("parent") ; console.log(child.obj.a) ; //1 console.log(parent.obj.a) ; //1 child.obj.a = "2" ; console.log(child.obj.a) ; //2 console.log(parent.obj.a) ; //1
綜合上面所有的考慮,Javascript繼承的具體實現(xiàn)如下,這里只考慮了Child和Parent都是函數(shù)的情況下:
var deepClone = function(source,target){ source = source || {} ; target = target || {}; var toStr = Object.prototype.toString , arrStr = "[object array]" ; for(var i in source){ if(source.hasOwnProperty(i)){ var item = source[i] ; if(typeof item === "object"){ target[i] = (toStr.apply(item).toLowerCase() === arrStr) ? [] : {} ; deepClone(item,target[i]) ; }else{ target[i] = item; } } } return target ; } ; var extend = function(Parent,Child){ Child = Child || function(){} ; if(Parent === undefined) return Child ; //借用父類構(gòu)造函數(shù) Child = function(){ Parent.apply(this,argument) ; } ; //通過深拷貝繼承父類原型 Child.prototype = deepClone(Parent.prototype) ; //重置constructor屬性 Child.prototype.constructor = Child ; } ;總結(jié)
說了這么多,其實Javascript中實現(xiàn)繼承是十分靈活多樣的,并沒有一種最好的方法,需要根據(jù)不同的需求實現(xiàn)不同方式的繼承,最重要的是要理解Javascript中實現(xiàn)繼承的原理,也就是原型和原型鏈的問題,只要理解了這些,自己實現(xiàn)繼承就可以游刃有余。
最后,安利下我的個人博客,歡迎訪問: http://bin-playground.top文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/85377.html
摘要:面向?qū)ο笾杏腥筇卣?,封裝,繼承,多態(tài)。這不僅無法做到數(shù)據(jù)共享,也是極大的資源浪費,那么引入對象實例對象的屬性指向其構(gòu)造函數(shù),這樣看起來實例對象好像繼承了對象一樣。實例對象的原型指向其構(gòu)造函數(shù)的對象構(gòu)造器的指向。 前言 為什么說是再談呢,網(wǎng)上講解這個的博客的很多,我開始學(xué)習(xí)也是看過,敲過就沒了,自以為理解了就結(jié)束了,書到用時方恨少啊。實際開發(fā)中一用就打磕巴,于是在重新學(xué)習(xí)了之后分享出來...
摘要:設(shè)計模式是以面向?qū)ο缶幊虨榛A(chǔ)的,的面向?qū)ο缶幊毯蛡鹘y(tǒng)的的面向?qū)ο缶幊逃行┎顒e,這讓我一開始接觸的時候感到十分痛苦,但是這只能靠自己慢慢積累慢慢思考。想繼續(xù)了解設(shè)計模式必須要先搞懂面向?qū)ο缶幊?,否則只會讓你自己更痛苦。 JavaScript 中的構(gòu)造函數(shù) 學(xué)習(xí)總結(jié)。知識只有分享才有存在的意義。 是時候替換你的 for 循環(huán)大法了~ 《小分享》JavaScript中數(shù)組的那些迭代方法~ ...
摘要:有一函數(shù)若是用來生成對象,則稱為構(gòu)造函數(shù)名。屬性指定了使用該構(gòu)造函數(shù)生成的對象實例繼承了哪個對象實例。因此,只要利用,就能在構(gòu)造函數(shù)中,為未來利用此構(gòu)造函數(shù)生成的對象實例,添加成員屬性和成員方法了。 與其它編程語言不一樣的是,javascript的面向?qū)ο蟛⒎且蕾囉诔橄蟮念?,而是通過原型鏈,將一個個具體的對象實例進行連接,位于原型鏈下游的對象實例可以讀取/使用位于上游的對象實例的屬性/...
摘要:如果構(gòu)造函數(shù)有返回值呢一般情況下構(gòu)造函數(shù)沒有返回值,但是我們依舊可以得到該對象的實例如果構(gòu)造函數(shù)有返回值,憑直覺來說情況應(yīng)該會不一樣。歡迎光臨小弟博客我的博客原文你真的弄明白了嗎參考再談面向?qū)ο缶幊痰膶嵗c繼承請停止使用關(guān)鍵字 好久沒有寫點東西了,總覺得自己應(yīng)該寫點牛逼的,卻又不知道如何下筆。既然如此,還是回歸最基本的吧,今天就來說一說這個new。關(guān)于javascript的new關(guān)鍵...
摘要:不必在構(gòu)造函數(shù)中定義對象實例的信息。其次,按照一切事物皆對象的這餓極本的面向?qū)ο蟮姆▌t來說,類本身并不是一個對象,然而原型方式的構(gòu)造函數(shù)和原型本身也是個對象。第二個問題就是在創(chuàng)建子類型的實例時,不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。 前言 對象(Object)應(yīng)該算是js中最為重要的部分,也是js中非常難懂晦澀的一部分。更是面試以及框架設(shè)計中各出沒。寫這篇文章,主要參考與JavaScrip...
閱讀 1049·2021-09-13 10:29
閱讀 3396·2019-08-29 18:31
閱讀 2642·2019-08-29 11:15
閱讀 3020·2019-08-26 13:25
閱讀 1377·2019-08-26 12:00
閱讀 2314·2019-08-26 11:41
閱讀 3412·2019-08-26 10:31
閱讀 1493·2019-08-26 10:25