前兩次總結了JavaScript中的基本數據類型(值類型<引用類型>,引用類型<復雜值>)以及他們在內存中的存儲,對內存空間有了一個簡單的了解,以及第二次總結了this深入淺出的用法,我們知道了this的用法取決於函數四種調用的方式。
這一次我們來對JavaScript中原型以及原型鏈做一個深入淺出的理解。
JavaScript深入淺出系列
1)復雜值vs原始值&&內存空間 - JavaScript深入淺出(一)
待續......關於原型的話題都會變得嚴肅。
實際上,原型只是一個被稱為"原型"的空對象屬性,它是由JavaScript在后台創建(當然我們知道了它的原理,可以手動完成這項工作);
當你創建一個函數時,這個函數都會有一個prototype屬性(不管你是不把它當做一個構造函數使用)。
βヾ(,,・∇・,,川←那么我們具體來看一下吧!!!
原型鏈概要
prototype屬性是JavaScript為每個Function實例創建的一個對象。
具體的說:"它將通過new關鍵字創建的<對象實例>鏈接回創建它們的<構造函數>" 。就這樣,我們可以共享或繼承通用的方法和屬性。當我們在屬性查找時,就會不自覺的開啟了我們的原型鏈之旅
讓我們通過一個簡單的例子開啟我們的原型鏈查詢之旅:我們使用Array構造函數創建一個數組,然后調用join方法
我想上面的例子對於js入門者是非常簡單的,那么但是我們再來仔細了解一下,你發現join方法並沒有定義為myArray對象實例的屬性,但是我們創建的數組卻可以訪問join()方法,就好像我們本來就可以訪問似的。
join()是在哪個地方定義的呢?
事實上,我們經常使用的join(),slice(),push()...等這些內建的方法,都被定義為了Array()構造函數的prototype屬性的屬性。由於在我們創建的myArray數組中沒有找到join(),因此JavaScript會在原型鏈中查找join()方法;
其實這樣做我們很容易就聯想到了效率和重用,通過把該屬性添加到原型中去,我們所有的數組都有充分利用了相同的join()函數,而不需要為每一個數組實例都創建函數的新實例。
原型在所有的function()實例上都是標准的
我們知道創建函數兩種方法
1、調用Function構造函數法:
2、使用字面量法:
其實,即使不直接使用Function構造函數,而是使用字面量表示法,所有的函數也都是由Function()構造函數創建的
我們用字面量方法創建了一個函數,發現它的prototype和Function()構造函數一樣,都指向了object(),這也就證實了我們上所說的.
默認的prototype屬性是object()對象
上面我已經談到,實際上,原型只是一個被稱為'原型'的空對象屬性,它在JavaScript的后台已經創建,並且通過Function()構造函數來使用。
我們可以手動完成這項在后面完成的工作,以便了解它的機制。
上面的代碼非常簡單,實際上也非常好用,它實質上復制了JavaScript在后面已經完成的工作。
將構造函數創建的實例鏈接至構造函數的prototype屬性
將構造函數所創建的實例鏈接至構造函數的prototype屬性,讓我們開始這條神秘的_proto_鏈接
上面我們所原型只是一個對象,但是它是特殊的,因為原型鏈將每個實例都鏈接至其構造函數的prototype屬性。
創建的對象實例和創建對象的構造函數的prototype屬性
當然,我們除了使用_proto_鏈接,還可以使用構造函數屬性:
事實上,_proto_ === constructor.prototype
這樣我們就不難理解,下面可以達到同樣的效果:
上面的例子中我寫到直接使用鏈也是可以的,下面會介紹它的查詢順序。雖然我相信對於入門者都是使用的鏈查詢,但是我們有必然要知道它背后的那些機制。
其實有只看不見的手,在幫助着我們的代碼完成任務
原型鏈的最后是Object.prototype
那么就讓我們來看一下它的原型鏈查詢吧。
由於prototype屬性是一個對象,因此原型鏈或查詢的最后一站是Object.prototype。
我想上面的代碼,對於我們來說是絲毫不費力氣的,但就借這個簡單的例子,最后一個簡單的undefined結果,卻經歷了一段不為我們所見的原型鏈查詢;
我們創建了一個myArray空數組,然后我們試圖訪問未定義的myArray屬性時,並不會直接返回undefined,而是要經歷一段原型鏈查詢。
①在myArray對象中查找foo屬性;
如果沒有找到
②則在Array.prototype中查找該屬性;
但它在哪里也沒有定義,
③最后查找的地方就是Object.prototype
三個對象中都沒有定義,最后才給我們了一個undefined的回饋。所以請好好對待你的undefined吧,因為它的出現一波三折,還真不容易啊。。哈哈
用新對象替換prototype屬性會刪除默認的構造函數屬性
我們可以用一個新值來替換prototype屬性的默認值,但是需要特別注意的是:這么做會刪除在"預制"原型對象中找到的默認的constructor屬性,除非我們手動指定一個 ;
所以當你想要替換JavaScript設置的默認的prototype屬性(與一些js oop模式類似),應該重新連接引用該構造函數的構造函數屬性。
下面我們簡單的改一下上面的代碼,以便構造函數屬性能夠再次為適當的構造函數提供引用
繼承原型屬性的實例總是能夠獲得最新值
其實prototype是動態的繼承原型的屬性的實例總是能夠獲得最新值,
這一點比較簡單,不管是使用原型對象還是自己的對象覆蓋它,繼承原型屬性的實例總是能夠獲得新值。
但是我們需要注意下面的一點:
丨
丨
丨
用新對象替換prototype屬性不會更新以前的實例
當你想用一個新對象完全替換prototype屬性時,覺得所有的實例都會被更新,那么就即將要走向一條尋錯的道路,可能會得到意想不到的結果。
創建一個實例時,該實例將在實例化時綁定至"剛完成"的原型,提供一個新對象作為prototype屬性不會更新已創建的實例和原型之間的連接
這里的重點是,一旦開始創建實例,就不應用一個新對象那個來替換對象的原型,這樣將會導致實例有一個指向不同原型的鏈接
自定義構造函數實現原型繼承
當我們在自定義構造函數時,同樣可以實現原型繼承:
上面我們寫的例子,很好的利用原型鏈,來創建一個構造函數。如果我們不提供參數的話,構造函數則可以繼承legs和arms屬性。如果傳入參數,就遮蓋繼承的屬性
創建繼承鏈
我們自定義的構造函數實現了原型繼承,設計原型繼承的目的是要在傳統的面向對象編程語言中找到模仿繼承模式的繼承鏈。繼承只是一個對象可以訪問另一個對象的屬性。
接下來我們來創建一個簡單的繼承鏈:
事實上,上述代碼我做的僅僅是利用一個已有的原生對象。
Person()和prototype屬性的默認的object()值沒有什么不同,這也正是一個prototype屬性包含默認空object()值所發生的事情,查找用於創建對象的構造函數的原型(即object.prototype),以便查找所繼承的屬性。
::我們為什么要關注prototype屬性呢?
(希望下面可以給<JavaScript入門者>一個了解prototype的理由)
你可能不喜歡原型繼承,而是更多的喜歡采用另一種模式的對象繼承。但是:
①原生構造函數(如Ocject(),Array(),Function()...)都使用了prototype屬性,以便讓你的實例可以繼承屬性和方法。
②如果想要更好的理解JavaScript,我們需要了解JavaScript本身是如何使用prototype對象的
③當你自定義一個構造函數時,可以像JavaScript原生對象那樣使用繼承,就必須要知道他是如何工作的
④通過使用原型繼承,我們可以創建有效的對象實例。因為並非所有的數組對象都需要他們自己的join()方法<我想這就需要我們做些工作了>,但所有的實例都可以利用相同的join()方法,這就提高了效率和重用性。
寫在后面
到這里我們的函數原型屬性的深入淺出系列已經介紹完畢了,這篇博文希望可以幫助初學者--記住原型鏈層次結構的工作原理、對於易混淆的原型繼承屬性有一個分類,解決初學者心中的原型困惑
喜歡的話,關注一下吧,你的關注和支持就是給我最大的動力