【javascript基礎】7、繼承


前言

由於本人水平有限,所以有些高手覺得現在寫的內容偏容易,要一點點來嘛,今天和大家學習或者復習一下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高級程序設計》的繼承部分,多看幾遍,自己好好想想它們的優缺點,就知道該如何設計繼承了,自己在謝謝實例就會明白這些方式是大神們怎么想出來的。

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM