深刻理解JavaScript原型鏈與繼承

原型鏈

原型鏈一直都是一個在JS中比較讓人費解的知識點,可是在面試中常常會被問到,這裏我來作一個總結吧,首先引入一個關係圖:面試

一.要理解原型鏈,首先能夠從上圖開始入手,圖中有三個概念:數組

1.構造函數: JS中全部函數均可以做爲構造函數,前提是被new操做符操做;app

function Parent(){
    this.name = 'parent';
}
//這是一個JS函數

var parent1 = new Parent()
//這裏函數被new操做符操做了,因此咱們稱Parent爲一個構造函數;
複製代碼

2.實例: parent1 接收了new Parent(),parent1能夠稱之爲實例;函數

3.原型對象: 構造函數有一個prototype屬性,這個屬性會初始化一個原型對象;優化

二.弄清楚了這三個概念,下面咱們來講說這三個概念的關係(參考上圖):this

1.經過new操做符做用於JS函數,那麼就獲得了一個實例;spa

2.構造函數會初始化一個prototype,這個prototype會初始化一個原型對象,那麼原型對象是怎麼知道本身是被哪一個函數初始化的呢?原來原型對象會有一個constructor屬性,這個屬性指向了構造函數;prototype

3.那麼關鍵來了實例對象是怎麼和原型對象關聯起來的呢?原來實例對象會有一個__proto__屬性,這個屬性指向了該實例對象的構造函數對應的原型對象;code

4.假如咱們從一個對象中去找一個屬性name,若是在當前對象中沒有找到,那麼會經過__proto__屬性一直往上找,直到找到Object對象尚未找到name屬性,才證實這個屬性name是不存在,不然只要找到了,那麼這個屬性就是存在的,從這裏能夠看出JS對象和上級的關係就像一條鏈條同樣,這個稱之爲原型鏈;cdn

5.若是看到這裏還沒理解原型鏈,能夠從下面我要說到繼承來理解,由於原型繼承就是基於原型鏈;

三.new操做符的工做原理

廢話很少說,直接上代碼
var newObj = function(func){
    var t = {}
    t.prototype = func.prototype
    var o = t
    var k =func.call(o);
    if(typeof k === 'object'){
        return k;
    }else{
        return o;
    }
}
var parent1 = newObj(Parent)等價於new操做

1.一個新對象被建立,它繼承自func.prototype。
2.構造函數func 被執行,執行的時候,相應的參數會被傳入,同時上下文(this) 會被指定爲這個新實例。
3.若是構造函數返回了一個新對象,那麼這個對象會取代整個new出來的結果,若是構造函數沒有返回對象,
那麼new出來的結果爲步驟1建立的對象。
複製代碼

繼承

一.構造函數實現繼承(構造繼承)

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//undefined

//如下代碼看完繼承方式2,再回過頭來看
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

從上面構造繼承的代碼能夠看出,構造繼承實現了繼承,
打印出來父級的name屬性,可是實例對象並無訪問到父級原型上面到屬性;
複製代碼

二.原型鏈實現繼承

function Parent(){
    this.name = 'parent'
    this.play = [1,2,3]
}
function Child(){
    this.type = 'child';
}
Child.prototype = new Parent();
Parent.prototype.id = '1';
var child1 = new Child();    
console.log(child1.name)//parent1
console.log(child1.id)//1

從這裏能夠看出,原型繼承彌補了構造繼承到缺點,繼承了原型上到屬性;
可是下面再作一個操做:
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[2,2,3]
這裏我只是改變了實例對象child1到play數組,可是實例打印實例對象child2到paly數組,發現也跟着變化
了,因此能夠得出結論,原型鏈繼承引用類型到屬性,在全部實例對象上面改變該屬性,全部實例對象該屬性都會
變化,這樣確定就存在問題,如今咱們回到繼承方式1(構造繼承),會發現構造繼承不會存在這個問題,因此
其實構造繼承和原型鏈繼承徹底能夠互補,由此咱們引入第三種繼承方式;

額外解釋:這裏經過一個原型鏈繼承,咱們再來回顧一下對原型鏈的理解,上面代碼,咱們進行了一個操做:
Child.prototype = new Parent();
這個操做把父類的實例賦值給子類的原型,而後結合上面原型鏈的關係圖,咱們再來理一下(爲了閱讀方便,復
制上圖到此處):
複製代碼

如今咱們能夠把圖中到實例當作child1,首先若是要找child1實例對象中的name屬性,那麼我首先到Child自己去找,發現沒有找到name屬性,由於Child函數裏面只有一個type屬性,那麼經過__proto__找到Child的原型對象,而剛纔咱們作了一個操做:

Child.prototype = new Parent(); 這個操做把父類的實例給了Child的原型,因此經過這個咱們就能夠找到父級的name,這就是原型鏈,一層一層的,像一個鏈條;

三.組合繼承

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Child.prototype = new Parent();
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//1
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

從上面代碼能夠看出,組合繼承就是把構造繼承和原型鏈繼承組合在一塊兒,把他們的優點互補,從而彌補了各自的
缺點;那麼組合繼承就完美了嗎?咱們繼續思考,從代碼中能夠發現,咱們調用了兩次Parent函數,一次是
new Parent(),一次是Parent.call(this),是否能夠優化呢?咱們引入第四種繼承方式;
複製代碼

四.組合繼承(優化1)

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Child.prototype = Parent.prototype;//這裏改變了
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//1
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

咱們改爲Child.prototype = Parent.prototype,這樣就只調用一次Parent了,解決了繼承方式3的問題,
好吧,咱們繼續思考,這樣就沒有問題了嗎,咱們作以下操做:
console.log(Child.prototype.constructor)//Parent
這裏咱們打印發現Child的原型的構造器成了Parent,按照咱們的理解應該是Child,這就形成了構造器紊亂,
因此咱們引入第五種繼承優化
複製代碼

五.組合繼承(優化2)

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child//這裏改變了
Parent.prototype.id = '1'
var child1 = new Child()
console.log(child1.name)//parent1
console.log(child1.id)//1
var child2 = new Child();
child1.play[0] = 2;
console.log(child2.play)//[1,2,3]

如今咱們打印
console.log(Child.prototype.constructor)//Child
這裏就解決了問題,可是咱們繼續打印
console.log(Parent.prototype.constructor)//Child
發現父類的構造器也出現了紊亂,全部咱們經過一箇中間值來解決這個問題,最終版本爲:

function Parent(){
    this.name = 'parent1'
    this.play = [1,2,3]
}
    
function Child{
    Parent.call(this);//apply
    this.type = 'parent2';
}

var obj = {};
obj.prototype = Parent.prototype;
Child.prototype = obj;
//上面三行代碼也能夠簡化成Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child

console.log(Child.prototype.constructor)//Child
console.log(Parent.prototype.constructor)//Parent
用一箇中間obj,完美解決了這個問題複製代碼
相關文章
相關標籤/搜索