傳宗接代——繼承
“小白,看繼承呢?”小銘忙完自己的事情走過來。
“是呀,剛才學習類,發現每個類都有3個部分,第一部分是構造函數内的,這是供執行個體化對象複制用的,第二部分是構造函數外的,直接通過點文法添加的,這是供類使用的,執行個體化對象是通路不到的,第三部分是類的原型中的,執行個體化對象可以通過其原型鍊間接地通路到,也是為供所有執行個體化對象所共用的。然而在繼承中所涉及的不僅僅是一個對象。”
“對呀,不過繼承這種思想卻很簡單,如千年文明能夠流傳至今靠的就是傳承,将這些有用的文化一年一年地流傳下來,又如我們祖先一代一代地繁衍,才有了今天的我們。是以繼承涉及的不僅僅是一個對象。如人類的傳宗接代,父母會把自己的一些特點傳給孩子,孩子具有了父母的一些特點,但又不完全一樣,總會有自己的特點,是以父母與孩子又是不同的個體。”
“可是JavaScript并沒有繼承這一現有的機制,它又是如何實作的呢?”
子類的原型對象——類式繼承
“對呀,也正因為JavaScript少了這些顯性的限制才使得其具有了一定的靈活性,是以我們可以根據不同的需求實作多樣式的繼承。比如常見的類式繼承。”
// 類式繼承
// 聲明父類
function SuperClass(){
this.superValue = true;
}
// 為父類添加共有方法
SuperClass.prototype.getSuperValue = function(){
return this.superValue;
};
// 聲明子類
function SubClass(){
this.subValue = false;
}
// 繼承父類
SubClass.prototype = new SuperClass();
// 為子類添加共有方法
SubClass.prototype.getSubValue = function (){
return this.subValue;
};
“很像,真的很像!”小白很驚訝。
“像什麼?”小銘不解地問。
“剛才看過的封裝呀,不同的是這裡聲明了2個類,而且第二個類的原型prototype被賦予了第一個類的執行個體。”小白解釋道。
“很對,繼承很簡單,就是聲明2個類而已,不過類式繼承需要将第一個類的執行個體指派給第二個類的原型。但你知道為何要這麼做麼?”
“類的原型對象的作用就是為類的原型添加共有方法,但類不能直接通路這些屬性和方法,必須通過原型prototype來通路。而我們執行個體化一個父類的時候,新建立的對象複制了父類的構造函數内的屬性與方法并且将原型proto指向了父類的原型對象,這樣就擁有了父類的原型對象上的屬性與方法,并且這個新建立的對象可直接通路到父類原型對象上的屬性與方法。如果我們将這個新建立的對象指派給子類的原型,那麼子類的原型就可以通路到父類的原型屬性和方法。”小白還有些不自信。
“對,你分析得很準确。補充一點,你說的新建立的對象不僅僅可以通路父類原型上的屬性和方法,同樣也可通路從父類構造函數中複制的屬性和方法。你将這個對象指派給子類的原型,那麼這個子類的原型同樣可以通路父類原型上的屬性和方法與從父類構造函數中複制的屬性和方法。這正是類式繼承原理。”
“原來是這樣,但是我們要如何使用子類呢?”小白問道。
“使用很簡單,像下面這樣即可。”小銘說。
var instance = new SubClass();
console.log(instance.getSuperValue()); //true
console.log(instance.getSubValue()); //false
“另外,我們還可以通過instanceof來檢測某個對象是否是某個類的執行個體,或者說某個對象是否繼承了某個類。這樣就可以判斷對象與類之間的繼承關系了。”小銘補充說。
“instanceof?它如何就知道對象與類之間的繼承關系呢?”小白不解。
“instanceof是通過判斷對象的prototype鍊來确定這個對象是否是某個類的執行個體,而不關心對象與類的自身結構。”
“原來是這樣。”于是小白寫下測試代碼。
console.log(instance instanceof SuperClass); //true
console.log(instance instanceof SubClass); //true
console.log(SubClass instanceof SuperClass); //false
“我們說subClass繼承superClass,可是為什麼SubClass instanceof SuperClass得到的結果是false呢?”小白不解。
“前面說了,instanceof是判斷前面的對象是否是後面類(對象)的執行個體,它并不表示兩者的繼承,這一點你不要弄混,其次我們看看前面的代碼,你看我們在實作subClass繼承superClass時是通過将superClass的執行個體指派給subClass的原型prototype,是以說SubClass.prototype繼承了superClass。”小銘解釋說。
于是小白半信半疑地寫下測試代碼。
“哦,這麼說Object就是所有對象的祖先了。”小白笑着說。
“哈哈,可是你知道嗎,這種類式繼承還有2個缺點。其一,由于子類通過其原型prototype對父類執行個體化,繼承了父類。是以說父類中的共有屬性要是引用類型,就會在子類中被所有執行個體共用,是以一個子類的執行個體更改子類原型從父類構造函數中繼承來的共有屬性就會直接影響到其他子類,比如你看下面的代碼。”
function SuperClass(){
this.books = ['JavaScript', 'html', 'css'];
}
function SubClass(){}
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books); // ["JavaScript", "html", "css"]
instance1.books.push('設計模式');
console.log(instance2.books); // ["JavaScript", "html", "css", "設計模式"]
“instance1的一個無意的修改就會無情地傷害了instance2的book屬性,這在程式設計中很容易埋藏陷阱。其二,由于子類實作的繼承是靠其原型prototype對父類的執行個體化實作的,是以在建立父類的時候,是無法向父類傳遞參數的,因而在執行個體化父類的時候也無法對父類構造函數内的屬性進行初始化。”
“那我們要如何解決這些問題呢?”小白好奇地追問。
建立即繼承——構造函數繼承
“别着急,JavaScript是靈活的,自然也會有其他繼承方法來解決,比如常見的構造函數繼承。”
//構造函數式繼承
// 聲明父類
function SuperClass(id){
// 引用類型共有屬性
this.books = ['JavaScript', 'html', 'css'];
// 值類型共有屬性
this.id = id;
}
// 父類聲明原型方法
SuperClass.prototype.showBooks = function(){
console.log(this.books);
}
// 聲明子類
function SubClass(id){
// 繼承父類
SuperClass.call(this, id);
}
// 建立第一個子類的執行個體
var instance1 = new SubClass();
// 建立第二個子類的執行個體
var instance2 = new SubClass();
instance1.books.push("設計模式");
console.log(instance1.books); // ["JavaScript", "html", "css", "設計模式"]
console.log(instance1.id); // 10
console.log(instance2.books); // ["JavaScript", "html", "css"]
console.log(instance2.id); // 11
instance1.showBooks(); // TypeError
“小白,注意這裡。SuperClass.call(this, id);這條語句是構造函數式繼承的精華,由于call這個方法可以更改函數的作用環境,是以在子類中,對superClass調用這個方法就是将子類中的變量在父類中執行一遍,由于父類中是給this綁定屬性的,是以子類自然也就繼承了父類的共有屬性。由于這種類型的繼承沒有涉及原型prototype,是以父類的原型方法自然不會被子類繼承,而如果要想被子類繼承就必須要放在構造函數中,這樣建立出來的每個執行個體都會單獨擁有一份而不能共用,這樣就違背了代碼複用的原則。為了綜合這兩種模式的優點,後來有了組合式繼承。”
将優點為我所用——組合繼承
“組合繼承是不是說将這兩種繼承模式綜合到一起呀?那麼它又是如何做到的呢?”
“别着急,我們先總結一下之前兩種模式的特點,類式繼承是通過子類的原型prototype對父類執行個體化來實作的,構造函數式繼承是通過在子類的構造函數作用環境中執行一次父類的構造函數來實作的,是以隻要在繼承中同時做到這兩點即可,看下面的代碼。”
// 組合式繼承
// 聲明父類
function SuperClass(name){
// 值類型共有屬性
this.name = name;
// 引用類型共有屬性
this.books = ["html", "css", "JavaScript"];
}
// 父類原型共有方法
SuperClass.prototype.getName = function(){
console.log(this.name);
};
// 聲明子類
function SubClass(name, time){
// 構造函數式繼承父類name屬性
SuperClass.call(this, name);
// 子類中新增共有屬性
this.time = time;
}
// 類式繼承 子類原型繼承父類
SubClass.prototype = new SuperClass();
// 子類原型方法
SubClass.prototype.getTime = function(){
console.log(this.time);
};
“小白看到沒,在子類構造函數中執行父類構造函數,在子類原型上執行個體化父類就是組合模式,這樣就融合了類式繼承和構造函數繼承的優點,并且過濾掉其缺點,你測試看看。”
于是小白寫下測試代碼。
var instance1 = new SubClass("js book", );
instance1.books.push("設計模式");
console.log(instance1.books); // ["html", "css", "JavaScript", "設計模式"]
instance1.getName(); // js book
instance1.getTime(); //
var instance2 = new SubClass("css book", );
console.log(instance2.books); // ["html", "css", "JavaScript"]
instance2.getName(); // css book
instance2.getTime(); //
“真的是這樣呀,”小白興奮地說,“子類的執行個體中更改父類繼承下來的引用類型屬性如books,根本不會影響到其他執行個體,并且子類執行個體化過程中又能将參數傳遞到父類的構造函數中,如name。這種模式真的很強大,是以這應該是繼承中最完美的版本吧?”
“還不是,因為我們在使用構造函數繼承時執行了一遍父類的構造函數,而在實作子類原型的類式繼承時又調用了一遍父類構造函數。是以父類構造函數調用了兩遍,是以這還不是最完美的方式。”
“難道還有更好的方式麼?”
“那當然,JavaScript很靈活嘛。不過在學習這種方式之前我們先學習一個簡單而很常用的方式。”
潔淨的繼承者——原型式繼承
“2006年道格拉斯·克羅克福德發表一篇《JavaScript中原型式繼承》的文章,他的觀點是,借助原型prototype可以根據已有的對象建立一個新的對象,同時不必建立新的自定義對象類型。大師的話了解起來可能很困難,不過我們還是先看一下他實作的代碼吧。”
// 原型是繼承
function inheritObject(o){
// 聲明一個過渡函數對象
function F(){}
// 過渡對象的原型繼承父對象
F.prototype = o;
// 傳回過渡對象的一個執行個體,該執行個體的原型繼承了父對象
return new F();
}
“這種方式怎麼和類式繼承有些像呢?”
“對,它是對類式繼承的一個封裝,其實其中的過渡對象就相當于類式繼承中的子類,隻不過在原型式中作為一個過渡對象出現的,目的是為了建立要傳回的新的執行個體化對象。”
“如果是這樣,是不是類式繼承中的問題在這裡也會出現呢?”小白追問。
“是這樣的,”小銘接着說,“不過這種方式由于F過渡類的構造函數中無内容,是以開銷比較小,使用起來也比較友善。當然如果你感覺有必要可以将F過渡類緩存起來,不必每次建立一個新過渡類F。當然這種顧慮也是不必要的。随着對這種思想的深入,後來就出現的Object.create()的方法。”
“建立的新對象會不會影響到父類對象呢?”于是小白寫下測試用例(測試代碼)。
var book = {
name: "js book",
alikeBook: ["css book", "html book"]
};
var newBook = inheritObject(book);
newBook.name = "ajax book";
newBook.alikeBook.push("xml book");
var otherBook = inheritObject(book);
otherBook.name = "flash book";
otherBook.alikeBook.push("as book");
console.log(newBook.name); //ajax book
console.log(newBook.alikeBook); //["css book", "html book", "xml book", "as book"]
console.log(otherBook.name); //flash book
console.log(otherBook.alikeBook); //["css book", "html book", "xml book", "as book"]
console.log(book.name); //js book
console.log(book.alikeBook); //["css book", "html book", "xml book", "as book"]
“跟類式繼承一樣,父類對象book中的值類型的屬性被複制,引用類型的屬性被共用。”小白感歎道。
“然而道格拉斯·克羅克福德推廣的繼承并不隻這一種,他在此基礎上做了一些增強而推出一種寄生式繼承。”
如虎添翼——寄生式繼承
“寄生式繼承?這還頭一次聽說,它是怎麼實作的?”
“不着急,大師對該模式論述的話我們就不深究了,我們還是看看這種繼承的實作吧。”
// 寄生式繼承
// 聲明基對象
var book = {
name: "js book",
alikeBook: ["css book", "html book"]
};
function createBook(obj){
// 通過原型繼承方式建立新對象
var o = new inheritObject(obj);
// 拓展新對象
o.getName = function(){
console.log(name);
};
// 傳回拓展後的新對象
return o;
}
“看懂了嗎?”小銘問小白,“其實寄生式繼承就是對原型繼承的第二次封裝,并且在這第二次封裝過程中對繼承的對象進行了拓展,這樣新建立的對象不僅僅有父類中的屬性和方法而且還添加新的屬性和方法。”
“哦,這種類型的繼承果如其名,寄生大概指的就是像寄生蟲一樣寄托于某個對象内部生長。當然寄生式繼承這種增強新建立對象的繼承思想也是寄托于原型繼承模式吧。”
“嗯,是這個道理,而這種思想的作用也是為了寄生組合式繼承模式的實作。”
終極繼承者——寄生組合式繼承
“寄生組合式繼承?”小白好奇地問道。
“嗯,之前我們學習了組合式繼承,那時候我們将類式繼承同構造函數繼承組合使用,但是這種方式有一個問題,就是子類不是父類的執行個體,而子類的原型是父類的執行個體,是以才有了寄生組合式繼承。但是你知道是哪兩種模式的組合麼?”
“寄生當然是寄生式繼承,寄生式繼承依托于原型繼承,原型繼承又與類式繼承相像,另外一種就不應該是這些模式了,是以另外一種繼承模式應該是構造函數繼承了吧。當然,子類不是父類執行個體的問題是由于類式繼承引起的。”小白回答道。
“對,正是這兩種繼承,但是這裡寄生式繼承有些特殊,這裡它處理的不是對象,而是類的原型。我們再次來看看道格拉斯·克羅克福德對寄生式繼承的一個改造。”
/**
* 寄生式繼承 繼承原型
* 傳遞參數 subClass 子類
* 傳遞參數 superClass 父類
**/
function inheritPrototype(subClass, superClass){
// 複制一份父類的原型副本儲存在變量中
var p = inheritObject(superClass.prototype);
// 修正因為重寫子類原型導緻子類的constructor屬性被修改
p.constructor = subClass;
// 設定子類的原型
subClass.prototype = p;
}
“組合式繼承中,通過構造函數繼承的屬性和方法是沒有問題的,是以這裡我們主要探究通過寄生式繼承重新繼承父類的原型。我們需要繼承的僅僅是父類的原型,不再需要調用父類的構造函數,換句話說,在構造函數繼承中我們已經調用了父類的構造函數。是以我們需要的就是父類的原型對象的一個副本,而這個副本我們通過原型繼承便可得到,但是這麼直接指派給子類會有問題的,因為對父類原型對象複制得到的複制對象p中的constructor指向的不是subClass子類對象,是以在寄生式繼承中要對複制對象p做一次增強,修複其constructor屬性指向不正确的問題,最後将得到的複制對象p指派給子類的原型,這樣子類的原型就繼承了父類的原型并且沒有執行父類的構造函數。”
“看上去好複雜呀。”小白驚歎道。
“是以你要去測一測呀。測試很簡單,與組合模式相比隻有一個地方做了修改,就是子類原型繼承父類原型這一處,測試看看吧。”
“好吧。”于是小白寫下了測試用例。
// 定義父類
function SuperClass(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
// 定義父類原型方法
SuperClass.prototype.getName = function(){
console.log(this.name);
};
// 定義子類
function SubClass(name, time){
// 構造函數式繼承
SuperClass.call(this, name);
// 子類新增屬性
this.time = time;
}
// 寄生式繼承父類原型
inheritPrototype(SubClass, SuperClass);
// 子類新增原型方法
SubClass.prototype.getTime = function(){
console.log(this.time);
};
// 建立兩個測試方法
var instance1 = new SubClass("js book", );
var instance2 = new SubClass("css book", );
小白首先建立了父類,以及父類的原型方法,然後建立子類,并在構造函數中實作構造函數式繼承,然後又通過寄生式繼承了父類原型,最後又對子類添加了一些原型方法。
最後小白測試了一下,結果如下:
instance1.colors.push("black");
console.log(instance1.colors); //["red", "blue", "green", "black"]
console.log(instance2.colors); //["red", "blue", "green"]
instance2.getName(); //css book
instance2.getTime(); //
“現在你明白了吧,其實這種方式繼承如下圖所示,其中最大的改變就是對子類原型的處理,被賦予父類原型的一個引用,這是一個對象,是以這裡有一點你要注意,就是子類再想添加原型方法必須通過prototype.對象,通過點文法的形式一個一個添加方法了,否則直接賦予對象就會覆寫掉從父類原型繼承的對象了。”
老師不止一位——多繼承
“是這樣呀,對了,我記得有一些面向對象語言中支援多繼承,在JavaScript中能實作麼?”
“嗯,不過是有一些局限性的。你知道,在JavaScript中繼承是依賴于原型prototype鍊實作的,隻有一條原型鍊,是以理論上是不能繼承多個父類的。然而JavaScript是靈活的,通過一些技巧方法你卻可以繼承多個對象的屬性來實作類似的多繼承。”小銘接着說,“講解多繼承之前先跟你說一下目前很流行的一個用來繼承單對象屬性的extend方法。”
// 單繼承 屬性複制
var extend = function(target, source) {
// 周遊源對象中的屬性
for (var property in source) {
// 将源對象中的屬性複制到目标對象中
target[property] = source[property];
}
// 傳回目标對象
return target;
};
“原來extend方法的實作就是對對象中的屬性的一個複制過程呀。”小白驚訝地說。
“嗯,是這樣,我們的這個extend方法是一個淺複制過程,他隻能複制值類型的屬性,對于引用類型的屬性它無能為力。而在jquery等一些架構中實作了深複制,就是将源對象中的引用類型的屬性再執行一遍extend方法而實作的。我們這裡實作得比較簡單,是以你測試也比較容易。”
于是小白寫下如下測試代碼。
var book = {
name : 'JavaScript設計模式',
alike : ['css', 'html', 'JavaScript']
}
var anotherBook = {
color : 'blue'
}
extend(anotherBook, book);
console.log(anotherBook.name); // JavaScript設計模式
console.log(anotherBook.alike); // ["css", "html", "JavaScript"]
anotherBook.alike.push('ajax');
anotherBook.name = '設計模式';
console.log(anotherBook.name); // 設計模式
console.log(anotherBook.alike); // ["css", "html", "JavaScript", "ajax"]
console.log(book.name); // JavaScript設計模式
console.log(book.alike); // ["css", "html", "JavaScript", "ajax"]
“真的是這樣。但是多繼承呢?”
“很容易,既然上面的方法可以實作對一個對象屬性的複制繼承,那麼如果我們傳遞多個對象呢?”
// 多繼承 屬性複制
var mix = function() {
var i = , // 從第二個參數起為被繼承的對象
len = arguments.length, // 擷取參數長度
target = arguments[], // 第一個對象為目标對象
arg; // 緩存參數對象
// 周遊被繼承的對象
for(; i < len; i++){
// 緩存目前對象
arg = arguments[i];
// 周遊被繼承對象中的屬性
for (var property in arg) {
// 将被繼承對象中的屬性複制到目标對象中
target[property] = arg[property];
}
}
// 傳回目标對象
return target;
};
“mix方法的作用就是将傳入的多個對象的屬性複制到源對象中,這樣即可實作對多個對象的屬性的繼承。”
“這是實作方式真不錯,可是使用的時候需要傳入目标對象(第一個參數——需要繼承的對象)。”
“當然你也可以将它綁定到原生對象Object上,這樣所有的對象就可以擁有這個方法了。”
Object.prototype.mix = function(){
var i = , // 從第一個參數起為被繼承的對象
len = arguments.length, // 擷取參數長度
arg; // 緩存參數對象
// 周遊被繼承的對象
for(; i < len; i++){
// 緩存目前對象
arg = arguments[i];
// 周遊被繼承對象中的屬性
for (var property in arg) {
// 将被繼承對象中的屬性複制到目标對象中
this[property] = arg[property];
}
}
}
“這樣我們就可以在對象上直接調用了。如……”
otherBook.mix(book1, book2);
console.log(otherBook); // Object {color: "blue", name: "JavaScript設計模式", mix: function, about: "一本JavaScript書"}
“在JavaScript中實作的多繼承是如此的美妙。”
多種調用方式——多态
“小銘,在面向對象程式設計中不是還有一種特性叫作多态麼?在JavaScript中可以實作麼?”
“多态,就是同一個方法多種調用方式吧。在JavaScript中也是可以實作的,隻不過要對傳入的參數做判斷以實作多種調用方式,如我們定義一個add方法,如果不傳參數則傳回10,如果傳一個參數則傳回10+參數,如果傳兩個參數則傳回兩個參數相加的結果。”
//多态
function add(){
// 擷取參數
var arg = arguments,
// 擷取參數長度
len = arg.length;
switch(len){
// 如果沒有參數
case :
return ;
// 如果隻有一個參數
case :
return + arg[];
// 如果有兩個參數
case :
return arg[] + arg[];
}
}
// 測試用例
console.log(add()); // 10
console.log(add()); // 15
console.log(add(,)); // 13
“當然我們還可以讓其轉化成更易懂的類形式:”
function Add(){
// 無參數算法
function zero(){
return ;
}
// 一個參數算法
function one(num){
return + num;
}
// 兩個參數算法
function two(num1, num2){
return num1 + num2;
}
// 相加共有方法
this.add = function(){
var arg = arguments,
// 擷取參數長度
len = arg.length;
switch(len){
// 如果沒有參數
case :
return zero();
// 如果隻有一個參數
case :
return one(arg[]);
// 如果有兩個參數
case :
return two(arg[], arg[]);
}
}
}
// 執行個體化類
var A = new Add();
//測試
console.log(A.add()); // 10
console.log(A.add()); // 15
console.log(A.add(,)); // 13
“對于多态類,當我們調用add運算方法時,他會根據傳參不同做相應運算,當然我們将不同運算方法封裝在類内,這樣代碼更易懂。”
回憶
封裝與繼承是面向對象中的兩個主要特性,繼承即是對原有對象的封裝,從中建立私有屬性、私有方法、特權方法、共有屬性、共有方法等,對于每種屬性與每種方法特點是不一樣的,有的不論對類如何執行個體化,它隻建立一次,那麼這類屬性或者方法我們稱之為靜态的。有的隻被類所擁有,那麼這類屬性和方法又是靜态類方法與靜态類屬性。當然可被繼承的方法與屬性無外乎兩類,一類在構造函數中,這類屬性與方法在對象執行個體化時被複制一遍。另一類在類的原型對象中,這類屬性與方法在對象執行個體化時被所有執行個體化對象所共用。
提到類的執行個體化我們就引出了繼承,當然如果執行個體化的是對象那麼則為對象繼承,如果執行個體化的是類(當然類也是一種對象,隻不過是用來建立對象的),那麼就是一種類的繼承。對于類的繼承我們根據繼承的方式又分為很多種,通過原型鍊繼承的方式我們稱之為類式繼承,通過構造函數繼承的方式我們稱之為構造函數式繼承,那麼将這兩種方式組合起來的繼承方式我們稱之為組合繼承,由于類式繼承過程中會執行個體化父類,這樣如果父類構造函數極其複雜,那麼這種方式對構造函數的開銷是不值得的,此時有了一種新的繼承方式,通過在一個函數内的過渡對象實作繼承并傳回新對象的方式我們稱之為寄生式繼承,此時我們在結合構造函數時繼承,這樣再融合構造函數繼承中的優點并去除其缺點,得到的繼承方式我們稱之為寄生組合式繼承。當然有時候子類對父類實作繼承可以通過拷貝方法與屬性的方式來實作,這就有了多繼承,即将多個父類(對象)的屬性與方法拷貝給子類實作繼承。
對于面向對象中的多态,在JavaScript中實作起來就容易得多了,通過對傳遞的參數判斷來決定執行邏輯,即可實作一種多态處理機制。