【javascript基礎】七、繼承

原文: 【javascript基礎】七、繼承

前言

因爲本人水平有限,因此有些高手以爲如今寫的內容偏容易,要一點點來嘛,今天和你們學習或者複習一下javascript的繼承。我也就是儘可能寫吧······javascript

繼承

javascript的繼承其實主要就是經過原型鏈來實現的,原型鏈咱們以前已經和你們一塊兒學習過,這裏就不浪費你們的時間了。javascript連類都沒有,還說啥繼承呢,這仍是模擬類的繼承。《javascript高級程序設計》上分紅了幾個方式,有的書上分爲類式繼承,原型式繼承,這就是模擬其餘語言類的繼承,還有什麼用摻元類實現的,在這裏都和你們說下。html

原型鏈

在這裏在說一下原型鏈的概念,由於javascript的繼承都是經過原型鏈來模擬的,因此在這裏幫助你們理解一下。咱們知道,每個構造函數都有一個原型對象,這個原型對象中包含一個指向構造函數的指針,同時每個實例都有一個指向原型對象的內部指針。好好想一下這個關係,當咱們訪問一個實例的屬性時,如今實例中查找,沒找到經過內部指針去原型中查找,仍是沒有再經過原型的內部指針查找原型的原型對象,一直迭代下去。嗯,就是這樣,java

如今咱們知道了原型鏈是這樣的話,咱們想要繼承的實現,咱們要把父類的屬性和方法放在子類的原型對象中就能夠了,這樣new出來的實例就會查找原型中的屬性和方法了,那這樣就能夠實現繼承了,那咱們要怎樣將父類的屬性和方法放在子類的原型中呢?咱們重寫子類的原型對象是否是就能夠了,這裏有個選擇的問題,咱們可讓子類的原型對象指向父類的原型對象,也能夠指向一個父類的實例,假如如今咱們將它指向了父類的原型對象,咱們知道父類構造函數中的屬性就不會在子類中獲得繼承,看個例子就知道了app

//父類
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 

其實這是另一種方式的雛形,寄生組合模式的雛形,下文我會講到,這裏暫且放過。函數

咱們如今再看看指向一個實例對象的狀況post

//父類
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來判斷實例與原型的關係,只要實例和原型鏈中出現過構造函數,就會返回truethis

console.log(Tom instanceof Cat);//true
console.log(Tom instanceof Animal);//true
console.log(Tom instanceof Object);//true

原型鏈的問題:其實這個和構造對象原型鏈的問題是同樣的,主要是原型對象的屬性是一個引用類型,會引發一些問題。這是由於全部的實例共用原型對象的屬性,當屬性爲引用類型時,任何一個實例對這個對象的修改會影響全部的實例。例子來了url

//父類
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"

原型鏈還有一個問題就是,不能向父類的構造函數中傳遞參數,就是這樣的我想給每個子類起一個名字,這裏是沒法辦到的,由於我給父類的名字都掛在了子類的原型上了。例如spa

//父類
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高級程序設計》的繼承部分,多看幾遍,本身好好想一想它們的優缺點,就知道該如何設計繼承了,本身在謝謝實例就會明白這些方式是大神們怎麼想出來的。

相關文章
相關標籤/搜索