原型鏈與繼承
原型鏈(Prototype chain)
ECMAScript中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承裏一個引用類型的屬性和方法。簡單回顧下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。那麽,加入我們讓原型對象等於另一個類型的實例,結果會怎樣呢?顯然,此時的原型獨享將包含一個指向另一哥原型的指針,相應地,另一個原型中也包含著一個指向另一哥構造函數的指針。加入另一個原型又是另一個類型的實例,那麽上述關系依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。
確定原型和實例的關系
可以通過兩種方式來確定原型和實例之間的關系。第一種方式是使用instanceof操作符,只要用這個操作符來測試實例與原型鏈中出現過的構造函數,就會返回true。第二種方式是使用isPrototypeOf()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的實例的原型。
謹慎地定義方法
子類型有時候需要重寫超類型中的某個方法,或者需要添加超類型中不存在的某個方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之後。
還要一定需要提醒讀者,即在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。因為這樣做就會重寫原型鏈。例如:
function SuperType(){this.property=true; } SuperType.prototype.getSuperValue=function(){ return this.property; }; function SubType(){ this.subproperty=false; } //繼承了SuperType SubType.prototype=new SuperType(); //使用字面量添加新方法,會導致上一行代碼無效 SubType.prototype={ getSubValue:function(){ return this.subproperty; } someotherMethod:function(){return false; } } var instance=new SubType(); alert(instance.getSuperValue());
以上代碼展示了剛剛把SuperType的實例賦值給原型,緊接著又將原型替換成一個對象字面量而導致的問題。由於現在的原型包含的是一個object的實例,而非SuperType的實例,因此我們設想中的原型鏈已經被切斷--SuperType和SubType之間已經沒有關系了。
原型鏈的問題
原型鏈雖然很強大,可以用它來實現繼承,但它也存在一些問題。其中,最主要的問題來自包含引用類型值的原型。因為包含引用類型值的原型屬性會被所有實例共享;而這也正是為什麽要在構造函數中,而不是在原型對象中定義屬性的原因。在同個原型來實現繼承時,原型實際上胡變成另一個類型的實例。於是,原先的實例屬性也就順理成章地變成了現在的原型屬性了。下面代碼可以用來說明這個問題。
function SuperType(){ this.colors=[‘red‘,‘blue‘,‘green‘]; } function SubType(){} SubType.prototype=new SuperType(); var instance1=new SubType(); instance1.colors.push(‘black‘); alert(instance1.colors); //‘red‘,‘blue‘,‘green‘,‘black‘ var instance2=new SubType(); alert(instance2.colors); //‘red‘,‘blue‘,‘green‘,‘black‘
這個例子中的SuperType構造函數定義了一個colors屬性,該屬性包含一個數組,SuperType的每個勢力都會有個字包含自己數組的colors屬性。當subType通過原型鏈繼承了SuperType之後,subType.prototype就變成了SuperType的一個實例,因此它也擁有了一個它自己的colors屬性。但結果是什麽呢?結果是subType的所有實例都會共享這一個colors屬性。而我們隊instance1.colors的修改能夠通過instance2.colors反映出來,就已經證實了這一點。
原型鏈的第二個問題:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響所有對象市裏的情況下,給超類型的構造函數傳遞參數。有鑒於此,再加上前面剛哥討論過的由於原型中包含引用類型值所帶來的問題,實踐中很少會單獨使用原型鏈。
繼承
所謂繼承就是子類繼承父類的特征與行為,使得子類對象具與父類相同的行為。但是javascript 是沒有class、抽象類、接口等抽象概念,javascript 只有對象,那麽js中是如何讓對象與對象之間產生繼承關系呢?
基於對象的繼承
在原型鏈中說過,如果在對象上沒有找到需要的屬性或者方法引用,js引擎就會繼續在內部屬性[[prototype]] 指向的對象上進行查找。同理如果還是沒有找到需要的引用,就會繼續查找它的內部屬性[[prototype]]指向的對象,以此類推,層層向上直到找到對象的原型為null為止,這一系列的鏈接稱為原型鏈。所以在原型鏈中我才會js中的繼承是基於原型實現;
組合繼承
組合繼承是比較常用的一種繼承方法,思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又保證每個實例都有它自己的屬性。
function Person(name,age){ this.name=name; this.age=age; } Person.prototype.sayName=function(){ return this.name; }; //父親 function Parent(work,country,name,age){ this.work=work; this.country=country; this.parentInfo=function(){ return ‘hello,我叫:‘ + this.name +‘ 我是一名:‘ + this.work + ‘,我來自:‘ + this.country; } Person.call(this,name,age);//父類型傳參 第二次調用Person() }; Parent.prototype=new Person();//重寫Parent的原型對象 第一次調用Person() var myParent=new Parent(‘manager‘,‘china‘,‘amy‘,23); myParent.sayName(); myParent.parentInfo(); //兒子 function Child(work,country,name,age,sex){ this.sex=sex; this.childInfo=function(){ return ‘hello,我叫:‘ + this.name + ‘ 我是一名:‘ + this.work + ‘,我來自‘ + this.country + ‘,我是一個可愛的 ‘ + this.sex; } Parent.call(this,work,country,name,age); } Child.prototype=new Parent();//重寫Child的原型對象 var myBaby=new Child(‘child‘, ‘廣州‘, ‘超級飛俠-多多‘, 3, ‘girl‘); myBaby.parentInfo() myBaby.childInfo()
這裏基類是人,子類是父親、兒子,這裏通過重寫原型對象以及在構造函數中調用父類的構造函數,並且用call改變了this 指針,從而達到兒子繼承了父親,父親繼承了人;
弊端:黃色字體的行中調用person()構造函數的代碼。在第一次調用person()構造函數時,parent.prototype會得到兩個屬性:name和age;他們都是person的實例屬性,只不過現在位於parent()的原型中。當調用parent()構造函數時,又會調用一次person()構造函數,這一次又在心對象上創建了實例屬性name和age。於是,這兩個屬性就屏蔽了原型中的兩個同名屬性。
寄生組合式繼承
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混合形式來繼承方法。其背後的基本思路:不必為了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型的原型的一個副本而已。本質上,就是使用寄生式繼承超類型的原型,然後再將結果指定給子類型的原型。
function inheritPrototype(subType,superType){ var prototype=object(superType.prototype);//創建對象 prototype.constructor=subType;//增強對象 SubType.prototype=prototype;//指定對象 }
在函數內部,第一步是創建超類型原型的一個副本。第二步是為了創建的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。最後一步,將新創建的對象(副本)賦值給子類型的原型。然後把代碼改寫如下:
function Person(name,age){ this.name=name; this.age=age; } Person.prototype.sayName=function(){ return this.name; }; //父親 function Parent(work,country,name,age){ this.work=work; this.country=country; this.parentInfo=function(){ return ‘hello,我叫:‘ + this.name +‘ 我是一名:‘ + this.work + ‘,我來自:‘ + this.country; } Person.call(this,name,age);//父類型傳參 }; inheritPrototype(Parent , Person) var myParent=new Parent(‘manager‘,‘china‘,‘amy‘,23); myParent.sayName(); myParent.parentInfo(); //兒子 function Child(work,country,name,age,sex){ this.sex=sex; this.childInfo=function(){ return ‘hello,我叫:‘ + this.name + ‘ 我是一名:‘ + this.work + ‘,我來自‘ + this.country + ‘,我是一個可愛的 ‘ + this.sex; } Parent.call(this,work,country,name,age); } inheritPrototype(Child,Parent) var myBaby=new Child(‘child‘, ‘廣州‘, ‘超級飛俠-多多‘, 3, ‘girl‘); myBaby.parentInfo() myBaby.childInfo()
總結:
1)對象之間的鏈接通過[[prototype]]進行關聯
2)new 操作符關鍵點
3)prototype 是函數對象的屬性,_proto_ 是實例的內部屬性[[prototype]]
4)原型鏈是沿著_proto_鏈接進行查找
5)寄生組合式繼承時實現基於原型繼承的最有效方式
參考:《javascript高級程序設計(第3版)》
原型鏈與繼承