繼承是面向?qū)ο笾幸粋€比較核心的概念者疤。其他正統(tǒng)面向?qū)ο笳Z言都會用兩種方式實現(xiàn)繼承:一個是接口實現(xiàn),一個是繼承叠赦。而ECMAScript只支持繼承驹马,不支持接口實現(xiàn)革砸,而實現(xiàn)繼承的方式依靠原型鏈完成。
)原型鏈繼承
function Box() {//Box構造
this.name = 'xiaoming';
}
function Desk() {//Desk構造
this.age = 100;
}
Desk.prototype = new Box();//Desc繼承了Box糯累,通過原型算利,形成鏈條
var desk = new Desk();
alert(desk.age); ??????????????????????????//100
alert(desk.name);//xiaoming得到被繼承的屬性
function Table() {//Table構造
this.level = 'AAAAA';
}
Table.prototype = new Desk();//繼續(xù)原型鏈繼承
var table = new Table();
alert(table.name);//繼承了Box和Desk
如果要實例化table,那么Desk實例中有age=100泳姐,原型中增加相同的屬性age=200效拭,最后結(jié)果是多少呢?
Desk.prototype.age = 200;//實例和原型中均包含age
PS:以上原型鏈繼承還缺少一環(huán)胖秒,那就是Obejct缎患,所有的構造函數(shù)都繼承自Obejct。而繼承Object是自動完成的阎肝,并不需要程序員手動繼承挤渔。
經(jīng)過繼承后的實例,他們的從屬關系會怎樣呢风题?
alert(table instanceof Object);//true
alert(desk instanceof Table);//false蚂蕴,desk是table的超類
alert(table instanceof Desk);//true
alert(table instanceof Box);//true
在JavaScript里,被繼承的函數(shù)稱為超類型(父類俯邓,基類也行骡楼,其他語言叫法),繼承的函數(shù)稱為子類型(子類稽鞭,派生類)鸟整。繼承也有之前問題,比如字面量重寫原型會中斷關系朦蕴,使用引用類型的原型篮条,并且子類型還無法給超類型傳遞參數(shù)。
原型鏈繼承的缺陷:
1吩抓、 無法從子類中調(diào)用父類的構造函數(shù)涉茧, 這樣就沒有辦法把子類中屬性賦值給父類。
2疹娶、 父類中屬性是在子類的原型中的伴栓,這違背了我們前面所講的封裝的理念( 屬性在對象中,方法在原型中)雨饺, 會出現(xiàn)前面值的混淆問題钳垮。
2)對象冒充的方式繼承
為了解決引用共享和超類型無法傳參的問題,我們采用一種叫借用構造函數(shù)的技術额港,或者成為對象冒充(偽造對象饺窿、經(jīng)典繼承)的技術來解決這兩種問題。
function Box(age) {
this.name = ['xiaoming', 'Jack', 'Hello']
this.age = age;
}
function Desk(age) {
Box.call(this, age);//對象冒充移斩,給超類型傳參
}
var desk = new Desk(200);
alert(desk.age);
alert(desk.name);
desk.name.push('AAA');//添加的新數(shù)據(jù)绢馍,只給desk
alert(desk.name);
3)組合繼承
借用構造函數(shù)雖然解決了剛才兩種問題,但沒有原型肠套,復用則無從談起痕貌。所以入宦,我們需要原型鏈+借用構造函數(shù)的模式哺徊,這種模式成為組合繼承。
function Box(age) {
this.name = ['xiaoming', 'Jack', 'Hello']
this.age = age;
}
Box.prototype.run = function () {
return this.name + this.age;
};
function Desk(age) {
Box.call(this, age);//對象冒充
}
Desk.prototype = new Box();//原型鏈繼承
var desk = new Desk(100);
alert(desk.run());
組合式繼承是JavaScript最常用的繼承模式乾闰;但落追,組合式繼承也有一點小問題,就是超類型在使用過程中會被調(diào)用兩次:一次是創(chuàng)建子類型的時候涯肩,另一次是在子類型構造函數(shù)的內(nèi)部轿钠。
function Box(name) {
this.name = name;
this.family = ['哥哥','妹妹','父母'];
}
Box.prototype.run = function () {
return this.name;
};
function Desk(name, age) {
Box.call(this, name);//第二次調(diào)用Box
this.age = age;
}
Desk.prototype = new Box();//第一次調(diào)用Box
4)空對象繼承
以上代碼是之前的組合繼承,那么用空對象直接繼承prototype方法病苗,解決了兩次調(diào)用的問題疗垛。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
//Child.uber = Parent.prototype;
}
這個extend函數(shù),就是YUI庫如何實現(xiàn)繼承的方法硫朦。
另外贷腕,說明一點,函數(shù)體最后一行
Child.uber = Parent.prototype;
意思是為子對象設一個uber屬性咬展,這個屬性直接指向父對象的prototype屬性泽裳。(uber是一個德語詞,意思是"向上"破婆、"上一層"涮总。)這等于在子對象上打開一條通道,可以直接調(diào)用父對象的方法祷舀。這一行放在這里瀑梗,只是為了實現(xiàn)繼承的完備性,純屬備用性質(zhì)蔑鹦。
function Box(name) {
this.name = name;
this.arr = ['哥哥','妹妹','父母'];
}
Box.prototype.run = function () {
return this.name;
};
function Desk(name, age) {
Box.call(this, name);
this.age = age;
}
extend( Desk,Box);//通過這里實現(xiàn)繼承
var desk = new Desk('xiaoming',100);
desk.arr.push('姐姐');
alert(desk.arr);
alert(desk.run());//只共享了方法
var desk2 = new Desk('Jack', 200);
alert(desk2.arr);//引用問題解決
5)拷貝繼承
上面是采用prototype對象夺克,實現(xiàn)繼承箕宙。我們也可以換一種思路嚎朽,純粹采用"拷貝"方法實現(xiàn)繼承。簡單說柬帕,如果把父對象的所有屬性和方法哟忍,拷貝進子對象狡门,不也能夠?qū)崿F(xiàn)繼承嗎?這樣我們就有了第五種方法锅很。
首先其馏,還是把Animal的所有不變屬性,都放到它的prototype對象上爆安。
function Animal(){}
Animal.prototype.species = "動物";
然后叛复,再寫一個函數(shù),實現(xiàn)屬性拷貝的目的扔仓。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p; }
這個函數(shù)的作用褐奥,就是將父對象的prototype對象中的屬性,一一拷貝給Child對象的prototype對象翘簇。
6)ECMA6 extends關鍵字 類的繼承
先來說一說撬码,es6的class語法
JavaScript語言的傳統(tǒng)方法是通過構造函數(shù),定義并生成新對象版保。下面是一個例子呜笑。
functionPoint(x,y){
this.x=x;
this.y=y;
}
Point.prototype.toString=function(){
return'('+this.x+', '+this.y+')';
};
varp=newPoint(1,2);
上面這種寫法跟傳統(tǒng)的面向?qū)ο笳Z言(比如C++和Java)差異很大,很容易讓新學習這門語言的程序員感到困惑彻犁。
ES6提供了更接近傳統(tǒng)語言的寫法叫胁,引入了Class(類)這個概念,作為對象的模板汞幢。通過class關鍵字曹抬,可以定義類〖宾基本上谤民,ES6的class可以看作只是一個語法糖,它的絕大部分功能疾宏,ES5都可以做到张足,新的class寫法只是讓對象原型的寫法更加清晰、更像面向?qū)ο缶幊痰恼Z法而已坎藐。上面的代碼用ES6的“類”改寫为牍,就是下面這樣
//定義類
classPoint{
constructor(x,y){
this.x=x;
this.y=y;
}
toString(){
return'('+this.x+', '+this.y+')';
}? }
上面代碼定義了一個“類”,可以看到里面有一個constructor方法岩馍,這就是構造方法碉咆,而this關鍵字則代表實例對象。也就是說蛀恩,ES5的構造函數(shù)Point疫铜,對應ES6的Point類的構造方法。
Point類除了構造方法双谆,還定義了一個toString方法壳咕。注意席揽,定義“類”的方法的時候,前面不需要加上function這個關鍵字谓厘,直接把函數(shù)定義放進去了就可以了幌羞。另外,方法之間不需要逗號分隔竟稳,加了會報錯属桦。
ES6的類,完全可以看作構造函數(shù)的另一種寫法他爸。
classPoint{
// ...
}
typeofPoint// "function"
Point===Point.prototype.constructor// true
上面代碼表明地啰,類的數(shù)據(jù)類型就是函數(shù),類本身就指向構造函數(shù)讲逛。
使用的時候亏吝,也是直接對類使用new命令,跟構造函數(shù)的用法完全一致盏混。
classBar{
doStuff(){
console.log('stuff');
}
}
varb=newBar();
b.doStuff()// "stuff"
構造函數(shù)的prototype屬性蔚鸥,在ES6的“類”上面繼續(xù)存在。事實上许赃,類的所有方法都定義在類的prototype屬性上面止喷。
classPoint{
constructor(){
// ...
}
toString(){
// ...
}
toValue(){
// ...
}
}
//等同于
Point.prototype={
toString(){},
toValue(){}
};
在類的實例上面調(diào)用方法,其實就是調(diào)用原型上的方法
classB{}
letb=newB();
b.constructor===B.prototype.constructor// true
上面代碼中混聊,b是B類的實例弹谁,它的constructor方法就是B類原型的constructor方法。
由于類的方法都定義在prototype對象上面句喜,所以類的新方法可以添加在prototype對象上面预愤。Object.assign方法可以很方便地一次向類添加多個方法。
classPoint{
constructor(){
// ...
}
}
Object.assign(Point.prototype,{
toString(){},
toValue(){}
});
prototype對象的constructor屬性咳胃,直接指向“類”的本身植康,這與ES5的行為是一致的。
Point.prototype.constructor===Point// true
類的實例對象需要注意:
生成類的實例對象的寫法展懈,與ES5完全一樣销睁,也是使用new命令。如果忘記加上new存崖,像函數(shù)那樣調(diào)用Class冻记,將會報錯。
/報錯
varpoint=Point(2,3);
//正確
varpoint=newPoint(2,3);
不存在變量提升
Class不存在變量提升(hoist)来惧,這一點與ES5完全不同冗栗。
newFoo();// ReferenceError
class Foo{}
上面代碼中,F(xiàn)oo類使用在前,定義在后贞瞒,這樣會報錯偶房,因為ES6不會把類的聲明提升到代碼頭部趁曼。這種規(guī)定的原因與下文要提到的繼承有關军浆,必須保證子類在父類之后定義。
Class的繼承
Class之間可以通過extends關鍵字實現(xiàn)繼承挡闰,這比ES5的通過修改原型鏈實現(xiàn)繼承乒融,要清晰和方便很多。
class ColorPoint extends Point{}
上面代碼定義了一個ColorPoint類摄悯,該類通過extends關鍵字赞季,繼承了Point類的所有屬性和方法。但是由于沒有部署任何代碼奢驯,所以這兩個類完全一樣申钩,等于復制了一個Point類。下面瘪阁,我們在ColorPoint內(nèi)部加上代碼撒遣。
class ColorPoint extends Point{
constructor(x,y,color){
super(x,y);//調(diào)用父類的constructor(x, y)
this.color=color;
}
toString(){
returnthis.color+' '+super.toString();//調(diào)用父類的toString()
}? ? }
上面代碼中,constructor方法和toString方法之中管跺,都出現(xiàn)了super關鍵字义黎,它在這里表示父類的構造函數(shù),用來新建父類的this對象豁跑。
子類必須在constructor方法中調(diào)用super方法廉涕,否則新建實例時會報錯。這是因為子類沒有自己的this對象艇拍,而是繼承父類的this對象狐蜕,然后對其進行加工。如果不調(diào)用super方法卸夕,子類就得不到this對象馏鹤。
class Point{/* ... */}
class ColorPoint extends Point{
constructor(){
}
}
letcp=newColorPoint();// ReferenceError
上面代碼中,ColorPoint繼承了父類Point娇哆,但是它的構造函數(shù)沒有調(diào)用super方法湃累,導致新建實例時報錯。
ES5的繼承碍讨,實質(zhì)是先創(chuàng)造子類的實例對象this治力,然后再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制完全不同勃黍,實質(zhì)是先創(chuàng)造父類的實例對象this(所以必須先調(diào)用super方法)宵统,然后再用子類的構造函數(shù)修改this。
如果子類沒有定義constructor方法,這個方法會被默認添加马澈,代碼如下瓢省。也就是說,不管有沒有顯式定義痊班,任何一個子類都有constructor方法勤婚。
constructor(...args){
super(...args);
}
另一個需要注意的地方是,在子類的構造函數(shù)中涤伐,只有調(diào)用super之后馒胆,才可以使用this關鍵字,否則會報錯凝果。這是因為子類實例的構建祝迂,是基于對父類實例加工,只有super方法才能返回父類實例器净。
class Point{
constructor(x,y){
this.x=x;
this.y=y;
}
}
class ColorPoint extends Point{
constructor(x,y,color){
this.color=color;// ReferenceError
super(x,y);
this.color=color;//正確
}
}
上面代碼中型雳,子類的constructor方法沒有調(diào)用super之前,就使用this關鍵字山害,結(jié)果報錯纠俭,而放在super方法之后就是正確的。
下面是生成子類實例的代碼粗恢。
letcp=newColorPoint(25,8,'green');
cpinstanceofColorPoint// true
cpinstanceofPoint// true
上面代碼中柑晒,實例對象cp同時是ColorPoint和Point兩個類的實例,這與ES5的行為完全一致眷射。