前言
由於本人水平有限,所以有些高手覺得現在寫的內容偏容易,要一點點來嘛,今天和大家學習或者復習一下javascript的繼承。我也就是盡量寫吧······
繼承
javascript的繼承其實主要就是通過原型鏈來實現的,原型鏈我們之前已經和大家一起學習過,這里就不浪費大家的時間了。javascript連類都沒有,還說啥繼承呢,這還是模擬類的繼承。《javascript高級程序設計》上分成了幾個方式,有的書上分為類式繼承,原型式繼承,這就是模擬其他語言類的繼承,還有什么用摻元類實現的,在這里都和大家說下。
原型鏈
在這里在說一下原型鏈的概念,因為javascript的繼承都是通過原型鏈來模擬的,所以在這里幫助大家理解一下。我們知道,每一個構造函數都有一個原型對象,這個原型對象中包含一個指向構造函數的指針,同時每一個實例都有一個指向原型對象的內部指針。好好想一下這個關系,當我們訪問一個實例的屬性時,現在實例中查找,沒找到通過內部指針去原型中查找,還是沒有再通過原型的內部指針查找原型的原型對象,一直迭代下去。嗯,就是這樣,
現在我們知道了原型鏈是這樣的話,我們想要繼承的實現,我們要把父類的屬性和方法放在子類的原型對象中就可以了,這樣new出來的實例就會查找原型中的屬性和方法了,那這樣就可以實現繼承了,那我們要怎樣將父類的屬性和方法放在子類的原型中呢?我們重寫子類的原型對象是不是就可以了,這里有個選擇的問題,我們可以讓子類的原型對象指向父類的原型對象,也可以指向一個父類的實例,假如現在我們將它指向了父類的原型對象,我們知道父類構造函數中的屬性就不會在子類中得到繼承,看個例子就知道了
//父類 function Animal(){ this.className = "動物"; } //父類原型 Animal.prototype.getClassName = function(){ console.log(this.className ); } //子類 function Cat(){} //重寫子類原型 Cat.prototype = Animal.prototype; var Tom = new Cat(); Tom.getClassName();//undefined
其實這是另外一種方式的雛形,寄生組合模式的雛形,下文我會講到,這里暫且放過。
我們現在再看看指向一個實例對象的情況
//父類 function Animal(){ this.className = "動物"; } //父類原型 Animal.prototype.getClassName = function(){ console.log(this.className ); } //子類 function Cat(){} //重寫子類原型 Cat.prototype =new Animal(); var Tom = new Cat(); Tom.getClassName();//動物
這下子大家會明白了,實例是把構造函數中this的屬性和原型中的屬性結合起來了,如果指向原型對象那么構造函數中的屬性就不會被繼承。
這就是繼承的最基礎和最核心的東西,這還不完善,重新原型對象我們知道,要增加一個constructor屬性,這里不添加了,不明白的看之前的原型與原型鏈的那篇文章。javascript用instanceof來判斷實例與原型的關系,只要實例和原型鏈中出現過構造函數,就會返回true
console.log(Tom instanceof Cat);//true console.log(Tom instanceof Animal);//true console.log(Tom instanceof Object);//true
原型鏈的問題:其實這個和構造對象原型鏈的問題是一樣的,主要是原型對象的屬性是一個引用類型,會引起一些問題。這是因為所有的實例共用原型對象的屬性,當屬性為引用類型時,任何一個實例對這個對象的修改會影響所有的實例。例子來了
//父類 function Animal(){ this.className = "動物"; this.colors = ["black"]; } //父類原型 Animal.prototype.getClassName = function(){ console.log(this.className ); } //子類 function Cat(){} //重寫子類原型 Cat.prototype = new Animal; var Tom = new Cat(); console.log(Tom.colors);//"black" Tom.colors.push("yellow"); var Garfield= new Cat(); console.log(Garfield.colors);//"black", "yellow"
原型鏈還有一個問題就是,不能向父類的構造函數中傳遞參數,就是這樣的我想給每一個子類起一個名字,這里是無法辦到的,因為我給父類的名字都掛在了子類的原型上了。例如
//父類 function Animal(name){ this.name = name; } //子類 function Cat(){} //重寫子類原型 Cat.prototype = new Animal("無名氏"); var Tom = new Cat(); console.log(Tom.name);//無名氏 var Garfield= new Cat(); console.log(Garfield.name);//無名氏
這里要起一個名字,所有實例都會影響,所以說沒有辦法在不影響所有實例的情況下給父類傳遞參數。
借用構造函數
這個方式可以解決上面的問題,我們知道上面的原型鏈的方法是子類的原型對象指向了父類的實例,就是把所有父類的屬性都掛在了子類的原型對象上,所有就會出現所有實例共享同一個屬性引發的問題,那我們可以換一種思路,我們把一些父類的屬性放在子類的構造函數中,就是在子類的構造函數中的this添加屬性,這樣就不需要所有的屬性都弄到子類的原型對象上了,這樣每個子類的實例都會有自己的屬性和方法,不用共享原型中的屬性了。這是一個簡單的思路,我們在子類的構造函數中給this添加父類的屬性,我們想到了之前的apply和call方法,看例子
//父類 function Animal(){ this.className = "動物"; this.colors = ["black"]; } //子類 function Cat(){ Animal.call(this);//相當於 this.className = "動物";this.colors = ["black"]; }var Tom = new Cat(); console.log(Tom.colors);//"black" Tom.colors.push("yellow"); console.log(Tom.colors);//"black", "yellow" var Garfield= new Cat(); console.log(Garfield.colors);//"black"
這時候你也可以給父類傳參數了,因為這些屬性都添加了子類構造函數中了,看例子
//父類 function Animal(name){ this.name = name; } //子類 function Cat(name){//傳入參數 Animal.call(this,name); } var Tom = new Cat("Tom"); console.log(Tom.name);//Tom var Garfield= new Cat("Garfield"); console.log(Garfield.name);//Garfield
借用構造函數問題:這個又回歸到了構造函數模式上出現的問題了,我們所有的方法都是在構造函數上定義的,無法復用。
組合繼承(類式繼承)
原型鏈和借用構造函數結合一起,使用原型鏈實現原型屬性和方法的繼承,使用借用構造函數實現對實例屬性的繼承,這樣通過在原型上定義方法實現函數的復用,又能保證每個實例都有自己的屬性。上例子
//父類 function Animal(name){ this.name = name; this.colors = ["black"]; } //父類原型 Animal.prototype.getName = function(){ return this.name; } //子類 function Cat(name,age){//傳入參數 Animal.call(this,name); this.age = age; } Cat.prototype = new Animal("無名氏"); Cat.prototype.constructor = Cat; Cat.prototype.getAge = function(){ return this.age; } var Tom = new Cat("Tom",20); console.log(Tom.getName() +" : "+ Tom.getAge());//Tom : 20 Tom.colors.push("red"); var Garfield= new Cat("Garfield",21); console.log(Garfield.getName() +" : "+ Garfield.getAge());//Garfield : 21 console.log(Garfield.colors);//black
這是最常用的繼承方式。有些書叫這種為類式繼承,把這種通過構造函數方式來實現繼承的叫做類式繼承,上面的我們可以把Animal看成一個類,通過構造函數原型鏈之間的關系實現繼承。
原型繼承
這種沒有方式類的概念,也就是沒有使用構造函數來實現,就是使一個函數的原型指向一個原有的對象,通過這個函數來創建一個新的對象。上例子
function create(obj){ function F(){}; F.prototype = obj; return new F(); } var Animal = { name : "無名氏", colors : ["black"] } var Tom = create(Animal); console.log(Tom.name);//"無名氏"
這就是原型繼承,可以看出它存在不少問題,只有在特定的情況下可以使用該方式,無法判斷類與實例之間的關系,共享引用類型屬性的問題等等。
寄生式繼承
如果知道了上面的知識,這個很好理解了,我們在創建對象那章的時候,就提到了寄生構造對象,所謂的寄生就是在函數的內部通過某種方式來增強對象之后,在返回這個對象,那么寄生式繼承也類似
function create(obj){ function F(){}; F.prototype = obj; return new F(); } var Animal = { name : "無名氏", colors : ["black"] } function getCat(obj){ //創建對象繼承自obj var newObj= create(obj); //增加方法 newObj.getName = function(){ return this.name; }; return newObj; } var Tom = getCat(Animal); console.log(Tom.getName());//"無名氏"
這個看看就知道是怎么回事了,在函數內部繼承一個對象之后,又增加了方法,之后返回這個對象。
寄生組合式繼承
組合繼承上面我們說完了,組合繼承還有一個問題就是,任何時候會調用兩次父類的構造函數,一次是創建子類的原型的時候,另一次是在子類的構造函數內部。看看就知道了
//父類 function Animal(name){ this.name = name; this.colors = ["black"]; } //父類原型 Animal.prototype.getName = function(){ return this.name; } //子類 function Cat(name,age){//傳入參數 Animal.call(this,name);//第二次調用Animal() this.age = age; } Cat.prototype = new Animal("無名氏");//第一次調用Animal() Cat.prototype.constructor = Cat; Cat.prototype.getAge = function(){ return this.age; } var Tom = new Cat("Tom",20); console.log(Tom.getName() +" : "+ Tom.getAge());//Tom : 20
我們分析一下這個過程:第一次調用的時候,在Cat.prototype對象上添加了name和colors屬性,添加到了子類的原型對象上,第二次調用父類的構造函數時,是將name和colors屬性添加到了子類的實例上,也就是說子類的原型對象和實例中都有了這兩個屬性,實例中的屬性屏蔽了原型中屬性。
我們想一下怎樣才能解決這問題呢?我們可以這樣,讓子類的原型對象直接指向父類的原型對象,就像文章開始我們說的那么選擇的問題,我們這次使用父類的原型對象,這里可以使用,是因為我們結合使用了借用構造模式,可以繼承父類構造函數中的屬性了,看看例子先
//父類 function Animal(name){ this.name = name; this.colors = ["black"]; } //父類原型 Animal.prototype.getName = function(){ return this.name; } //子類 function Cat(name,age){//傳入參數 Animal.call(this,name);//第二次調用Animal() this.age = age; } Cat.prototype = Animal.prototype;//指向父類的原型 Cat.prototype.constructor = Cat; Cat.prototype.getAge = function(){ return this.age; } var Tom = new Cat("Tom",20); console.log(Tom.getName() +" : "+ Tom.getAge());//Tom : 20
這樣是可以的,但是我們這里就有問題了,我們在給子類的原型指定constructor屬性時,修改了父類的constructor屬性,
console.log(Animal.prototype.constructor); /* function Cat(name,age){//傳入參數 Animal.call(this,name);//第二次調用Animal() this.age = age; } */
所以我們不能直接這樣指向父類的原型,要通過一種中轉,使子類的原型和父類原型指向不同的對象,就是使用原型模式繼承,建一個對象,這個對象的原型指向父類的原型,之后子類的原型對象再指向這個對象,這樣就使子類的原型和父類原型指向不同的對象。
//父類 function Animal(name){ this.name = name; this.colors = ["black"]; } //父類原型 Animal.prototype.getName = function(){ return this.name; } //子類 function Cat(name,age){//傳入參數 Animal.call(this,name);//第二次調用Animal() this.age = age; } function F(){}; F.prototype = Animal.prototype; Cat.prototype = new F(); Cat.prototype.constructor = Cat; /*封裝起來就是這樣 function create(obj){ function F(){}; F.prototype = obj; return new F(); } function inheirt(sub,sup){ var pro = create(sup.prototype); pro.constructor = sub; sub.prototype = pro; }
inherit(Cat,Animal); */ Cat.prototype.getAge = function(){ return this.age; } var Tom = new Cat("Tom",20); console.log(Tom.getName() +" : "+ Tom.getAge());//Tom : 20
就這樣循序漸進,我們就完成了javascript的繼承的內容。
參元類(復制繼承)
復制繼承,顧名思義就是一個一個復制原型對象的屬性,將給定的類的原型的屬性循環復制到指定的原型中,
function inherit(subClass,supClass){ for(var name in supClass.prototype){ if(!subClass.prototype[name]){ subClass.prototype[name] = supClass.prototype[name] } } } function Animal(){} Animal.prototype.aname = "無名氏"; function Cat(){}; inherit(Cat,Animal); var Tom = new Cat(); console.log(Tom.aname);//無名氏
就是復制繼承,參元類就是通過這種方式來實現的,參元類是包含了一系列的通用方法,如果哪個類想用這些方法就適使用這種方式來繼承參元類。
小結
就這樣循序漸進,我們就完成了javascript的繼承的內容,繼承這塊的知識初學者要多看書,《javascript高級程序設計》的繼承部分,多看幾遍,自己好好想想它們的優缺點,就知道該如何設計繼承了,自己在謝謝實例就會明白這些方式是大神們怎么想出來的。