原型鏈一直都是一個在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();
這個操做把父類的實例賦值給子類的原型,而後結合上面原型鏈的關係圖,咱們再來理一下(爲了閱讀方便,復
制上圖到此處):
複製代碼
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,完美解決了這個問題複製代碼