【寒暄】好久沒有更新博客了,說來話長,因爲我下定決心要從一個後臺程序員轉爲Front End,其間走過了一段漫長而艱辛的時光,今天跟大家分享下自己對javascript中原型鏈繼承的理解。
總的說來,js中的常用的繼承方式可以分爲兩種,一種是原型鏈式繼承,這也是本文要談的重點;另外一種是借用構造函數繼承,這方面的理解,我將在下次的博客中更新。好了,閒話不多說,進入正題。
一,關於原型
function Person(){}//這裏我們聲明一個函數Person,js中函數是對象,也是構造函數
console.log(Person.prototype)//打印一下Person對象的原型,會出現什麼呢?如下圖所示:
大家在圖中看到了,Person對象的原型擁有一個constructor,它指向Person的構造函數,即Person本身,另外一個屬性是__proto__屬性,這個屬性我會在後文中說明。
到這裏,大家肯定會明白了,一個對象建立後,會產生一個局部的“小鏈式結構”,即Person對象擁有一個prototype屬性,這個屬性指向原型對象,在原型對象中又有一個構造器constructor,指向構造函數。用一張圖來說明:
那麼,原型對象的作用是什麼呢?這個原型對象包含由特定類型的實例共享的屬性和方法。大家要注意共享這兩個字,用一段代碼解釋下
function Person(){
this.name="bob" //這是一個實例屬性
}
Person.prototype.eat=function(){ //給對象的原型對象添加一個eat的方法,接下來,new的實例會共享這個方法
return "food";
}
var p1=new Person(); //這裏究竟發生生了什麼?
p1.eat()//->food
var p2=new Person();
p2.eat()//->food,所以只要是Person的對象,他們都會共享原型對象的方法,當然,p1.name也會共享Person的實例屬性,因爲p1是Person的一個實例
好了,到這裏原型的概念我們已經講完了,大家或許會疑問,上面的new一個Person實例的過程中究竟發生了什麼呢?爲什麼這個實例能夠訪問到原型對象中的方法?其實,在這個過程過程中,p1實例擁有了一個指針,這個指針指向構造函數的原型對象。此時原型對象中的方法自然能夠被實例所訪問。用一張圖來說明下:
這裏,我們總結下構造函數、原型和實例的關係:每個構造函數都有一個原型對象,原型對象擁有一個指向構造函數的指針,而實例擁有一個指向原型對象的內部指針(這就是前面所提到的[[Prototype]],即__proto__,要注意的是這個__proto__屬性在chrome瀏覽器中是可以看到的,而在大部分瀏覽器是隱藏的!)
二,關於原型鏈繼承
好了,說了這麼多終於到回到我們的主角了【原型鏈】,提出一個思考:如果我們讓原型對象等於另外一個對象的實例,將會有一個什麼樣的結果呢?先看下面一段代碼
function Person(){
this.name="bob";
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
Student.prototype=new Person();//將Person實例賦給Student的原型對象
var one=new Student();
one.name//bob
one.eat()//food,Student的實例能訪問到Person對象的實例方法,也能訪問到其原型屬性中的方法
以上就是原型鏈繼承的一種基本模式,那麼我們怎麼解釋這樣的原理呢?之前說過,對象的實例擁有一個指向原型對象的指針,那麼student的原型對象擁有了Person對象實例後,自然也擁有一個指向Person原型對象的指針。此時,我們再new一個Student實例one時,one實例包含一個指向Student原型的指針,而Student.prototype擁有一個指向Person原型對象的指針,Person原型本身包含一個指向自身構造函數的指針。這樣一來,就構成了實例與原型的鏈條。這就是所謂的原型鏈的概念!
用一張圖描繪一下上面講的情況:
大家要注意一下,這裏的one對象的constructor現在指向誰呢?它並不指向Student,因爲Student的原型指向另一個對象--Person的原型,而這個原型對象的constructor指向的是Person。
三,原型鏈方法的改寫及注意的問題
function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
//Student.prototype.eat=function(){
// return "food1";
//}
//注意如果更改原型語句的代碼放在替換之前,那麼下面one.eat()的結果將仍然是food
//,原因很簡單,前面對prototype對象的修改,在後面的替換一句中被Person實例對象覆蓋了
//,換句話說,就是現在的prototype實例中仍舊是以前的eat方法
Student.prototype=new Person();
Student.prototype.eat=function(){
return "food1";
}
var one=new Student();
console.log(one.eat());//food1
但是大家要注意一下一種情況,在通過原型鏈繼承時,不能通過對象字面量個方式來更新原型對象function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
Student.prototype=new Person();
Student.prototype={
run:function(){
return "run";
}
};
var one=new Student();
console.log(one.eat());//Uncaught TypeError: undefined is not a function
在上面的代碼中,把Person的實例賦給Student的原型,接下來又把原型改寫成另一個對象字面量,現在原型包含的是Object實例,不再是Person實例,因此原型鏈已經被切斷了,也就是說Student和Person沒關係了。function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
Student.prototype=new Person();
Student.prototype.constructor=Student;//把Student原型對象中原本指向Person構造函數的對象強行指向到Student
var one=new Student();
console.log(one.eat());//food
從代碼運行的情況來看,這個動作並沒有切斷原型鏈的繼承,原因何在?四,如何確定原型和實例關係
function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
Student.prototype=new Person();
Student.prototype.constructor=Student;
var one=new Student();
var person=new Person();
console.log(one instanceof Student);//true
console.log(one instanceof Person);//true
console.log(person instanceof Person);//true
console.log(person instanceof Student);//false
最後一個出現了false,什麼原因,instanceof的工作是什麼呢?function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
Student.prototype=new Person();
var one=new Student();
var person=new Person();
console.log(Student.prototype.isPrototypeOf(one));//true
console.log(Person.prototype.isPrototypeOf(one));//true
console.log(Person.prototype.isPrototypeOf(person));//true
console.log(Student.prototype.isPrototypeOf(person));//false