在面向?qū)ο笤O(shè)計(jì)中,最多被使用的就是類了。但是js中沒(méi)有類的概念即纲,也就沒(méi)有繼承等一系列相應(yīng)的操作。然后會(huì)有一種仿類的方式博肋,實(shí)現(xiàn)繼承方式低斋,其中就使用到了一種重要的概念:[[Prototype]]蜂厅,但是要如何理解[[Prototype]],什么才是[[Prototype]]膊畴,該如何使用呢掘猿?
-
[[Prototype]]是什么?
先看一段代碼:
// case1 function Fun(name){ this.name = name } var obj = new Fun('obj') console.log(Object.getPrototypeOf(obj) === Fun.prototype) console.log(obj.__proto__ === Fun.prototype) 輸出: true true // case2 var a={ name: 'a' } var b = Object.create(a) console.log(b.name) console.log(Object.getPrototypeOf(b) == a) 輸出: a true
如case1中通過(guò)構(gòu)造函數(shù)方式調(diào)用方法Fun新生成對(duì)象,通過(guò)
Object.getPrototypeof
方法獲取到對(duì)象的[[Prototype]]鏈指向了Fun.prototype
屬性值唇跨。(其實(shí)有些瀏覽器中支持__proto__
獲取對(duì)象的[[Prototype]]鏈)case2中通過(guò)
Object.create
方式創(chuàng)建一個(gè)新對(duì)象b
關(guān)聯(lián)到對(duì)象a
稠通,b的prototype指向了a,此時(shí)雖然b
對(duì)象沒(méi)有屬性name轻绞,但是console.log(b.name)
輸出了a
采记。此處prototype就起到了重要的作用。js中獲取一個(gè)對(duì)象的屬性值涉及到對(duì)象的[[Get]]操作:先從對(duì)象的直接屬性里面找尋政勃,如果未找到唧龄,此時(shí)會(huì)沿著對(duì)象的[[Prototype]]鏈上找尋,直至最頂層的[[Prototype]]
(一般Object.prototype為最頂層),如果還未找到奸远,就會(huì)返回undefined
,這也很好的解釋了b.name
既棺。prototype只是對(duì)象中的一個(gè)屬性,它讓對(duì)象可以關(guān)聯(lián)到另外一個(gè)對(duì)象懒叛。
prototype會(huì)影響到j(luò)s中的[[Get]]操作和[[Put]]操作丸冕,至于如何影響[[Get]]操作上述已描述過(guò)烹植,下面描述下[[Put]]操作
再看段代碼:
var a={ name: 'a', set id(val){ this._id_=val }, get id(){ return this._id_ } } Object.defineProperty(a,'desc',{ writable:false, value: 'desc of a' }) var b = Object.create(a) // case1:writable=true屬性 b.name = 'b' console.log('a.name:' + a.name) // a.name:a console.log('b.name:' + b.name) // b.name:b // case2:writable=false 屬性 b.desc='desc of b' // 默認(rèn)無(wú)效许饿,在strict模式下會(huì)報(bào)錯(cuò)!者蠕! console.log('a.desc:' + a.desc) // a.desc:desc of a console.log('b.desc:' + b.desc) // b.desc:desc of a // case3:setter屬性 b.id = 'b_id' console.log('a.id:' + a.id) // a.id:undefined console.log('b.id:' + b.id) // b.id:b_id
當(dāng)給對(duì)象屬性賦值時(shí)會(huì)執(zhí)行[[Put]]操作诅迷,如果屬性直屬于該對(duì)象佩番,則直接更改屬性值。如果屬性不直屬于該對(duì)象同時(shí)也不存在對(duì)象的[[Prototype]]鏈上時(shí)罢杉,直接給該對(duì)象創(chuàng)建該屬性并賦值趟畏。如存在于[[Prototype]]鏈上時(shí),就像上面的代碼示例中表現(xiàn)的那樣:
- 如果該屬性可寫滩租,此時(shí)則在對(duì)象上創(chuàng)建同樣的屬性并賦值赋秀,此時(shí)屏蔽了[[Prototype]]鏈上的屬性值。
- 如果該屬性不可寫律想,若運(yùn)行在非嚴(yán)格模式下猎莲,代碼默認(rèn)無(wú)效,若在嚴(yán)格模式下蜘欲,會(huì)拋錯(cuò)益眉。
- 如果屬性是一個(gè)setter,此時(shí)執(zhí)行該setter姥份,然后也會(huì)在對(duì)象上創(chuàng)建同樣的屬性并賦值郭脂,屏蔽[[Prototype]]鏈上的屬性值。
由此可以得出結(jié)論:對(duì)象中的[[Prototype]]其實(shí)是其中的一個(gè)屬性澈歉,用來(lái)關(guān)聯(lián)到另外一個(gè)對(duì)象展鸡。它起到了一個(gè)委托關(guān)聯(lián)的作用,可以訪問(wèn)到被關(guān)聯(lián)對(duì)象中的屬性埃难、方法莹弊。類似于其余面向?qū)ο笳Z(yǔ)言中的類繼承概念,只是類似涡尘,其中并唔發(fā)生類繼承中的拷貝操作忍弛。
-
prototype使用場(chǎng)景?
基本上了解到了[[Prototype]]鏈考抄,那么會(huì)在那些場(chǎng)景中使用呢细疚?
主要應(yīng)用于“類”繼承中。
js中很多場(chǎng)景會(huì)被設(shè)計(jì)成:將眾多操作對(duì)象抽象成一個(gè)抽象的"類"川梅,然后定義n多“子類”疯兼,通過(guò)重寫繼承過(guò)來(lái)的方法實(shí)現(xiàn)“偽多態(tài)”的現(xiàn)象。
代碼示例:function View(name){ this.name = name } View.prototype.print= function(){ console.log(this.name) } function IView(name, id){ View.call(this,name) this.id = id } // ES5 IView.prototype=Object.create(View.prototype) // Object.setPrototypeOf(IView.prototype, View.prototype) ES6 IView.prototype.print=function(){ View.prototype.print.call(this) console.log('name:%s,id:%s',this.name,this.id) } var v = new IView('iview', 1) v.print() // 輸出: iview name:iview,id:1
上述代碼中定義了兩個(gè)“類”:View和IView贫途,然后通過(guò)IView.prototype=Object.create(View.prototype)吧彪,看上去讓IView繼承View,再重寫View原型鏈中的方法print丢早。
當(dāng)然要實(shí)現(xiàn)上述功能姨裸,并非要通過(guò)創(chuàng)造偽類(構(gòu)造函數(shù)方式調(diào)用/new 函數(shù)調(diào)用)的方式。因?yàn)樯厦娲a中雖然IView看上去繼承View怨酝,但是實(shí)質(zhì)上并沒(méi)有像繼承一樣拷貝一份View中的內(nèi)容到IView中傀缩,只是通過(guò)[[Prototype]]方式讓IView關(guān)聯(lián)到View上。這樣就不是真正意義上類繼承的操作方式了凫碌。下面介紹種通過(guò)關(guān)聯(lián)的方式(符合js特征)
var View={ init(name){ this.name = name }, print(){ console.log(this.name) } } var IView = Object.create(View) IView.set=function(name,id){ this.init(name) this.id=id } // **注意此處不可再定義IView.init方法扑毡,否則會(huì)出現(xiàn)循環(huán)調(diào)用(報(bào)錯(cuò):RangeError: Maximum call stack size exceeded)** IView.show=function(){ this.print() console.log('name:%s,id:%s',this.name,this.id) } var v = Object.create(IView) v.set('iview',2) v.show() // 輸出: iview name:iview,id:2
上述通過(guò)Object.create方式將IView的[[Prototype]]指向了View,然后再創(chuàng)建對(duì)象關(guān)聯(lián)到IView對(duì)象上盛险。同樣實(shí)現(xiàn)了上述代碼片斷1中的效果瞄摊。
-
ES6中的語(yǔ)法糖class
ES6中引入了class關(guān)鍵字語(yǔ)法糖,從此不需要通過(guò)改寫Prototype苦掘,像上述第一段代碼中的方式實(shí)現(xiàn)"類"繼承了换帜。
將上述代碼用class改寫:class View { construtor(name){ this.name = name } print(){ console.log(this.name) } } class IView extends View { construtor(name,id){ super(name) this.id=id } print(){ super.print() console.log('name:%s,id:%s',this.name,this.id) } } var v = new IView('iview',3) v.print()
最后(Last but not least)
[[Prototype]]原型鏈提供了一個(gè)對(duì)象關(guān)聯(lián)到另一個(gè)對(duì)象的方式。js中被沒(méi)有類的概念鹤啡,實(shí)現(xiàn)類似的類繼承的方式惯驼,可以通過(guò)委托關(guān)聯(lián)的形式實(shí)現(xiàn)我們想要的需求,這樣更有利于加深我們對(duì)js中[[Prototype]]的理解。
參考資料:[美]KYLE SIMPSON著 趙望野 梁杰 譯 《你不知道的javascript》上卷