javascript雖然是一門物件導向的語言,但是它的繼承機制從一開始設計的時候就不同於傳統的其他物件導向語言,是基於原型的繼承機制,但是在這種機制下,繼承依然有一些不同的實現方式。
方法一:類別繼承
所謂的類別繼承就是指模仿傳統物件導向語言的繼承方式,繼承與被繼承的雙方都是“類別”,程式碼如下:
先定義一個父類別(或超類別):
function Person(name){ this.name=name; } Person.prototype.getName=function(){ return this.name; };
該父類person的屬性在建構子中定義,可以保證繼承它的子類的name屬性不會與它共享這個屬性,而是單獨屬於子類,將getName方法掛載到原型上,是為了讓繼承它的子類別的多個實例共享這一個方法體,這樣能夠節省內存,(對於多個實例來講,每次New一個實例出來都會保證這些實例的getName方法引用的是同一段內存空間,而不是獨立的空間)。
定義一個繼承方法extend,如下:
function extend(subClass,superClass){ var F=function(){}; F.prototype=superClass.prototype; subClass.prototype=new F(); subClass.prototype.constructor=subClass; subClass.superClass=superClass.prototype; if(superClass.prototype.constructor==Object.prototype.constructor){ superClass.prototype.constructor=superClass; } }
在這個方法中,先建立一個新的類別為F,讓它的原型為父類別的原型,並且讓子類別的原型指向該類別F的一個實例,從而達到了一個繼承父類別的目的,同時,子類別的原型由於被修改,所以將修改後的原型的constructor屬性指向子類,讓其擁有建構函數,同時給該子類別掛載一個superClass屬性,子類別可以透過該屬性呼叫父類,從而建立起了子類別和父類別的關係。
定義一個子類別Author來繼承父類別Person,如下:
function Author(name,books){ Author.superClass.constructor.call(this,name); this.book=books; } extend(Author,Person); Author.prototype.getBooks=function(){ return this.book; }
這裡在子類別的建構子中透過其superClass屬性呼叫父類別的建構函數,同時採用call方法,轉換該方法呼叫的this指向,讓子類別Author也擁有(繼承)父類別的屬性,同時子類別又擁有自己的屬性book,所以在建構函式中將參數books賦值給屬性book,達到建構的目的。採用extend函數繼承父類別Person原型上的屬性和方法(實際上只繼承了方法,因為我們之前只是將方法掛載到了原型上,屬性是在建構函式中定義的)。同時,Author又擁有自己的方法getBooks,將其掛載到對應的原型上,達到了在繼承的基礎上進一步擴充自身的目的。
這種繼承方式很明顯就是採用類似傳統物件導向語言的類別繼承,優點是對習慣於傳統物件導向概念的程式設計師來講很容易理解,缺點是過程比較繁瑣,記憶體消耗稍大,因為子類別也擁有自己的建構函式及原型,而且子類別和父類別的屬性完全是隔離的,即使兩者是一樣的值,但是不能共享同一段記憶體。
方法二:原型式繼承
先定義一個父類,這裡不會刻意模仿使用構造函數來定義,而是直接採用對象字面量的方式定義一個對象,該對象就是父類
var Person={ name:'default name', getName:function(){ return this.name; } } ;
和第一種方法一樣,該物件擁有一個屬性name和一個方法getName .
然後再定義一個克隆方法,用來實作子類別對父類別的繼承,如下:
function clone(obj){ function F(){} F.prototype=obj; return new F(); }
此克隆方法新建一個對象,將該對象的原型指向父類別即參數obj,同時返回這個對象。
最後子類別再透過克隆函數繼承父類,如下:
var Author=clone(Person); Author.book=['javascript']; Author.showBook=function(){ return this.book; }
这里定义一个子类,通过clone函数继承父类Person,同时拓展了一个属性book,和一个方法showBook,这里该子类也拥有属性name,但是它和父类的name值是一样的,所以没有进行覆盖,如果不一样,可以采用
Author.name='new name';覆盖这个属性,从而得到子类的一个新的name属性值。
这种原型式继承相比于类式继承更为简单自然,同时如果子类的属性和父类属性值相同,可以不进行修改的话,那么它们两者其实共享的是同一段内存空间,如上面的name属性,缺点是对于习惯了传统面向对象的程序员难以理解,如果两者要进行选择的话,无疑是这种方式更为优秀一些。
既然javascript中采用基于原型的方式来实现继承,而且每个对象的原型只能指向某个特定的类的实例(不能指向多个实例),那么如何实现多重继承(即让一个类同时具有多个类的方法和属性,而且本身内部不自己定义这些方法和属性)?
在javascript设计模式中给出了一种掺元类(mixin class)的方式:
首先定义一个掺元类,用来保存一些常用的方法和属性,这些方法和属性可以通过拓展的方式添加到任何其他类上,从而被添加类就具有了该类的某些方法和属性,如果定义多个掺元类,同时添加给一个类,那么该类就是间接实现了“多重继承”,基于这种思想,实现如下:
掺元类定义:
var Mixin=function(){}; Mixin.prototype={ serialize:function(){ var output=[]; for(key in this){ output.push(key+":"+this[key]); } return output.join(','); } }
该掺元类具有一个serialize方法,用来遍历其自身,输出自身的属性和属性值,并且将他们以字符串形式返回,中间用逗号隔开。
定义一个扩充方法,用来使某个类经过扩充之后具备掺元类的属性或方法,如下:
function augment(receivingClass,givingClass){ if(arguments[2]){ for(var i= 2,len=arguments.length;i<len;i++){ receivingClass.prototype[arguments[i]]=givingClass.prototype[arguments[i]]; } } else { for(methodName in givingClass.prototype){ if(!receivingClass.prototype[methodName]){ receivingClass.prototype[methodName]=givingClass.prototype[methodName]; } } } }
该方法默认是两个参数,第一个参数是接受被扩充的类,第二个参数是掺元类(用来扩充其他类的类),还可以有其他参数,如果大于两个参数,后面的参数都是方法或者属性名,用来表示被扩充类只想继承掺元类的指定的属性或方法,否则默认继承掺元类的所有属性和方法,在该函数中,第一个if分支是用来继承指定属性和方法的情形,else分支是默认继承所有属性和方法的情形。该方法的实质是将掺元类的原型上的属性和方法都扩充(添加)到了被扩充类的原型上面,从而使其具有掺元类的属性和方法。
最后,使用扩充方法实现多重继承
augment(Author,Mixin); var author= new Author('js',['javascript design patterns']); alert(author.serialize());
这里定义了一个author的类,该类继承自Person父类,同时又具备掺元类Mixin的方法和属性,如果你愿意,可以定义N个掺元类用来扩充该类,它同样能够继承你定义的其他掺元类的属性和方法,这样就实现了多重继承,最后,author的serialize方法的运行结果如下:
你会发现该类同时具有person类,Author类,Mixin类的属性和方法,其中Person和Mixin的属性和方法都是通过“继承”得来的,从实际上来讲,它实现了多重继承。