1. 程式人生 > >複習面向物件 -- 繼承

複習面向物件 -- 繼承

本文是面向物件第三部分--繼承,相對於前兩個,篇幅過長,理解稍微難點,不過多思考多敲敲,會一下子茅塞頓開,就懂了,不太懂面向物件-建立物件的,可以看這篇文章,傳送門,不太懂面向物件-原型與原型鏈的,可以看這篇文章,傳送門

面向物件繼承

  許多語言都支援兩種繼承方式:介面繼承實現繼承   介面繼承只繼承方法簽名(方法簽名由方法名稱和一個引數列表(方法的引數的順序和型別)組成,java所屬)。   而js中的函式沒有簽名,所以無法實現介面繼承,只能實現實現繼承,而實現繼承主要依靠原型以及原型鏈實現的

  高程中寫到:假如我們讓原型物件等於另一個型別的例項,結果會怎麼樣呢?顯然,此時的原型物件將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個型別的例項,那麼上述關係依然成立,如此層層遞進,就構成了例項與原型的鏈條

。這就是所謂原型鏈的基本概念

  js的繼承是依據原型以及原型鏈來實現的,回顧前幾節的知識,可以得知,一個建構函式創建出來的例項,都可以訪問到該建構函式的的屬性,方法,還有建構函式的原型的屬性以及方法。

  首先 先了解建構函式、原型和例項的關係:每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。通俗的說:例項通過內部指標可以訪問到原型物件,原型物件通過constructor指標,找到其建構函式。

那麼js中的繼承的基本思路就是利用原型以及原型鏈的特性,改變內部指標的指向,進而實現繼承。

 原型鏈繼承:

  js所有繼承都基於原型鏈繼承。

function Animal(){
    this.type = 'animal';
}
Animal.prototype.feature = function(){
    alert(this.type);
}
function Cat(name,color){
    this.name = name;
    this.color = color;
}  
Cat.prototype = new Animal();

var tom = new Cat('tom','blue');
console.log(tom.name);  // 'tom'
console.log(tom.color); //
'blue' console.log(tom.type); // 'animal' tom.feature() // 'animal'

  這個例子中有建立了兩個建構函式,Animal建構函式有一個type屬性和feature方法。Cat建構函式有兩個屬性:name和color。例項化了一個Animal物件,並且掛載到了Cat函式的原型上,相當於重寫了Cat的原型,所以Cat函式擁有Animal函式的所有屬性和方法。  然後又Cat函式new出一個tom物件,tom物件擁有Cat函式的屬性和方法,因此也擁有Animal的屬性和方法。

  通過上面的例子,可以總結:通過改變建構函式的原型,進而實現繼承。

  ps:  1 如果在繼承原型物件之前,產生的例項,其內部指標還是隻想最初的原型物件。

function Animal(){
    this.type = 'animal';
}
Animal.prototype.feature = function(){
    alert(this.type);
}
function Cat(name,color){
    this.name = name;
    this.color = color;
}
Cat.prototype.type = 'cat';
Cat.prototype.feature = function(){
    alert(this.type);
}    
var tom = new Cat('tom','blue');
Cat.prototype = new Animal();// 先例項化物件,再重寫原型,結果指標還是指向最初的原型
console.log(tom.name);  // 'tom'
console.log(tom.color); // 'blue'
console.log(tom.type);  // 'cat'  ----- 是最初的type
tom.feature()    // 'cat'

  從列印結果來看:new出來的tom物件,在Cat.prototype重寫原型之後,依然還是指向沒重寫的原型上。

  2 在通過原型鏈實現繼承時,不能使用物件字面量建立原型方法。因為這樣做就會重寫原型鏈。

function Animal(){
    this.type = 'animal';
    this.size = 'small'
}
Animal.prototype.feature = function(){
    alert(this.type);
}
function Cat(name,color){
    this.name = name;
    this.color = color;
}  
Cat.prototype = new Animal();
// 使用字面量新增新方法,會導致上一行程式碼無效
Cat.prototype = {
    type:'cat',
    feature:function(){
        alert(this.type)
    }
}
var tom = new Cat('tom','blue');
console.log(tom.name);  // 'tom'
console.log(tom.color); // 'blue'
console.log(tom.type);  // 'cat'
tom.feature()    // 'cat'  
console.log(tom.size)  // undefined   ----- tom拿不到size屬性

  由列印結果可知:tom這個物件拿不到將繼承的size屬性,所以用字面量新增屬性或方法,會切斷將繼承的與例項之間的聯絡。

  原型鏈繼承並不是完美的,用原型鏈實現繼承,有一定的問題存在

  1 首先 值型別與引用型別在引數傳遞時,方式是不一樣的

   值型別 將值本身拷貝一份賦值給其他變數,若該值發生變化,也不會影響到其他變數。   引用型別 將指標(記憶體中的地址)拷貝一份 賦值給其他變數;若記憶體中的地址內容發生改變,其他變數內的內容也會發生變化。

var a = 11;
var b = a;

console.log(a,b) // 11 11
b = 22;
console.log(a,b) // 11 22


var obj ={
    name:'peter',
    age:18
};
var m = obj;
console.log(m) // {name: "peter", age: 18}
m.name = 'tom';
m.age = 22;
console.log(obj) // {name: "tom", age: 22}

  同樣的道理,在原型鏈繼承中,包含引用型別值的原型屬性會被所有例項共享,如果某一個例項更改了屬性或方法,會影響到原型屬性,進而影響所有的例項。

function Animal(){
    this.type = 'animal';
    this.size = ['large','small'];
}
Animal.prototype.feature = function(){
    alert(this.type);
}
function Cat(name,color){
    this.name = name;
    this.color = color;
}  
Cat.prototype = new Animal();

var tom = new Cat('tom','blue');
var peter = new Cat('peter','yellow')
console.log(tom.size);  // ["large", "small"]
tom.size.push('middle');
console.log(tom.size);  // ["large", "small", "middle"]
console.log(tom.size);  // ["large", "small", "middle"]

  通過列印結果可知,在一個例項新增一個顏色時,同時也影響了其他例項。

  2 在高程上說還有個問題,在建立子型別(Cat)的例項時,不能向超型別(Animal)的建構函式中傳遞引數。意思就是沒有辦法在不影響所有物件例項的情況下,給超型別(Animal)的建構函式傳遞引數

function Animal(type){
    this.type = type;
    this.size = ['large','small'];
}
Animal.prototype.feature = function(){
    alert(this.type);
}
function Cat(type,name,color){
    this.name = name;
    this.color = color;
}  
Cat.prototype = new Animal();

var tom = new Cat('animal','tom','blue');
console.log(tom.type); // undefined

  當給被繼承的建構函式傳引數時,發現為undefined,所以原型鏈繼承無法傳引數。

  因此原型鏈繼承有這兩個問題,所以在實踐中很少使用原型鏈繼承。

 借用建構函式繼承

  借用建構函式繼承的基本思想:就是在子型別建構函式的內部使用 apply() 和 call() 方法呼叫超型別建構函式裡的屬性或方法。

  ps:函式只不過是在特定環境中執行程式碼的物件,因此通過使用 apply() 和 call() 方法也可以在(將來)新建立的物件上執行建構函式。

function Animal(name){
    this.name = name;
    this.size = ["large", "small"];
}
Animal.prototype.say = function(){
    alert(this.name);
}
function Cat(name,age){
    Animal.call(this,name);
    this.age = age;

}
var tom = new Cat('tom',18);
var peter = new Cat('peter',22);
console.log(tom); // {name: "tom", size: Array[2], age: 18};
console.log(peter); // {name: "peter", size: Array[2], age: 22};

tom.size.push('middle');
console.log(tom.size);  // ["large", "small", "middle"]
console.log(peter.size); // ["large", "small"]

tom.say();  // Uncaught TypeError: tom.say is not a function

  由上述列印的結果,我們可以得出以下結論:

   1 可以往超型別(Animal)傳引數,Animal可以接受一個引數,將引數賦值給一個屬性,所以在Cat建構函式內部呼叫Animal時,就是給Cat的例項設定該屬性。

   ps:為了確保Animal 建構函式不會重寫Cat的屬性,可以在呼叫超型別建構函式後,再新增應該在子型別中。

function Cat(name,age){
    Animal.call(this,name);
    this.age=age;
    this.name = 'jerry'; // 如果該屬性寫在Animal.call(this,name);之前的話,沒有作用,還是被呼叫的給覆蓋了
}

  2 因為是子型別呼叫超型別,所以每個子型別呼叫的都是超型別的初始化的屬性或內部方法。每個例項都互不影響。  3 超型別的原型上的方法對子型別不可見,不可被呼叫。

  借用建構函式繼承最主要的就是子型別利用call或apply呼叫超型別的方式去使用該方法或屬性。但是很顯然也有缺點:

   1 由於每個子型別宣告自己屬性或方法,而且別人不能使用,所以不能複用。   2 無法呼叫超型別的原型上的方法。

 組合繼承

  組合繼承是採用了原型鏈繼承和借用建構函式繼承的方式。  其基本思想就是:原型鏈繼承實現對原型屬性和方法的繼承,借用建構函式繼承實現對例項屬性的繼承。

function Animal(name){
    this.name = name;
    this.size = ["large", "small"];
}
Animal.prototype.say = function(){
    alert(this.name);
}
function Cat(name,age){
    Animal.call(this,name); // 呼叫Animal的屬性
    this.age = age;

}
Cat.prototype = new Animal(name); // 呼叫Ainaml的原型上的方法。
Cat.prototype.constructor = Cat;  // 保證Cat的原型上的構造器物件還是指向Cat。
Cat.prototype.skill = function(){
    alert('running');
}
var tom = new Cat('tom',18);
var peter = new Cat('peter',22);
console.log(tom); // {name: "tom", size: Array[2], age: 18};
console.log(peter); // {name: "peter", size: Array[2], age: 22};

tom.size.push('middle');
console.log(tom.size);  // ["large", "small", "middle"]
console.log(peter.size); // ["large", "small"]

tom.say(); // tom
peter.say(); // peter

  上面的例子:在Cat建構函式裡使用call呼叫Animal裡屬性,並且在Cat的原型上例項化Animal,進而呼叫Animal原型上的方法。

  ps:子型別擴充套件方法時要放在原型鏈繼承之後,因為原型鏈繼承後,重寫了其constructor屬性,導致沒繼承前的屬性或方法失效。

Cat.prototype = new Animal(name); // 呼叫Ainaml的原型上的方法。
Cat.prototype.constructor = Cat;  // 保證Cat的原型上的構造器物件還是指向Cat。
Cat.prototype.skill = function(){   // 該一步要放在呼叫Animal原型方法之後,如果放在前面,會導致其skill方法失效。
    alert('running');
}

這樣融合兩者的優點,摒棄了缺點。成為最常用的繼承方式。但是也有一個不足:無論什麼情況下都會呼叫兩次超型別建構函式:1 在建立子型別原型的時候,2 在子型別建構函式內部。

 原型式繼承

  原型式繼承是道格拉斯.克羅克福德提出的繼承方式,其基本思想是:藉助原型可以基於已有的物件建立新物件,同時不必因此建立自定義型別。主要函式如下:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

  這個函式主要是在內部建立了一個建構函式,該建構函式的原型就是傳來的物件(繼承),並且返回這個建構函式的例項。  這種方式的要求是必須有一個物件,作為另一個物件的基礎,通過物件的淺拷貝的方式,建立這個物件的副本作為新物件使用,在該基礎上進行修改。

var peter = {
    name:'peter',
    age:18,
    say:function(){
        alert(this.name);
    }
}
var tom = object(peter);
console.log(tom);  // F {}
tom.say();  // peter

tom.name = 'tom';
tom.age = 22;
console.log(tom);  // F {name: "tom", age: 22}
tom.say();  // tom

  由上述結果可知,新建立的物件,返回的是物件,但是列印tom.say(),出現的是peter,說明新物件呼叫了原型上的屬性和方法。隨後在修改後新物件的屬性時,會返回新的屬性和方法。  但是顯然易見:此模式就是新物件是利用原要繼承的物件掛載到原型上的原理,去使用原型上的屬性和方法,然後修改其屬性和方法。同樣如果不修改屬性值,會被所有例項共享。

  主要適用於:在沒必要興師動眾的建立建構函式,而只想讓一個物件與另一個物件保持類似的情況下,可以使用原型式繼承。

  在es5出來一個新方法,可以替代克羅克福德建立的函式: Object.create(),前提條件只傳一個引數情況下。

var tom = Object.create(peter);
console.log(tom);  // F {}
tom.say();  // peter

tom.name = 'tom';
tom.age = 22;
console.log(tom);  // F {name: "tom", age: 22}
tom.say();  // tom

  tip:Object.create()

   接受兩個引數:    1.必需。 要用作原型的物件。可以為 null。    2.可選。 包含一個或多個屬性描述符的 JavaScript 物件。“資料屬性”是可獲取且可設定值的屬性。     資料屬性描述符包含 value 特性,以及 writable、enumerable 和 configurable 特性。如果未指定最後三個特性,則它們預設為 false。    返回值:一個具有指定的內部原型且包含指定的屬性(如果有的話)的新物件。

寄生式繼承

  寄生式繼承也是克羅克福德提出的,並在原型式繼承上進行推廣的。其基本思想就是:即建立一個僅用於封裝繼承過程的函式,該函式在內部以某種方式來增強物件,最後返回這個物件。  該繼承方式最大的特點就是封裝成一個函式,在內部擴充套件物件的屬性或方法

function inherit(o){
    var clone = Object.create(o);  // 通過呼叫函式建立一個物件
    clone.type = 'people';    // 擴充套件物件屬性或方法
    clone.say=function(){
        alert(this.name);
    }
    return clone;
}
var peter = {
    name:'peter'
}
var perterSon = inherit(peter);
console.log(perterSon.type);    // people
perterSon.say();  // peter

  inherit函式中返回一個新建立的物件,這個物件有擴充套件的方法和屬性,另一個物件在呼叫這個方法時會繼承擴充套件屬性和方法。  由此可見,擴充套件方法已經寫死了,所以不能不復用,進而降低效率。

  使用情況:在主要考慮物件而不是自定義型別和建構函式的情況下,可以採用寄生式繼承。

 寄生組合式繼承

  在組合繼承的方式中,有一個不足,就是多次呼叫超型別建構函式,為了避免這個情況,寄生組合式繼承就出現了。  其基本思路是:通過借用建構函式來繼承屬性,用原型鏈的混成形式來繼承方法。不必為了指定子型別的原型而呼叫超型別的建構函式。  最簡單的說法:使用寄生式繼承來繼承超型別的原型,然後將結果指定給子型別的原型。

function inheritPrototype(sub,supers){
    var clone = Object.create(supers.prototype);
    clone.constructor = sub;
    sub.prototype = clone;
}

  這個函式實現了三個步驟:(先傳兩個引數,一個子型別建構函式sub,一個超型別建構函式super。)    1 建立超型別原型的一個副本。    2 為建立的副本新增constructor屬性,從而彌補重寫原型而失去的預設的constructor屬性。    3 將新建立的物件(副本)賦值給子型別的原型。

function Animal(name){
    this.name = name;
    this.size = ["large", "small"];
}
Animal.prototype.say = function(){
    alert(this.name);
}
function Cat(name,age){
    Animal.call(this,name); 
    this.age = age;

}
inheritPrototype(Cat,Animal);
Cat.prototype.skill = function(){
    alert('running')
}
var tom = new Cat('tom',18);
console.log(tom); // Cat {name: "tom", size: Array(2), age: 18}
tom.say();   // tom
tom.skill();  // running

  由上述可以得知:也能實現組合繼承所實現的方案,只不過只調用了一次超型別建構函式,提高了效率,同時原型鏈也能保持不變,能夠使用instanceof和isPrototypeOf()方法,組合繼承也是一樣的。

console.log(tom instanceof Cat) // true;
console.log(tom instanceof Animal) // true

  tip:  1 instanceof    該運算子用來檢測 constructor.prototype 是否存在於引數 object 的原型鏈上。返回值是一個bool。

obj instanceof Object // 例項obj在不在Object建構函式中

   意思就是:檢測Object.prototype是否存在於引數obj的原型鏈上。

  2 isPrototypeOf()    該函式用於指示物件是否存在於另一個物件的原型鏈中。如果存在,返回true,否則返回false。   該方法屬於Object物件,由於所有的物件都"繼承"了Object的物件例項,因此幾乎所有的例項物件都可以使用該方法。

後記:   在複習面向物件中,由於篇幅過長,將知識點分成了三部分,面向物件--建立物件面向物件--原型與原型鏈,面向物件--繼承,對應的知識點我放在了github裡,有需要的可以去clone學習,覺得好的話,給個star。   文章有不對或者不理解的地方,請私信或者評論,一起討論進步。 參考資料:   javascript高階程式設計(第三版)