在JS中经柴,原型鏈有時候讓人覺得很胡里花哨挟憔,又是prototype钟些、proto又是各種指向什么的,讓人覺得很頭疼绊谭。如果你也有這種感覺政恍,或許這篇文章可以幫助到你
一、認(rèn)識原型
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
看暈了吧篙耗?是不是很胡里花哨?不用擔(dān)心宪赶,其實一張圖就能了明白這其中的關(guān)系:
- 藍(lán)色的是構(gòu)造函數(shù)
- 綠色的是構(gòu)造函數(shù)實例出來的對象
- 橙色的是構(gòu)造函數(shù)的prototype,也是構(gòu)造函數(shù)實例出來的對象的原型(它其實也是一個對象)
2宗弯、這里特別要注意的是prototype
與__proto__
的區(qū)別,prototype
是函數(shù)才有的屬性搂妻,而__proto__
是每個對象都有的屬性蒙保。(__proto__
不是一個規(guī)范屬性,只是部分瀏覽器實現(xiàn)了此屬性,對應(yīng)的標(biāo)準(zhǔn)屬性是[[Prototype]]
)。
二震捣、認(rèn)識原型鏈
1、我們剛剛了解了原型,那原型鏈在哪兒呢懈糯?不要著急涤妒,再上一張圖:
通過這張圖我們可以了解到,person1的原型鏈?zhǔn)牵?/p>
person1 ----> Person.prototype ----> Object.prototype ----> null
2、事實上赚哗,函數(shù)也是一個對象她紫,所以硅堆,Person的原型鏈?zhǔn)牵?/p>
Person ----> Function.prototype ----> Object.prototype ----> null
由于Function.prototype定義了apply()等方法,因此贿讹,Person就可以調(diào)用apply()方法渐逃。
3、如果把原型鏈的關(guān)系都顯示清楚民褂,那會復(fù)雜一些茄菊,如下圖:
這里需要特別注意的是:所有函數(shù)的原型都是Function.prototype,包括
Function
構(gòu)造函數(shù)和Object
構(gòu)造函數(shù)(如圖中的標(biāo)紅部分)
三、原型鏈的繼承
1赊堪、假設(shè)我們要基于Person擴(kuò)展出Student面殖,Student的構(gòu)造如下:
function Student(props) {
// 調(diào)用Person構(gòu)造函數(shù),綁定this變量:
Person.call(this, props);
this.grade = props.grade || 1;
}
但是哭廉,調(diào)用了Person
構(gòu)造函數(shù)不等于繼承了Person
脊僚,Student
創(chuàng)建的對象的原型是:
new Student() ----> Student.prototype ----> Object.prototype ----> null
示意圖如下所示:
必須想辦法把原型鏈修改為:
new Student() ----> Student.prototype ----> Person.prototype ----> Object.prototype ----> null
示意圖如下所示:
那我們應(yīng)該怎么修改呢?仔細(xì)觀察兩張圖的差異遵绰,我們會發(fā)現(xiàn)辽幌,如果我們將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) {
// 調(diào)用Person構(gòu)造函數(shù),綁定this變量:
Person.call(this, props);
this.grade = props.grade || 1;
}
var person1 = new Person("wanger")
Student.prototype = person1 ;
Student.prototype.constructor = Student;
三赎离、用以上原型鏈繼承帶來的問題
1逛犹、如果在控制臺執(zhí)行一遍上述的代碼,我們會發(fā)現(xiàn)一些問題梁剔,如圖所示:
Student.prototype
上含有之前person1帶有的屬性虽画,那么,這樣的繼承的方法就顯得不那么完美了
2荣病、這個時候码撰,我們可以借助一個中間對象來實現(xiàn)正確的原型鏈,這個中間對象的原型要指向Person.prototype个盆。為了實現(xiàn)這一點脖岛,參考道爺(就是發(fā)明JSON的那個道格拉斯)的代碼,中間對象可以用一個空函數(shù)F來實現(xiàn):
var Person = function(msg){
this.msg = msg;
}
var Student = function(props) {
// 調(diào)用Person構(gòu)造函數(shù)颊亮,綁定this變量:
Person.call(this, props);
this.grade = props.grade || 1;
}
// 空函數(shù)F:
function F() {
}
// 把F的原型指向Person.prototype:
F.prototype = Person.prototype;
// 把Student的原型指向一個新的F對象柴梆,F(xiàn)對象的原型正好指向Person.prototype:
Student.prototype = new F();
// 把Student原型的構(gòu)造函數(shù)修復(fù)為Student:
Student.prototype.constructor = Student;
// 繼續(xù)在Student原型(就是new F()對象)上定義方法:
Student.prototype.getGrade = function () {
return this.grade;
};
// 創(chuàng)建wanger:
var wanger = new Student({
name: '王二',
grade: 9
});
wanger.msg; // '王二'
wanger.grade; // 9
// 驗證原型:
wanger.__proto__ === Student.prototype; // true
wanger.__proto__.__proto__ === Person.prototype; // true
// 驗證繼承關(guān)系:
wanger instanceof Student; // true
wanger instanceof Person; // true
這其中主要用到了一個空函數(shù)F作為過橋函數(shù)。為什么道爺會用過橋函數(shù)终惑?用過橋函數(shù)F(){}主要是為了清空構(gòu)造的屬性绍在。如果有些原Person的構(gòu)造用不到,那么過橋函數(shù)將是一個好的解決方案
這樣寫的話,Student.prototype
上就沒有任何自帶的私有屬性偿渡,這是理想的繼承的方法
3臼寄、如果把繼承這個動作用一個inherits()函數(shù)封裝起來,還可以隱藏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) {
// 調(diào)用Person構(gòu)造函數(shù),綁定this變量:
Person.call(this, props);
this.grade = props.grade || 1;
}
inherits(Student,Person) ;
這樣再一封裝的話适揉,代碼就很完美了留攒。
事實上,我們也可以在inherits
中使用Object.create()
來進(jìn)行操作涡扼,代碼如下:
function inherits(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
}
如果有興趣了解Object.create()
的其他用法稼跳,可以參考我的這篇博客JS中Object.create的使用方法;
四、ES6的新關(guān)鍵字class
在ES6中吃沪,新的關(guān)鍵字class汤善,extends被正式被引入,它采用的類似java的繼承寫法票彪,寫起來就像這樣:
class Student extends Person {
constructor(name, grade) {
super(msg); // 記得用super調(diào)用父類的構(gòu)造方法!
this.grade = grade || 1;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
這樣寫的話會更通俗易懂红淡,繼承也相當(dāng)方便。讀者可以進(jìn)入廖雪峰的官方網(wǎng)站詳細(xì)了解class的用法
參考文獻(xiàn):廖雪峰的官方網(wǎng)站
原文地址:王玉略的個人網(wǎng)站