兩年前在我學習JavaScript的時候我就寫過兩篇關于原型繼承的博客:
理解JavaScript中原型繼承
JavaScript中的原型繼承
這兩篇博客講的都是原型的使用,其中一篇還有我學習時的錯誤理解。今天看《Understanding Scopes》這讓我從新思考了一下原型繼承,更重要的是站在一個繼承設計者的角度再看一下原型繼承。
在傳統的面向類的繼承體系中,我們有個Best Practices是優先使用(對象)組合代替(類)繼承,而原型繼承是這個思想的一個運用。和面向對象和函數式編程一樣,使用幾乎任何語言都可以實現這樣的思想,我以前學的只是這個思想的一個JavaScript實現,而已。
基于原型的繼承其實是一種組合式的繼承,樸素的說法就是子域中屬性找不到的話就去父域中找找,這里的父域是用原型(__proto__)去引用的,依次遞歸整個原型鏈。最終的實現其實就是對象的組合。子對象包含父對象的引用。既然是繼承必然涉及到重名問題,子對象和父對象各自相當于一個作用域,重名問題的處理也是就近(可覆蓋shadow/隱藏hide)原則,即子作用域的同名屬性會起作用,隱藏了父作用域的同名屬性,但是由于是組合,這兩個屬性是獨立的。我們用偽代碼看看:
aParent = {name:’jerry’}
aChild = {__proto__:aParent, name:’frank’}
aChild中的name和aParent中的name是各自獨立的。我們aChild.name=’unknown’并不會改變aParent.name。
?
有一點要拿出單獨說說,造成迷糊的最大根源就是誤解,對于如下代碼:
aParent = {name:’jerry’}
aChild = {__proto__:aParent}
若我們取aChild.name的值,我們很容易resolve,那就是子域中找不到,去父域中找,找到了jerry。但是對于:aChild.name = ‘frank’這樣的賦值代碼我們會產生歧義(ambiguous),我們可能有兩中含義:
1,更新父域中的name屬性為frank。
2,設置子域中的name屬性為frank。
JavaScript選用的方式是第2種。即設置(新建)子域自己的name屬性為frank,并隱藏了父域中的name屬性。我們通常誤以為JavaScript是按1的方式工作,其實不是。
?
另外類(模版)其實在編程語言的實現中是可有可無的,像JavaScript壓根就沒有類(模版),他只有對象,new Point()只不過是一個語法糖,跟aObj = createObject()是一樣的,只是調用一個方法去生成一個對象,而已。