您的位置:首頁>正文

這一次,徹底搞懂JavaScript中的繼承

你應該知道, 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的“繼承”與“原型鏈”的理解更加深刻, 所以務必保障你已經搞清楚了他們之間的關係。 (如果沒有, 務必多看幾遍, 你可以找張紙寫寫畫畫, 我第一次就是這麼做的)

推薦下我的前端群:524262608, 不管你是小白還是大牛, 小編我都歡迎, 不定期分享乾貨, 包括我自己整理的一份前端資料和零基礎入門教程, 歡迎初學者和在進階中的小夥伴。

徹底搞清楚了?那讓我們繼續我們的主題“繼承”。

你是否覺得奇怪,

為什麼我們的實例物件可以訪問到構造函數原型屬性上的屬性(真是拗口)?答案是因為“每一個物件自身都擁有一個隱式的[[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 = Barvar o = new Bar(1)o.sayX() // 10o.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 = Barvar o = new Bar(1)o.sayX() // 1o.sayY() // 1o.sayZ() // 1

Done!我們成功竊取了構造函數Foo的兩個自有屬性,構造函數Bar的實例物件現在也有了x和y的值!

推薦下我的前端群:524262608,不管你是小白還是大牛,小編我都歡迎,不定期分享乾貨,包括我自己整理的一份前端資料和零基礎入門教程,歡迎初學者和在進階中的小夥伴。

雖然答案已經一目了然了,但還是讓我再解釋一下這是怎麼做到的:首先我們知道構造函數也是函數,因此我們可以像普通函數一樣調用他,讓我們以單純的函數視角看待構造函數Foo,它不過是往this所指的物件上添加了兩個屬性,然後返回了undefined值,當我們單純調用該函數時,this的指向為window(不明白為什麼指向window,你可以閱讀我的這篇文章)。但是通過call()和apply()函數,我們可以人為的改變函數內this指標的指向,所以我們將構造函數內的this傳入call()函數中,奇妙的事情發生了,原先為Foo函數實例物件添加的屬性現在添加到了Bar函數的實例物件上!

作者:浮客

原文:http://www.cnblogs.com/libinfs/archive/2017/11/23/7885337.html

幾乎所有函數都擁有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 = Barvar o = new Bar(1)o.sayX() // 10o.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 = Barvar o = new Bar(1)o.sayX() // 1o.sayY() // 1o.sayZ() // 1

Done!我們成功竊取了構造函數Foo的兩個自有屬性,構造函數Bar的實例物件現在也有了x和y的值!

推薦下我的前端群:524262608,不管你是小白還是大牛,小編我都歡迎,不定期分享乾貨,包括我自己整理的一份前端資料和零基礎入門教程,歡迎初學者和在進階中的小夥伴。

雖然答案已經一目了然了,但還是讓我再解釋一下這是怎麼做到的:首先我們知道構造函數也是函數,因此我們可以像普通函數一樣調用他,讓我們以單純的函數視角看待構造函數Foo,它不過是往this所指的物件上添加了兩個屬性,然後返回了undefined值,當我們單純調用該函數時,this的指向為window(不明白為什麼指向window,你可以閱讀我的這篇文章)。但是通過call()和apply()函數,我們可以人為的改變函數內this指標的指向,所以我們將構造函數內的this傳入call()函數中,奇妙的事情發生了,原先為Foo函數實例物件添加的屬性現在添加到了Bar函數的實例物件上!

作者:浮客

原文:http://www.cnblogs.com/libinfs/archive/2017/11/23/7885337.html

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示