是的我們要理解繼承模式首先 來瞭解下原型鏈這個東西。
一:原型鏈
其基本的思想就是讓一個參考型別繼承另一個參考型別的屬性和方法。要理解原型鏈我們可以先回顧下建構函式,原型和執行個體三者的關係:每個建構函式都有一個原型對象,原型對象都包含一個指向建構函式的指標,而執行個體都包含一個指向原型對象的內部指標。
我們假如讓原型對象等於另一個類型的執行個體,那麼此時的原型對象將包含一個指向另一個原型的指標,而相應的另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個類型的執行個體,那麼上述關係仍然成立。這差不多就是所謂的原型鏈概念。
我們通過代碼看下三者的關係吧:
1:函數與原型的關係
a.每個建構函式都有一原型對象
function fun(){
}
console.log(fun.prototype); //fun{} 說明prototype是一個原型對象
b.原型對象(即prototype屬性)會自動獲得一個constructor屬性,constructor屬性包含了prototype屬性所在函數(fun)的指標,用來標識執行個體化對象的類型
如果建構函式中定義了原型中的同名屬性方法時,那麼執行個體就會調用重新定義的屬性與方法了
function fun(name){
console.log(fun.prototype.name == this.name);//true(yjh)
this.name = “yjh1″;
console.log(this.name);//yjh1
console.log(fun.prototype.name == this.name);//false(yjh,yjh1)
}
fun.prototype = {
constructor: fun,
name: “yjh”
}
var fun1 = new fun();
console.log(fun.prototype.constructor == fun); //true
console.log(fun1.constructor == fun); //true
2:執行個體化對象與原型的關係
a.當函數使用new操作符執行個體化一個對象時,那麼對象就包含了一個內在的指向原型的__proto__屬性,它只存在執行個體對象與原型對象之間
如:
function fun(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
}
}
fun.prototype = {
constructor: fun,
age: 22,
sayName: function(){
alert(this.age);
}
}
var fun1 = new fun(“yjh”,”23″);
console.log(fun1.__proto__) //fun { age=”22″, sayAge=function()}
console.log(fun1.__proto__ == fun.prototype); //true
3:執行個體對象與函數(建構函式)的關係
a.當函數使用new操作符執行個體化一個對象時,建構函式中的this對象為執行個體對象,執行個體對象擷取了綁定到this中的屬性和方法
如:
function fun(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
}
}
var fun1 = new fun(“yjh”,”23″);
fun1.sayName(); //yjh
4:函數(建構函式)與執行個體對象、原型三者之間的關係
在調用執行個體化對象屬性,方法時,那麼首先會搜尋執行個體對象自身定義的屬性和方法,如果沒有的話,會繼續搜尋原型
function fun(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
}
}
fun.prototype.age = “22″;
fun.prototype.sayAge = function(){
alert(this.age);
}
var fun1 = new fun(“yjh”,”23″);
fun1.age = 24;
fun1.sayAge(); //24 ,調用了原型中的方法;
原型鏈很強大,可以用它來實現繼承,不過在用原型鏈實現繼承時,不能使用對象字面量建立原型方法,因為這樣會重寫原型鏈。如:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subprotype = false;
}
SubType.protopyte = new SuperType();
SubType.protopyte = {
getSubValue:function(){
return this.subproperty;
},
someOtherMethod:function(){
return false;
}
};
var instance = new SubType();
alert(instance.getSupervalue()); //error
原型鏈實現繼承的主要問題來自包含參考型別值的原型。因為在通過原型來實現繼承時,原型實際上會變成另一個類型的執行個體,於是原型的執行個體屬性就變成了現在的原型屬性。還有一個問題就是在建立子類型的執行個體時,不能向超類型的建構函式中傳遞參數。
二:借用建構函式
借用建構函式技術有時候也叫偽造對象或經典繼承。其基本思想就是在子類型建構函式的內部調用超類型建構函式。我們知道函數只是在特定環境中執行代碼的對象,因此可以通過使用apply()和call()方法也可以在新建立的對象上執行建構函式。如下:
function SuperType(){
this.color = ["red","blue","green"];
}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
instancel.colors.push(“black”);
alert(instancel.colors); //”red,blue,green,balck”
var instance2 = new SubType();
alert(instance2.colors); //”red,blue,green”
上面就是通過call()方法在新建立的SubType執行個體環境下調用了SuperType建構函式,這樣就會在新的SubType對象上執行SuperType()函數中定義的所有對象初始化代碼。
傳遞參數:
前面說了,原型鏈不能傳遞參數,而建構函式剛好有這個優勢。
舉個例子:
function SuperType(nmae){
this.name = name;
}
function SubType(){
SuperType.call(this,”Nicholas”);
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //”Nicholas”;
alert(instance.age); //29
如果僅僅使用借用建構函式來繼承,那麼也有點不足的地方,那就是 方法都在哦建構函式中定義,函數就起不到複用的效果。
三:組合繼承
組合繼承也叫作偽經典繼承,主要是指將原型鏈和借用建構函式的技術組合到一塊,從而發揮二者之長的一種繼承模式。其基本思路就是通過使用原型鏈實現對原型屬性和方法的繼承,通過借用建構函式來實現對執行個體屬性的繼承。
如下:
function SuperType(){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
SuperType.call(this,name); //繼承屬性
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge =function(){
alert(this.age);
}
var instance1 = new SubType(“Nicholas”,29);
instance1.colors.push(“black”);
alert(instance1.colors); //”red,blue,green,black”
instance1.sayName(); //”Nicholas”
instance1.sayAge(); //29
var instance2 = new SubType(“Greg”,27);
alert(instance2.colors); //”red,blue,green”
instance2.sayName(); //”Greg”
instance2.sayAge(); //27
組合繼承是JavaScript中最常用的一種繼承模式,而且instanceof和isPrototypeof()也能夠識別基於組合繼承建立的對象。
四:原型式繼承
這種方法並沒有使用嚴格意義上的建構函式,其基本思想是藉助原型可以基於已有的對象建立新對象,同時還不必因此建立自訂類型。其實這本質上來講是一個淺複製(淺拷貝)。如下:
var person = {
name:”Nicholas”;
friends:["Shelby","Court","Van"]
};
var anotherPerson = object(person);
anotherPerson.name = “Greg”;
anotherPerson.friends.push(“Rob”);
var yetAnotherPerson =object(person);
yetAnotherPerson.name = “Linda”;
yetAnotherPerson.friends.push(“Barbie”);
alert(person.friends); //”Shelby,Court,Van,Rob,Barbie”
這種繼承模式就是要求你有一個對象可以作為另一個對象的基礎,如果有這麼一個對象的話,可以把它傳遞給object()函數,然後再根據具體需求對得到的對象加以修改即可。
在ECMAScript5裡通過新增Object.creat()方法規範了原型式繼承。這個方法接受兩個參數:一個用作新對象原型的對象和一個為新對象定義額外屬性的對象,(後者可選)。不過這個方法只是所有的瀏覽器都支援,比如ie9以下愛的都不支援。
五:寄生式繼承
這個繼承方式是與原型式繼承緊密相關的一種思路。其基本思想即:建立一個僅用於封裝繼承過程的函數,該函數內部以某種方式來增強對象,最後再像真的是它做了所有工作一樣返回對象。如下:
function creatAnother(original){
var clone = object(original); //通過調用函數建立一個新對象
clone.sayHi = function(){ //以某種方式來增強這個對象
alert(“hi”);
};
return clone; //返回這個對象
}
這個例子裡,creatAnother()函數接收了一個參數,這個參數就是將要作為新對象基礎的對象。然後把這個對象(original)傳遞給object()函數,將返回的結果賦值給clone.再為clone對象添加一個新方法sayHi(),最後返回給clone.
ps:使用寄生式繼承來為對象添加函數,會由於不能做到函數複用而降低效率。
六:寄生組合式繼承
組合繼承是最常用的一種繼承模式,不過它也有不足。其問題就是無論什麼情況下都會調用兩次超類型建構函式:一次是在建立子類型原型的時候,另一次是在子類型建構函式內部。
看一個組合繼承的例子:
function SuperType(name){
this.name = name;
this.clone = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
SybType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
所謂的寄生式組合繼承就是通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法;基本模式如下:
function inheritPrototype(subType,SuperType){
var prototype = object(superType.prototype); //建立對象
prototype.constructor = SubType; //增強對象
SubType.prototype = prototype; //指定對象
}
這個例子inheritPrototype()函數實現了寄生式組合最簡單的形式,這個函數接收兩個參數:子類型建構函式和超類型建構函式。
建構函式的繼承
對象之間的"繼承"的五種方法。
比如,現在有一個"動物"對象的建構函式。
function Animal(){
this.species = "動物";
}
還有一個"貓"對象的建構函式。
function Cat(name,color){
this.name = name;
this.color = color;
}
怎樣才能使"貓"繼承"動物"呢?
一、 建構函式綁定
第一種方法也是最簡單的方法,使用call或apply方法,將父物件的建構函式綁定在子物件上,即在子物件建構函式中加一行:
function Cat(name,color){
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
二、 prototype模式
第二種方法更常見,使用prototype屬性。
如果"貓"的prototype對象,指向一個Animal的執行個體,那麼所有"貓"的執行個體,就能繼承Animal了。
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
代碼的第一行,我們將Cat的prototype對象指向一個Animal的執行個體。
Cat.prototype = new Animal();
它相當於完全刪除了prototype 對象原先的值,然後賦予一個新值。但是,第二行又是什麼意思呢?
Cat.prototype.constructor = Cat;
原來,任何一個prototype對象都有一個constructor屬性,指向它的建構函式。如果沒有"Cat.prototype = new Animal();"這一行,Cat.prototype.constructor是指向Cat的;加了這一行以後,Cat.prototype.constructor指向Animal。
alert(Cat.prototype.constructor == Animal); //true
更重要的是,每一個執行個體也有一個constructor屬性,預設調用prototype對象的constructor屬性。
alert(cat1.constructor == Cat.prototype.constructor); // true
因此,在運行"Cat.prototype = new Animal();"這一行之後,cat1.constructor也指向Animal!
alert(cat1.constructor == Animal); // true
這顯然會導致繼承鏈的紊亂(cat1明明是用建構函式Cat產生的),因此我們必須手動糾正,將Cat.prototype對象的constructor值改為Cat。這就是第二行的意思。
這是很重要的一點,編程時務必要遵守。下文都遵循這一點,即如果替換了prototype對象,
o.prototype = {};
那麼,下一步必然是為新的prototype對象加上constructor屬性,並將這個屬性指回原來的建構函式。
o.prototype.constructor = o;
三、 直接繼承prototype
第三種方法是對第二種方法的改進。由於Animal對象中,不變的屬性都可以直接寫入Animal.prototype。所以,我們也可以讓Cat()跳過 Animal(),直接繼承Animal.prototype。
現在,我們先將Animal對象改寫:
function Animal(){ }
Animal.prototype.species = "動物";
然後,將Cat的prototype對象,然後指向Animal的prototype對象,這樣就完成了繼承。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
與前一種方法相比,這樣做的優點是效率比較高(不用執行和建立Animal的執行個體了),比較省記憶體。缺點是 Cat.prototype和Animal.prototype現在指向了同一個對象,那麼任何對Cat.prototype的修改,都會反映到Animal.prototype。
所以,上面這一段代碼其實是有問題的。請看第二行
Cat.prototype.constructor = Cat;
這一句實際上把Animal.prototype對象的constructor屬性也改掉了!
alert(Animal.prototype.constructor); // Cat
四、 利用Null 物件作為中介
由於"直接繼承prototype"存在上述的缺點,所以就有第四種方法,利用一個Null 物件作為中介。
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
F是Null 物件,所以幾乎不佔記憶體。這時,修改Cat的prototype對象,就不會影響到Animal的prototype對象。
alert(Animal.prototype.constructor); // Animal
我們將上面的方法,封裝成一個函數,便於使用。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
使用的時候,方法如下
extend(Cat,Animal);
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
這個extend函數,就是YUI庫如何?繼承的方法。
另外,說明一點,函數體最後一行
Child.uber = Parent.prototype;
意思是為子物件設一個uber屬性,這個屬性直接指向父物件的prototype屬性。(uber是一個德語詞,意思是"向上"、"上一層"。)這等於在子物件上開啟一條通道,可以直接調用父物件的方法。這一行放在這裡,只是為了實現繼承的完備性,純屬備用性質。
五、 拷貝繼承
上面是採用prototype對象,實現繼承。我們也可以換一種思路,純粹採用"拷貝"方法實現繼承。簡單說,如果把父物件的所有屬性和方法,拷貝進子物件,不也能夠實現繼承嗎?這樣我們就有了第五種方法。
首先,還是把Animal的所有不變屬性,都放到它的prototype對象上。
function Animal(){}
Animal.prototype.species = "動物";
然後,再寫一個函數,實現屬性拷貝的目的。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
這個函數的作用,就是將父物件的prototype對象中的屬性,一一拷貝給Child對象的prototype對象。
使用的時候,這樣寫:
extend2(Cat, Animal);
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動物
非建構函式的繼承
一、什麼是"非建構函式"的繼承?
比如,現在有一個對象,叫做"中國人"。
var Chinese = {
nation:'中國'
};
還有一個對象,叫做"醫生"。
var Doctor ={
career:'醫生'
}
請問怎樣才能讓"醫生"去繼承"中國人",也就是說,我怎樣才能產生一個"中國醫生"的對象?
這裡要注意,這兩個對象都是普通對象,不是建構函式,無法使用建構函式方法實現"繼承"。
二、object()方法
json格式的發明人Douglas Crockford,提出了一個object()函數,可以做到這一點。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
這個object()函數,其實只做一件事,就是把子物件的prototype屬性,指向父物件,從而使得子物件與父物件連在一起。
使用的時候,第一步先在父物件的基礎上,產生子物件:
var Doctor = object(Chinese);
然後,再加上子物件本身的屬性:
Doctor.career = '醫生';
這時,子物件已經繼承了父物件的屬性了。
alert(Doctor.nation); //中國
三、淺拷貝
除了使用"prototype鏈"以外,還有另一種思路:把父物件的屬性,全部拷貝給子物件,也能實現繼承。
下面這個函數,就是在做拷貝:
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
使用的時候,這樣寫:
var Doctor = extendCopy(Chinese);
Doctor.career = '醫生';
alert(Doctor.nation); // 中國
但是,這樣的拷貝有一個問題。那就是,如果父物件的屬性等於數組或另一個對象,那麼實際上,子物件獲得的只是一個記憶體位址,而不是真正拷貝,因此存在父物件被篡改的可能。
請看,現在給Chinese添加一個"出生地"屬性,它的值是一個數組。
Chinese.birthPlaces = ['北京','上海','香港'];
通過extendCopy()函數,Doctor繼承了Chinese。
var Doctor = extendCopy(Chinese);
然後,我們為Doctor的"出生地"添加一個城市:
Doctor.birthPlaces.push('廈門');
發生了什麼事?Chinese的"出生地"也被改掉了!
alert(Doctor.birthPlaces); //北京, 上海, 香港, 廈門
alert(Chinese.birthPlaces); //北京, 上海, 香港, 廈門
所以,extendCopy()只是拷貝基本類型的資料,我們把這種拷貝叫做"淺拷貝"。這是早期jQuery實現繼承的方式。
四、深拷貝
所謂"深拷貝",就是能夠實現真正意義上的數組和對象的拷貝。它的實現並不難,只要遞迴調用"淺拷貝"就行了。
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
使用的時候這樣寫:
var Doctor = deepCopy(Chinese);
現在,給父物件加一個屬性,值為數組。然後,在子物件上修改這個屬性:
Chinese.birthPlaces = ['北京','上海','香港'];
Doctor.birthPlaces.push('廈門');
這時,父物件就不會受到影響了。
alert(Doctor.birthPlaces); //北京, 上海, 香港, 廈門
alert(Chinese.birthPlaces); //北京, 上海, 香港
目前,jQuery庫使用的就是這種繼承方法。