摘要:寄生組合式繼承終于寫到最后一個(gè)繼承了,我們?cè)谥爸v了種繼承方式,分別是原型鏈,借用構(gòu)造函數(shù)繼承,組合繼承,原型式繼承,寄生式繼承,其中,前三種聯(lián)系比較緊密,后面兩種也比較緊密,而我們要講的最后一種,是和組合繼承還有寄生式繼承有關(guān)系的。
前言
趁周末結(jié)束之前趕緊先把坑填上。上回我們說到了原型鏈,并且留下了幾個(gè)思考題,先把答案公布一下。
在最后一個(gè)例子里,console.log(b1.constructor),結(jié)果是什么?
答案:function A,因?yàn)閎1本身沒有constructor屬性,會(huì)沿著原型鏈向上找到B prototype對(duì)象,然后再往上找到A prototype對(duì)象,此時(shí)找到了constructor屬性,也就是指向函數(shù)對(duì)象A,可參見上文最后一張圖片
B.prototype = new A();和 B.prototype.sayB = function(){ console.log("from B") }這兩句的執(zhí)行順序能不能交換?
答案:不能,因?yàn)槲覀冋f過了,第一句是把改寫B(tài)函數(shù)對(duì)象的prototype指向的原型對(duì)象,如果我們交換了順序,是在原先的B的原型對(duì)象上綁定了方法,然后再把指針指向新的原型對(duì)象,那新的原型對(duì)象上自然就沒有綁定sayB方法,接下來的b1.sayB()就會(huì)報(bào)函數(shù)未定義錯(cuò)誤,
在最后一個(gè)例子里,A看似已經(jīng)是原型鏈的最頂層,那A還能再往上嗎?
答案,可以,因?yàn)槠鋵?shí)所有的引用類型都默認(rèn)繼承了了Object,也就是說,完整的原型鏈應(yīng)該是A prototype的[Prototype]屬性指向Object prototype。如圖:
順便補(bǔ)充一下,Object prototype上的原生方法,包括我們常用的hasOwnProperty()、isPropertyOf()等。
在上一篇我們講解了原型鏈的原理,建議沒有理解清楚的讀者朋友先理解之前的知識(shí)點(diǎn),避免難點(diǎn)疊加
原型鏈的缺陷
引用類型的值在原型鏈傳遞中存在的問題
我們知道js中有值類型和引用類型,其中引用類型包括Object.Array等,引用類型的值有一個(gè)特點(diǎn):在賦值的時(shí)候,賦給變量的是它在內(nèi)存中的地址。換句話說,被賦值完的變量相當(dāng)于一個(gè)指針,這會(huì)有什么問題呢?看例子:
function A() { this.name = "a" this.color = ["red","green"]; } function B(){ } //讓B的原型對(duì)象指向A的一個(gè)實(shí)例 B.prototype = new A(); //生成兩個(gè)個(gè)B的實(shí)例 var b1 = new B(); var b2 = new B(); //觀察color屬性 console.log(b1.name)//a console.log(b2.name)//a console.log(b1.color)//[red,green] console.log(b2.color)//[red,green] //改變b1的name和color屬性 b1.name = "b" b1.color.push("black") //重新觀察color屬性 console.log(b1)//b console.log(b2)//a console.log(b2.name) console.log(b1.color)//["red", "green", "black"] console.log(b2.color)//["red", "green", "black"]
發(fā)現(xiàn)問題了嗎?我們修改了b1的color和name屬性,但是b2的name屬性不變,color屬性發(fā)生了改變。為了搞清楚這里問題,請(qǐng)嘗試回答我的問題(想不出來的話,可以自己通過在控制臺(tái)打印出來驗(yàn)證):
b1和b2有自己的color屬性嗎?
答案:沒有,只是B prototype上有color屬性,因?yàn)樗?b>A的一個(gè)實(shí)例,b1和b2其實(shí)是通過[Proto]屬性訪問B prototype上的color屬性(指針),從而訪問和操作color數(shù)組的;
b1和b2有自己的name屬性嗎?
答案:一開始都沒有,當(dāng)執(zhí)行了b1.name = "b"時(shí),相當(dāng)于b1有了自己的name屬性,而b2依然沒有name屬性。
所以以上問題的原因來源就是我們前面說的:引用類型的值在賦值的時(shí)候,賦給變量的是它在內(nèi)存中的地址。(如果關(guān)于值類型和引用類型有沒掌握的同學(xué)可以先去看看或者私下問我,這里默認(rèn)這個(gè)是已經(jīng)了解的。)
所以在原型鏈中如果A(其實(shí)就是繼承中的父類型)含有引用類型的值,那么子類型的實(shí)例共享這個(gè)引用類型得值,也就是上面的color數(shù)組,這就是原型鏈的第一個(gè)缺陷。
第二個(gè)缺陷是:在創(chuàng)建子類型的實(shí)例(如b1,b2)時(shí),無法向父類型的構(gòu)造函數(shù)中傳遞參數(shù)。比如在上面的例子中,如果A的name屬性是要傳遞參數(shù)的而不是寫死的,那么我們?cè)趯?shí)例化b1和b2的時(shí)候根本沒法傳參
借用構(gòu)造函數(shù)繼承為了解決引用類型值帶來的問題,我們會(huì)采用借用構(gòu)造函數(shù)繼承的方式,又名*偽造對(duì)象或者經(jīng)典繼承,核心思路是:我們?cè)谧宇愋偷臉?gòu)造函數(shù)中調(diào)用父類型的構(gòu)造函數(shù),這里要用到一個(gè)方法call()或者apply()函數(shù),關(guān)于這個(gè)函數(shù),我這里簡單介紹一下,可以簡單的理解功能就是,允許一個(gè)對(duì)象調(diào)用另一個(gè)對(duì)象的方法。具體的作用如果大家覺得需要可以在評(píng)論區(qū)回復(fù),我會(huì)后面多帶帶寫一下這兩個(gè)函數(shù)。在這里就不展開了。具體實(shí)現(xiàn)如下:
function A() { this.name = "a" this.color = ["red","green"]; } function B(){ //“借用”|就體現(xiàn)在這里,子類型B借用了父類型A的構(gòu)造函數(shù),從而在這里實(shí)現(xiàn)了繼承 A.call(this); } //生成兩個(gè)個(gè)B的實(shí)例 var b1 = new B(); var b2 = new B(); //觀察color屬性 console.log(b1.name)//a console.log(b2.name)//a console.log(b1.color)//["red","green"] console.log(b2.color)//["red","green"] //改變b1的name和color屬性 b1.name = "b" b1.color.push("black") //重新觀察屬性 console.log(b1.name)//b console.log(b2.name)//a console.log(b1.color)//["red","green","black"] console.log(b2.color)//["red", "green"]
在這里我們沒有采用原型鏈,而是利用call()方法來實(shí)現(xiàn)在子類型的構(gòu)造函數(shù)中借用父類型的構(gòu)造函數(shù),完成了繼承,這樣繼承的結(jié)果就是:b1,b2都分別擁有自己的name和color屬性(可以直接console.log(b1)查看對(duì)象的屬性),也就是b1和b2完全獨(dú)立的。這就解決了之前的第一個(gè)問題,而且傳遞參數(shù)的問題其實(shí)也可以解決,再稍微改一下A函數(shù):
//這里name改成傳遞參數(shù)的 function A(name) { this.name = name this.color = ["red","green"]; } function B(name){ //在這里我們接受一個(gè)參數(shù),并且通過call方法傳遞到A的構(gòu)造函數(shù)中 A.call(this,name); } //生成兩個(gè)個(gè)B的實(shí)例 var b1 = new B("Mike"); var b2 = new B("Bob"); //觀察屬性 console.log(b1.name)//Mike console.log(b2.name)//Bob console.log(b1.color)//["red","green"] console.log(b2.color)//["red","green"]
其實(shí)上面就可以直接寫成這樣,但是為了讓大家更容易理解,故意分開,隔離變量(大家看我這么用心真的不考慮點(diǎn)個(gè)贊嗎?),順便再解釋一下A.call(this,name);就是讓this對(duì)象(這里是指B)調(diào)用構(gòu)造函數(shù)A,同時(shí)傳入一個(gè)參數(shù)name。
可以看到,借用構(gòu)造函數(shù)繼承不會(huì)有原型鏈繼承的問題,那為什么不都借用采用構(gòu)造函數(shù)繼承的方法呢?原因在于:這種繼承方式,所有的屬性和方法都要在構(gòu)造函數(shù)中定義,比如我們這里也要綁定之前的sayA()方法并繼承,就只能寫在A的構(gòu)造函數(shù)里面,而寫在A prototype的的方法,沒法通過這種方式繼承,而把所有的屬性和方法都要在構(gòu)造函數(shù)中定義的話,就不能對(duì)函數(shù)方法進(jìn)行復(fù)用.
組合繼承學(xué)習(xí)了原型鏈的繼承和借用構(gòu)造函數(shù)的繼承后,我們可以發(fā)現(xiàn),這兩種方法的優(yōu)缺點(diǎn)剛好互補(bǔ):
原型鏈繼承可以把方法定義在原型上,從而復(fù)用方法
借用構(gòu)造函數(shù)繼承法可以解決引用類型值的繼承問題和傳遞參數(shù)問題
因此,就自然而然的想到,結(jié)合這兩種方法,于是就有了下面的組合繼承,也叫偽經(jīng)典繼承,(前面的借用構(gòu)造函數(shù)是經(jīng)典繼承,可以聯(lián)系起來),具體實(shí)現(xiàn)如下:
function A(name) { this.name = name this.color = ["red","green"]; } A.prototype.sayA = function(){ console.log("form A") } function B(name,age){ //借用構(gòu)造函數(shù)繼承 A.call(this,name); this.age = age; } //原型鏈 B.prototype = new A(); B.prototype.sayB = function(){ console.log("form B") } //生成兩個(gè)個(gè)B的實(shí)例 var b1 = new B("Mike",12); var b2 = new B("Bob",13); //觀察color屬性 console.log(b1)//{name:"Mike"...} console.log(b2)//{name:"Bob"...} b1.sayA()//from A b2.sayB()//from B
這個(gè)例子只是對(duì)上面的例子稍作修改:
我們?cè)?b>A prototype上定義了sayA() ,在B prototype 定義了sayB()
我們?cè)黾恿?b>B.prototype = new A();原型鏈
最終實(shí)現(xiàn)的效果就是,b1和b2都有各自的屬性,同時(shí)方法都定義在兩個(gè)原型對(duì)象上,這就達(dá)到了我們的目的:屬性獨(dú)立,方法復(fù)用,這種繼承的理解相對(duì)簡單,因?yàn)榫褪前亚皟煞N繼承方式簡單的結(jié)合一下,原型鏈負(fù)責(zé)原型對(duì)象上的方法,call借用構(gòu)造函數(shù)負(fù)責(zé)讓子類型擁有各自的屬性。
組合繼承是js中最常用的繼承方式
原型式繼承與之前的繼承方式不太相同,原理上相當(dāng)于對(duì)對(duì)象進(jìn)行一次淺復(fù)制,淺復(fù)制簡單的說就是:把父對(duì)像的屬性,全部拷貝給子對(duì)象。但是我們前面說到,由于引用類型值的賦值特點(diǎn),所以屬性如果是引用類型的值,拷貝過去的也僅僅是個(gè)指針,拷貝完后父子對(duì)象的指針是指向同一個(gè)引用類型的(關(guān)于深復(fù)制和淺復(fù)制如果需要細(xì)講的同樣可以在評(píng)論區(qū)留言。)原型式繼承目前可以通過Object.create()方式來實(shí)現(xiàn),(這個(gè)函數(shù)的原理我不想在這里提,因?yàn)槲蚁Mx者在看完這里內(nèi)容以后自己去查閱一下這個(gè)內(nèi)容)本文只講實(shí)現(xiàn)方式:
Object.create()接收兩個(gè)參數(shù):
第一個(gè)參數(shù)是作為新對(duì)象的原型的對(duì)象
第二個(gè)參數(shù)是定義為新對(duì)象增加額外屬性的對(duì)象(這個(gè)是可選屬性)
如果沒有傳遞第二個(gè)參數(shù)的話,就相當(dāng)于直接運(yùn)行object()方法(這個(gè)方法如果不懂直接百度就好)
上面的說法可能有點(diǎn)拗口,換句話說:
比如說我們現(xiàn)在要?jiǎng)?chuàng)建一個(gè)新對(duì)象B,那么要先傳入第一個(gè)參數(shù)對(duì)象A,這個(gè)A將被作為B prototype;然后可以再傳入一個(gè)參數(shù)對(duì)象C,C對(duì)象中可以定義我們需要的一些額外的屬性。來看例子
var A = { name:"A", color:["red","green"] } //使用Object.create方法先復(fù)制一個(gè)對(duì)象 var B = Object.create(A); B.name = "B"; B.color.push("black"); //使用Object.create方法再復(fù)制一個(gè)對(duì)象 var C = Object.create(A); C.name = "C"; B.color.push("blue"); console.log(A.name)//A console.log(B.name)//B console.log(C.name)//C console.log(A.color)//["red", "green", "black", "blue"]
在這個(gè)例子中,我們只傳入第一個(gè)參數(shù),所以B和C都是對(duì)A淺復(fù)制的結(jié)果,由于name是值類型的,color是引用類型的,所以ABC的name值獨(dú)立,color屬性指向同一個(gè)對(duì)象。接下來舉個(gè)傳遞兩個(gè)參數(shù)的例子:
var A = { name:"A", color:["red","green"], sayA:function(){ console.log("from A"); } }; //使用Object.create方法先復(fù)制一個(gè)對(duì)象 var B = Object.create(A,{ name:{ value:"B" } }); console.log(B)//Object{name:"B"} B.sayA()//"from A"
這個(gè)例子就很清楚的表明了這個(gè)函數(shù)的作用了,傳入的A對(duì)象被當(dāng)做B的原型,所以生成B對(duì)象沒有sayA()方法,卻可以調(diào)用該方法(類似于通過原型鏈),同時(shí)我們?cè)诘诙€(gè)參數(shù)中修改了B自己的name,所以就實(shí)現(xiàn)了這種原型式繼承。原型式繼承的好處是:如果我們只是簡單的想保持一個(gè)對(duì)象和另一個(gè)對(duì)象類似,不必大費(fèi)周章寫一堆代碼,直接調(diào)用就能實(shí)現(xiàn)
寄生式繼承寄生式繼承和原型繼承聯(lián)系緊密,思路類似于工廠模式,即創(chuàng)建一個(gè)只負(fù)責(zé)封裝繼承過程的函數(shù),在函數(shù)中根據(jù)需要增強(qiáng)對(duì)象,最后返回對(duì)象
function createA(name){ //創(chuàng)建新對(duì)象 var obj = Object(name); //增強(qiáng)功能 obj.sayO = function(){ console.log("from O") }; //返回對(duì)象 return obj; } var A = { name:"A", color:["red","green","blue"] }; //實(shí)現(xiàn)繼承 var B = createA(A); console.log(B)//Object {name: "A", color: Array[3]} B.sayO();//from O
繼承的結(jié)果是B擁有A的所有屬性和方法,而且具有自己的sayO()方法,效果和原型式繼承很相似,讀者可以比較一下寄生式繼承和原型式繼承的相似和區(qū)別。
寄生組合式繼承終于寫到最后一個(gè)繼承了,我們?cè)谥爸v了5種繼承方式,分別是原型鏈,借用構(gòu)造函數(shù)繼承,組合繼承,原型式繼承,寄生式繼承,其中,前三種聯(lián)系比較緊密,后面兩種也比較緊密,而我們要講的最后一種,是和組合繼承還有寄生式繼承有關(guān)系的。(看名字就知道了嘛)
友情提示:如果看到這里有點(diǎn)累的讀者可以先休息一下,因?yàn)殡m然已經(jīng)分了一二兩篇,本文的篇幅還是稍長(我都打了兩個(gè)多小時(shí)了),而且如果先把之前的理解清楚,比較容易理解最后一種繼承。
組合繼承仍有缺陷我們?cè)谥罢f過,最常用的繼承方式就是組合繼承,但是看似完美的組合繼承依然有缺點(diǎn):子類型會(huì)兩次調(diào)用父類型的構(gòu)造函數(shù),一次是在子類型的構(gòu)造函數(shù)里,另一次是在實(shí)現(xiàn)原型鏈的步驟,來看之前的代碼:
function A(name) { this.name = name this.color = ["red","green"]; } A.prototype.sayA = function(){ console.log("form A") } function B(name,age){ //第二次調(diào)用了A A.call(this,name); this.age = age; } //第一次調(diào)用了A B.prototype = new A(); B.prototype.sayB = function(){ console.log("form B") } var b1 = new B("Mike",12); var b2 = new B("Bob",13); console.log(B.prototype)//A {name: undefined, color: Array[2]}
在第一次調(diào)用的時(shí)候,生成了B.prototype對(duì)象,它具有name和color屬性,因?yàn)樗?b>A的一個(gè)實(shí)例;第二次調(diào)用的時(shí)候,就是實(shí)例化b1和b2的時(shí)候,這時(shí)候b1和b2也具有了name和color屬性,我們之前說過,原型鏈的意義是:當(dāng)對(duì)象本身不存在某個(gè)屬性或方法的時(shí)候,可以沿著原型鏈向上查找,如果對(duì)象自身已經(jīng)有某種屬性或者方法,就訪問自身的,但是我們現(xiàn)在發(fā)現(xiàn),通過組合繼承,只要是A里面原有的屬性,B prototype對(duì)象一定會(huì)有,b1和b2肯定也會(huì)有,這樣就造成了一種浪費(fèi):B prototyope上的屬性其實(shí)我們根本用不上,為了解決這個(gè)問題,我們采用寄生組合式繼承。
寄生組合式繼承的核心思路是其實(shí)就是換一種方式實(shí)現(xiàn) B.prototype = new A();從而避免兩次調(diào)用父類型的構(gòu)造函數(shù),官方定義是:使用寄生式繼承來繼承父類型的原型,然后將結(jié)果指定給子類型的原型,。`這句話不容易理解,來看例子:
//我們一直默認(rèn)A是父類型,B是子類型 function inheritPrototype(B,A){ //復(fù)制一個(gè)A的原型對(duì)象 var pro = Object(A.prototype); //改寫這個(gè)原型對(duì)象的constructor指針指向B pro.constructor = B; //改寫B(tài)的prototype指針指向這個(gè)原型對(duì)象 B.prototype = pro; }
這個(gè)函數(shù)很簡短,只有三行,函數(shù)內(nèi)部發(fā)生的事情是:我們復(fù)制一個(gè)A的原型對(duì)象,然后把這個(gè)原型對(duì)象替換掉B的原型對(duì)象。為什么說這樣就代替了 B.prototype = new A();,不妨思考一下,我們最初為什么要把B的prototype屬性指向A的一個(gè)實(shí)例?無非就是想得到A的prototype的一個(gè)復(fù)制品,然后實(shí)現(xiàn)原型鏈。而現(xiàn)在我們這樣的做法,同樣達(dá)到了我們的母的目的,而且,此時(shí)B的原型對(duì)象上不會(huì)再有A的屬性了,因?yàn)樗皇茿的實(shí)例。因此,只要把將上面的 B.prototype = new A();,替換成inheritPrototype(B,A),就完成了寄生組合式繼承。
寄生組合式繼承保持了組合繼承的優(yōu)點(diǎn),又避開了組合繼承會(huì)有無用屬性的缺陷,被認(rèn)為是最理想的繼承方式。
小結(jié)終于寫完了!! 明天還得起早去上班,下一次更新可能會(huì)放在這一周的周末。關(guān)于這一篇內(nèi)容,建議的閱讀方式是先讀前三種繼承方式,再看后兩種繼承,都理解的差不多了,就可以看最后一種繼承方式了。中間注意消化和休息。最后再提一下吧:如果喜歡本文,請(qǐng)大方的點(diǎn)一下右上角的推薦和收藏(反正你們還是喜歡只收藏不推薦),雖然說寫這個(gè)一方面是為了自己鞏固知識(shí),但是為了讓讀者更容易理解,我盡量都是采用拆解的方式來講,而且穿插了新知識(shí)的時(shí)候都會(huì)給出解釋,并不是直接搬運(yùn)書本知識(shí)過來,那樣毫無意義。這么做還是希望寫的文章能夠更有價(jià)值,讓更多人能夠得到幫助!以上內(nèi)容屬于個(gè)人見解,如果有不同意見,歡迎指出和探討。請(qǐng)尊重作者的版權(quán),轉(zhuǎn)載請(qǐng)注明出處,如作商用,請(qǐng)與作者聯(lián)系,感謝!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/82096.html
摘要:當(dāng)然這還沒完,因?yàn)槲覀冞€有重要的一步?jīng)]完成,沒錯(cuò)就是上面的第行代碼,如果沒有這行代碼實(shí)例中的指針是指向構(gòu)造函數(shù)的,這樣顯然是不對(duì)的,因?yàn)檎G闆r下應(yīng)該指向它的構(gòu)造函數(shù),因此我們需要手動(dòng)更改使重新指向?qū)ο蟆? 第一節(jié)內(nèi)容:javaScript原型及原型鏈詳解(二) 第一節(jié)中我們介紹了javascript中的原型和原型鏈,這一節(jié)我們來講利用原型和原型鏈我們可以做些什么。 普通對(duì)象的繼承 ...
摘要:系列種優(yōu)化頁面加載速度的方法隨筆分類中個(gè)最重要的技術(shù)點(diǎn)常用整理網(wǎng)頁性能管理詳解離線緩存簡介系列編寫高性能有趣的原生數(shù)組函數(shù)數(shù)據(jù)訪問性能優(yōu)化方案實(shí)現(xiàn)的大排序算法一怪對(duì)象常用方法函數(shù)收集數(shù)組的操作面向?qū)ο蠛驮屠^承中關(guān)鍵詞的優(yōu)雅解釋淺談系列 H5系列 10種優(yōu)化頁面加載速度的方法 隨筆分類 - HTML5 HTML5中40個(gè)最重要的技術(shù)點(diǎn) 常用meta整理 網(wǎng)頁性能管理詳解 HTML5 ...
摘要:系列種優(yōu)化頁面加載速度的方法隨筆分類中個(gè)最重要的技術(shù)點(diǎn)常用整理網(wǎng)頁性能管理詳解離線緩存簡介系列編寫高性能有趣的原生數(shù)組函數(shù)數(shù)據(jù)訪問性能優(yōu)化方案實(shí)現(xiàn)的大排序算法一怪對(duì)象常用方法函數(shù)收集數(shù)組的操作面向?qū)ο蠛驮屠^承中關(guān)鍵詞的優(yōu)雅解釋淺談系列 H5系列 10種優(yōu)化頁面加載速度的方法 隨筆分類 - HTML5 HTML5中40個(gè)最重要的技術(shù)點(diǎn) 常用meta整理 網(wǎng)頁性能管理詳解 HTML5 ...
摘要:理解的函數(shù)基礎(chǔ)要搞好深入淺出原型使用原型模型,雖然這經(jīng)常被當(dāng)作缺點(diǎn)提及,但是只要善于運(yùn)用,其實(shí)基于原型的繼承模型比傳統(tǒng)的類繼承還要強(qiáng)大。中文指南基本操作指南二繼續(xù)熟悉的幾對(duì)方法,包括,,。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。 怎樣使用 this 因?yàn)楸救藢儆趥吻岸耍虼宋闹兄豢炊?8 成左右,希望能夠給大家?guī)韼椭?...(據(jù)說是阿里的前端妹子寫的) this 的值到底...
閱讀 462·2023-04-25 23:00
閱讀 3493·2021-11-22 13:54
閱讀 1892·2021-10-27 14:14
閱讀 1485·2019-08-30 13:59
閱讀 3510·2019-08-23 16:15
閱讀 1957·2019-08-23 16:06
閱讀 3326·2019-08-23 15:26
閱讀 1256·2019-08-23 13:48