在JS中,原型鏈有時候讓人覺得很胡裡花哨,又是prototype
、__proto__
又是各種指向什麼的,讓人覺得很頭疼。如果你也有這種感覺,或許這篇文章可以幫助到你
一、認識原型
1、先來一串程式碼
var Person = function(msg){
this.msg = msg;
}
var person1 = new Person("wanger")
person1.constructor===Person; //true
Person === Person.prototype.constructor; //true
person1.__proto__ === Person.prototype; //true
person1.__proto__.constructor === person1.constructor //true複製程式碼
看暈了吧?是不是很胡裡花哨?不用擔心,其實一張圖就能了明白這其中的關係:
- 藍色的是建構函式
- 綠色的是建構函式例項出來的物件
- 橙色的是建構函式的prototype,也是建構函式例項出來的物件的原型(它其實也是一個物件)
2、這裡特別要注意的是prototype
與__proto__
的區別,prototype
是函式才有的屬性,而__proto__
是每個物件都有的屬性。(__proto__
不是一個規範屬性,只是部分瀏覽器實現了此屬性,對應的標準屬性是[[Prototype]]
)。
二、認識原型鏈
1、我們剛剛瞭解了原型,那原型鏈在哪兒呢?不要著急,再上一張圖:
通過這張圖我們可以瞭解到,person1的原型鏈是:
person1 ----> Person.prototype ----> Object.prototype ----> null
2、事實上,函式也是一個物件,所以,Person的原型鏈是:
Person ----> Function.prototype ----> Object.prototype ----> null
由於Function.prototype定義了apply()等方法,因此,Person就可以呼叫apply()方法。
3、如果把原型鏈的關係都顯示清楚,那會複雜一些,如下圖:
這裡需要特別注意的是:所有函式的原型都是Function.prototype,包括
Function
建構函式和Object
建構函式(如圖中的標紅部分)
三、原型鏈的繼承
1、假設我們要基於Person擴充套件出Student,Student的構造如下:
function Student(props) {
// 呼叫Person建構函式,繫結this變數:
Person.call(this, props);
this.grade = props.grade || 1;
}複製程式碼
但是,呼叫了Person
建構函式不等於繼承了Person
,Student
建立的物件的原型是:
new Student() ----> Student.prototype ----> Object.prototype ----> null
示意圖如下所示:
必須想辦法把原型鏈修改為:
new Student() ----> Student.prototype ----> Person.prototype ----> Object.prototype ----> null
示意圖如下所示:
那我們應該怎麼修改呢?仔細觀察兩張圖的差異,我們會發現,如果我們將Student
的prototype
改成person1
物件不就大功告成了?於是有了下面的程式碼:
Student.prototype = person1 ;複製程式碼
但是這時候有個問題:
Student.prototype.constructor === Student; //false複製程式碼
原來Student.prototype
(即person1
)的constructor
指向的還是Person
,這時候還需要我們再改一下程式碼:
Student.prototype.constructor = Student;複製程式碼
這樣就能把Student的原型鏈順利的修改為: new Student() ----> Student.prototype ----> Person.prototype ----> Object.prototype ----> null 了;
完整的程式碼顯示如下:
var Person = function(msg){
this.msg = msg;
}
var Student = function(props) {
// 呼叫Person建構函式,繫結this變數:
Person.call(this, props);
this.grade = props.grade || 1;
}
var person1 = new Person("wanger")
Student.prototype = person1 ;
Student.prototype.constructor = Student;複製程式碼
三、用以上原型鏈繼承帶來的問題
1、如果在控制檯執行一遍上述的程式碼,我們會發現一些問題,如圖所示:
Student.prototype
上含有之前person1帶有的屬性,那麼,這樣的繼承的方法就顯得不那麼完美了
2、這個時候,我們可以藉助一箇中間物件來實現正確的原型鏈,這個中間物件的原型要指向Person.prototype。為了實現這一點,參考道爺(就是發明JSON的那個道格拉斯)的程式碼,中間物件可以用一個空函式F來實現:
var Person = function(msg){
this.msg = msg;
}
var Student = function(props) {
// 呼叫Person建構函式,繫結this變數:
Person.call(this, props);
this.grade = props.grade || 1;
}
// 空函式F:
function F() {
}
// 把F的原型指向Person.prototype:
F.prototype = Person.prototype;
// 把Student的原型指向一個新的F物件,F物件的原型正好指向Person.prototype:
Student.prototype = new F();
// 把Student原型的建構函式修復為Student:
Student.prototype.constructor = Student;
// 繼續在Student原型(就是new F()物件)上定義方法:
Student.prototype.getGrade = function () {
return this.grade;
};
// 建立wanger:
var wanger = new Student({
name: '王二',
grade: 9
});
wanger.msg; // '王二'
wanger.grade; // 9
// 驗證原型:
wanger.__proto__ === Student.prototype; // true
wanger.__proto__.__proto__ === Person.prototype; // true
// 驗證繼承關係:
wanger instanceof Student; // true
wanger instanceof Person; // true複製程式碼
這其中主要用到了一個空函式F作為過橋函式。為什麼道爺會用過橋函式?用過橋函式F(){}主要是為了清空構造的屬性。如果有些原Person的構造用不到,那麼過橋函式將是一個好的解決方案
這樣寫的話,Student.prototype
上就沒有任何自帶的私有屬性,這是理想的繼承的方法
3、如果把繼承這個動作用一個inherits()函式封裝起來,還可以隱藏F的定義,並簡化程式碼:
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}複製程式碼
封裝後,寫起來就像這樣:
var Person = function(msg){
this.msg = msg;
}
var Student = function(props) {
// 呼叫Person建構函式,繫結this變數:
Person.call(this, props);
this.grade = props.grade || 1;
}
inherits(Student,Person) ;複製程式碼
這樣再一封裝的話,程式碼就很完美了。
事實上,我們也可以在inherits
中使用Object.create()
來進行操作,程式碼如下:
function inherits(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}複製程式碼
如果有興趣瞭解Object.create()
的其他用法,可以參考我的這篇部落格JS中Object.create的使用方法;
四、ES6的新關鍵字class
在ES6中,新的關鍵字class,extends被正式被引入,它採用的類似java的繼承寫法,寫起來就像這樣:
class Student extends Person {
constructor(name, grade) {
super(msg); // 記得用super呼叫父類的構造方法!
this.grade = grade || 1;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}複製程式碼
這樣寫的話會更通俗易懂,繼承也相當方便。讀者可以進入廖雪峰的官方網站詳細瞭解class的用法