摘要:這正是我們想要的太棒了毫不意外的,這種繼承的方式被稱為構造函數繼承,在中是一種關鍵的實現的繼承方法,相信你已經很好的掌握了。
你應該知道,JavaScript是一門基于原型鏈的語言,而我們今天的主題 -- “繼承”就和“原型鏈”這一概念息息相關。甚至可以說,所謂的“原型鏈”就是一條“繼承鏈”。有些困惑了嗎?接著看下去吧。
一、構造函數,原型屬性與實例對象要搞清楚如何在JavaScript中實現繼承,我們首先要搞懂構造函數,原型屬性與實例對象三者之間的關系,讓我們先看一段代碼:
function Person(name, age) { var gender = girl // ① this.name = name // ② this.age = age } // ③ Person.prototype.sayName = function() { alert(this.name) } // ④ var kitty = new Person("kitty", 14) kitty.sayName() // kitty
讓我們通過這段代碼澄清幾個概念:
Person是一個“構造函數”(它用來“構造”對象,并且是一個函數),①處gender是該構造函數的“私有屬性”,②處的語句定義了該構造函數的“自有屬性”;
③處的prototype是Person的“原型對象”(它是實例對象的“原型”,同時它是一個對象,但同時它也是構造函數的“屬性”,所以也有人稱它為“原型屬性”),該對象上定義的所有屬性(和方法)都會被“實例對象”所“繼承”(我們終于看到這兩個字了,但是不要心急,我們過一會才會談論它);
④處的變量“kitty”的值是構造函數Person的“實例對象”(它是由構造函數生成的一個實例,同時,它是一個對象),它可以訪問到兩種屬性,一種是通過構造函數生成的“自有屬性”,一種是原型對象可以訪問的所有屬性;
對以上這些概念有清楚的認識,才能讓你對JavaScript的“繼承”與“原型鏈”的理解更加深刻,所以務必保障你已經搞清楚了他們之間的關系。(如果沒有,務必多看幾遍,你可以找張紙寫寫畫畫,我第一次就是這么做的)
徹底搞清楚了?那讓我們繼續我們的主題 -- “繼承”。
你是否覺得奇怪,為什么我們的實例對象可以訪問到構造函數原型屬性上的屬性(真是拗口)?答案是因為“每一個對象自身都擁有一個隱式的[[proto]]屬性,該屬性默認是一個指向其構造函數原型屬性的指針”(其實我想說它是一個鉤子,在對象創建時默認“勾住”了其構造函數的原型屬性,但是我發現emoji居然沒有鉤子的圖標,所以...???♂?,不過我還是覺得鉤子更形象些...)。
當JavaScript引擎發現一個對象訪問一個屬性時,會首先查找對象的“自有屬性”,如果沒有找到則會在[[proto]]屬性指向的原型屬性中繼續查找,如果還沒有找到的話,你知道其實原型屬性也是一個對象,所以它也有一個隱式的[[proto]]屬性指向它的原型屬性...,正如你所料,如果一直沒有找到該屬性,JavaScript引擎會一直這樣找下去,直到找到最頂部構造函數Object的prototype原型屬性,如果還是沒有找到,會返回一個undefined值。這個不斷查找的過程,有一個形象生動的名字“攀爬原型鏈”。
現在你應該對“原型鏈”就是“繼承鏈”這一說法有點感覺了吧,讓我們暫時休息一下,對兩個我們遺漏的知識點補充說明:
隱式的[[proto]]屬性
原型對象prototype
(一)隱式的[[proto]]屬性何為“隱式屬性”呢?即是開發者無法訪問卻確實存在的屬性,你可能會問,既然是隱式的,如何證明它的存在呢?問得好,答案是雖然JavaScript語言沒有暴露給我們這個屬性,但是瀏覽器卻幫助我們可以獲取到該屬性,在Chorme中,我們可以通過瀏覽器為對象添加的_proto_屬性訪問到[[proto]]的值。你可以自己試試在控制臺中打印這個屬性,證明我沒有說謊。
(二)原型對象prototype還記的我們之前提到JavaScript世界一條重要的概念嗎?“每一個對象自身都擁有一個隱式的[[proto]]屬性,該屬性默認是一個指向其構造函數原型屬性的指針”。其實與其對應的,還有一條重要的概念我需要在這里告訴你“幾乎所有函數都擁有prototype原型屬性”。這兩個概念確實非常重要,因為每當你搞混了構造函數,原型屬性,實例對象之間的關系,以及JavaScript世界中的繼承規則時,想想這兩個概念總能幫助你剝離迷霧,重新發現真相。
(三)JavaScript世界兩個重要概念因為他們真的很重要,所以我特別使用一個藍色開頭的列表再寫一遍(保持耐心,朋友!)
每一個對象自身都擁有一個隱式的[[proto]]屬性,該屬性默認是一個指向其構造函數原型屬性的指針;
幾乎所有函數都擁有prototype原型屬性;
至此,我們搞清楚了構造函數,原型屬性與實例對象三者的關系,相信我,理解清楚這三者的關系能讓你以更清晰的視角去觀察JavaScript的繼承世界,而在下一章中,我們將更進一步,直奔主題的闡述在JavaScript世界中如何實現繼承,當然,還有背后的原理。
二、在JavaScript世界中實現繼承既然說了要直奔主題,我們便直接開始對JavaScript世界中對象的繼承方式展開說明。不過在那之前,讓我們再統一我們對“繼承”這一概念的認識:即我們想要一個對象能夠訪問另一個對象的屬性,同時,這個對象還能夠添加自己新的屬性或是覆蓋可訪問的另一個對象的屬性,我們實現這個目標的方式叫做“繼承”。
而在JavaScript世界,實現繼承的方式有以下兩種:
創建一個對象并指定其繼承對象(原型對象);
修改構造函數的原型屬性(對象);
看起來很合乎邏輯對吧,我們能夠針對“對象”,令一個對象繼承另一個對象,也能夠轉而針對創建對象的“構造函數”,以實現實例對象的繼承。但是這里有個陷阱(你可能注意到了),對于一個已經定義的對象,我們無法再改變其繼承關系,我們的第一種方式只能在“創建對象時”定義對象的繼承對象。這是為什么呢?答案是因為“我們設置一個對象的繼承關系,本質上是在操作對象隱式的[[proto]]屬性”,而JavaScript只為我們開通了在對象創建時定義[[proto]]屬性的權限,而拒絕讓我們在對象定義時再修改或訪問這一屬性(所以它是“隱式”的)。很遺憾,在對象定義后改變它的繼承關系確實是不可能的。
好了,是時候看看JavaScript世界中繼承的主角了 -- Object.create()
(一)關于Object.create() 和對象繼承
正如之前所說,Object.create()函數是JavaScript提供給我們的一個在創建對象時設置對象內部[[proto]]屬性的API,相信你已經清楚的知道了,通過修改[[proto]]屬性的值,我們就能決定對象所繼承的對象,從而以我們想要的方式實現繼承。
讓我們細致的了解一下Object.create()函數:
var x = { name: "tom", sayName: function() { console.log(this.name) } } var y = Object.create(x, { name: { configurable: true, enumerable: true, value: "kitty", writable: true, } }) y.sayName() // "kitty"
看到了嗎,Object.create()函數接收兩個參數,第一個參數是創建對象想要繼承的原型對象,第二個參數是一個屬性描述對象(不知道什么是屬性描述對象?看看我之前的這篇文章),然后會返回一個對象。
讓我們談談在調用Object.create()時究竟發生了什么:
創建了一個空對象,并賦值給相應變量;
將第一個參數對象設置為該對象[[proto]]屬性的值;
在該對象上調用defineProperty()方法,并將第二個參數傳入該方法中;
相信到這里你已經完全明白了如何在創建對象時實現繼承了,但這樣的方法有很多局限,比如我們只能在創建對象時設置對象的繼承對象,又比如這種設置繼承的方式是一次性的,我們永遠無法依靠這種方式創造出多個有相同繼承關系的對象,而對于這種情況,我們理所當然的要請出我們的第二個主角 -- prototype原型對象。
(二)關于prototype 和構造函數繼承還記得我們之前反復提及構造函數,原型屬性與實例對象的關系吧?我們還強調了“幾乎所有的函數都擁有prototype屬性”,現在就是應用這些知識的時候了,其實說到繼承,構造函數生產實例對象的過程本身就是一種天然的繼承。實例對象天然的繼承著原型對象的所有屬性,這其實是JavaScript提供給開發者第二種(也是默認的)設置對象[[proto]]屬性的方法。
但是這種”天然的“繼承方式缺點在于只存在兩層繼承:自定義構造函數的prototype對象繼承Object構造函數的prototype屬性,構造函數的實例對象繼承構造函數的prototype屬性。而我們有時想要更加靈活,滿足需求,甚至是”更長“的原型鏈(或者說是”繼承鏈“)。這是JavaScript默認的繼承模式下無法實現的,但解決方式也很符合直覺,既然我們無法修改對象的[[proto]]屬性,我們就去修改[[proto]]屬性指向的對象 -- 原型對象。
我們說過原型對象也是一個對象對吧?所以我們就有了以下操作:
function Foo(x, y) { this.x = x this.y = y } Foo.prototype.sayX = function() { console.log(this.x) } Foo.prototype.sayY = function() { console.log(this.y) } function Bar(z) { this.z = z this.x = 10 } Bar.prototype = Object.create(Foo.prototype) // 注意這里 Bar.prototype.sayZ = function() { console.log(this.z) } Bar.prototype.constructor = Bar var o = new Bar(1) o.sayX() // 10 o.sayZ() // 1
相信你注意到了,我通過修改了構造函數Bar的原型屬性,將其值設置為一個繼承對象為Foo.prototype的空對象,在之后,我又為在該對象添加了一些屬性(注意到我添加的constructor屬性了嗎?如果你不明白為什么,你應該去了解一下我這么做的理由。)和方法。這樣,構造函數Bar的實例對象就會在查詢屬性時攀爬原型鏈,從自有屬性開始,途徑Bar.prototype,Foo.prototype,最終到達Object.prototype。這正是我們想要的!太棒了!
毫不意外的,這種繼承的方式被稱為”構造函數繼承“,在JavaScript中是一種關鍵的實現的繼承方法,相信你已經很好的掌握了。
但是慢著,還有一個問題沒有解決,讓我們回到剛才的代碼,看看如果我們在源代碼上添加一條o.sayY()會發生什么?答案是控制臺會輸出undefined。
毫不意外對吧,畢竟我們從來都沒有定義過y屬性。但是假如我們也想讓構造函數Bar的實例對象擁有構造函數Foo的設置的自有屬性又該怎么辦呢?答案是通過”構造函數竊取“技術,這將是我們下一章也是最后一章要討論的話題。
(三)構造函數竊取如果”竊取“所繼承的構造函數的自有屬性呢?答案是巧妙的使用.call()和.apply()方法,讓我們修改一下之前的代碼:
function Foo(x, y) { this.x = x this.y = y } Foo.prototype.sayX = function() { console.log(this.x) } Foo.prototype.sayY = function() { console.log(this.y) } function Bar(z) { this.z = z this.x = 10 Foo.call(this, z, z) // 注意這里 } Bar.prototype = Object.create(Foo.prototype) Bar.prototype.sayZ = function() { console.log(this.z) } Bar.prototype.constructor = Bar var o = new Bar(1) o.sayX() // 1 o.sayY() // 1 o.sayZ() // 1
Done!我們成功竊取了構造函數Foo的兩個自有屬性,構造函數Bar的實例對象現在也有了x和y的值!
雖然答案已經一目了然了,但還是讓我再解釋一下這是怎么做到的:首先我們知道構造函數也是函數,因此我們可以像普通函數一樣調用他,讓我們以單純的函數視角看待構造函數Foo,它不過是往this所指的對象上添加了兩個屬性,然后返回了undefined值,當我們單純調用該函數時,this的指向為window(不明白為什么指向window,你可以閱讀我的這篇文章)。但是通過call()和apply()函數,我們可以人為的改變函數內this指針的指向,所以我們將構造函數內的this傳入call()函數中,奇妙的事情發生了,原先為Foo函數實例對象添加的屬性現在添加到了Bar函數的實例對象上!
“構造函數竊取”,我喜歡“竊取”這兩個字,確實很巧妙。
太棒了 你終于看完了這篇文章,是否徹底搞懂JavaScript中的繼承了呢?希望如此。
算是個獎勵,我之前有將JavaScript中的繼承知識總結為一張思維導圖,你可以點擊這里查看。知識總是反復記憶才能真正掌握,希望你能常回來看看。加油? !
? Hey!喜歡這篇文章嗎?別忘了在下方? 點贊讓我知道。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/90040.html
摘要:一有和無在圖中,值的六種類型用藍底色的矩形表示。想一下在語言中,根本沒有布爾類型,通常用來表示邏輯真假的正是整數和。根據圖,需要將布爾類型轉為數字類型,而轉為數字的結果是,所以表達式變為兩個操作數變成了對象類型數字類型。 大家知道,==是JavaScript中比較復雜的一個運算符。它的運算規則奇怪,容易讓人犯錯,從而成為JavaScript中最糟糕的特性之一。 在仔細閱讀了ECMASc...
摘要:對應于上述的,等。匹配到的子字符串在原字符串中的偏移量。插入當前匹配的子串右邊的內容。 javascript這門語言一直就像一位帶著面紗的美女,總是看不清,摸不透,一直專注服務器端,也從來沒有特別重視過,直到最近幾年,javascript越來越重要,越來越通用。最近和前端走的比較近,借此機會,好好鞏固一下相關知識點。 1.初識replace 在js中有兩個replace函數 一個是lo...
摘要:徹底搞懂執行機制首先我們大家都了解的是,是一門單線程語言,所以我們就可以得出是按照語句順序執行的首先看這個顯然大家都知道結果,依次輸出,然而換一種這個時候再看代碼的順序執行,輸出,,,。不過即使主線程為空,也是達不到的,根據標準,最低是。 徹底搞懂JavaScript執行機制 首先我們大家都了解的是,JavaScript 是一門單線程語言,所以我們就可以得出: JavaScript 是...
摘要:從學習前端以來,屬性和特性已困惑我很久。注意啦,屬性和特性的一個關系出現了。以下除外屬性表明節點的類型。與之前了解到的特性用來描述屬性的行為并無出入。下面,我們一起來看看屬性和特性是如何訪問的。操作特性的方法主要有三個,分別是和。 從學習前端以來,屬性和特性已困惑我很久。今天使用jQuery時,又踩到了這個坑。于是下定決心,徹底搞懂它。 一、對象、屬性和特性的關系 先來看一下詞典的解釋...
摘要:從學習前端以來,屬性和特性已困惑我很久。注意啦,屬性和特性的一個關系出現了。以下除外屬性表明節點的類型。與之前了解到的特性用來描述屬性的行為并無出入。下面,我們一起來看看屬性和特性是如何訪問的。操作特性的方法主要有三個,分別是和。 從學習前端以來,屬性和特性已困惑我很久。今天使用jQuery時,又踩到了這個坑。于是下定決心,徹底搞懂它。 一、對象、屬性和特性的關系 先來看一下詞典的解釋...
閱讀 3355·2021-10-13 09:40
閱讀 2592·2021-10-08 10:17
閱讀 3995·2021-09-28 09:45
閱讀 929·2021-09-28 09:35
閱讀 1812·2019-08-30 10:51
閱讀 2903·2019-08-26 12:11
閱讀 1649·2019-08-26 10:41
閱讀 3096·2019-08-23 17:10