1. 程式人生 > >js中的繼承詳解

js中的繼承詳解

js中的繼承

假設我們有一個Animal類,我們想構造Cat類,Cat類可以繼承Animal類的屬性和方法。以這個場景為列,我來講一講我所理解的js的繼承。

  1. 構造繼承
function Animal(name){
  this.name = name;
  this.age = 15;
}

function Cat(name){
  Animal.call(this, name);
  this.catName = 'cat';
}

let o1 = new Cat('test1');
console.log(o1);
//Cat {name: "test1", age: 15, catName: "cat"}

這就是構造繼承,在子類的建構函式中,呼叫父類的建構函式,這樣父類建構函式中的屬性就出現在了子類的建構函式中。
缺點:子類無法繼承父類原型上的方法。

  Animal.prototype.say = function(){
     console.log(this.name)
  }
  o1.say;//undefined
  1. 原型繼承
  function Cat2(name){
    this.catName = name;
  }
  Cat2.prototype = new Animal();
  let o2 = new Cat2('test2');
  console.log(o2);
  // Cat2 {catName: "test2"}
  o2.name;//undefined
  o2.age;//15
  o2.say;
  //ƒ (){
  //  console.log(this.name)
  //}

這種方法的缺點是無法進行父類傳參初始化屬性的繼承,而且繼承的父類的屬性是所有子類共享的new Animal()這個物件例項上的屬性,這就會導致如果更改了該物件例項的屬性,那麼這個影響就是所有子類例項共享的。
我們對父類做出如下更改

  function Animal(name){
    this.name = name;
    this.age = 15;
    this.friend = [1,2,3];
  }

  function Cat21(name){
    this.catName = name;
  }

  Cat21.prototype = new Animal();
  
  let o3 = new Cat21('o3');
  let o4 = new Cat21('o4');
  console.log('o3.age',o3.age);
  console.log('o4.age',o4.age);
  console.log('o3.friend',o3.friend);
  console.log('o4.friend',o4.friend);
  
  //更改陣列friend
  o3.friend.push(4);
  console.log('o3.friend',o3.friend);
  console.log('o4.friend',o4.friend);
  
  //更改屬性age
  o3.age = 19;
  console.log('o3.age', o3.age);
  console.log('o4.age', o4.age);

輸出如下

    o3.age 15
    o4.age 15
    o3.friend (3) [1, 2, 3]
    o4.friend (3) [1, 2, 3]
    o3.friend (4) [1, 2, 3, 4]
    o4.friend (4) [1, 2, 3, 4]
    o3.age 19
    o4.age 15

我們可以發現更改friend時,這個更改在子類例項上都發生了改變,而更改age時,只在更改的例項上發生了變化。
我們來看看o3和o4
avatar
可以發現o3上的age屬性是直接在例項上的,而o3和o4的例項本身都是沒有friend屬性的。
這是為什麼呢?
這是因為在查詢物件的屬性和方法時,是沿著原型鏈進行查詢的,而你更改屬性時,如果這個屬性不是一個引用型別,是會直接為例項物件本身新增一個相應的屬性,如果是引用型別,是會改變所引用物件指向的內容的。 而原型鏈上的方法,是等同於非引用型別的屬性的。
因此,如果我們想要修改age,讓所有例項共享修改後的結果,我們可以這麼修改
o3.__proto__.age = 99;
當然,這個前提是子類例項上還沒有新增age屬性。

  1. 構造 + 原型繼承
     function Animal(name){
       this.name = name;
       this.age = 15;
       this.friend = [1,2,3];
     }
    
     Animal.prototype.say = function(){
         console.log(this.name);
     }
     function Cat3(name){
       Animal.call(this, name);
       this.catName = 'test3';
     }
     Cat3.prototype = new Animal();
    
     let o31 = new Cat3('o31');
     let o32 = new Cat3('o32');
    
     console.log('o31', o31);
     console.log('o32', o32);
     console.log('o31.friend', o31.friend);
     console.log('o32.friend', o32.friend);
    
     o31.friend.push(4);
     console.log('o31.friend', o31.friend);
     console.log('o32.friend', o32.friend);
    
     o31.say();
    

得到的輸出如下:
avatar

可以發現父類的屬性和方法子類例項都可以繼承,但是還是有一個問題,那就是父類的建構函式多執行了一次,而這個多餘的操作是不必要的。

  1. 原型+構造+優化1
    function Cat4(name){
      Animal.call(this, name);
      this.catName = name;
    }
    
    Cat4.prototype = Object.create(Animal.prototype);
    
     let o41 = new Cat4('o41');
     let o42 = new Cat4('o42');
    
     console.log('o41', o41);
     console.log('o42', o42);
     console.log('o41.friend', o41.friend);
     console.log('o42.friend', o42.friend);
    
     o41.friend.push(4);
     console.log('o41.friend', o41.friend);
     console.log('o42.friend', o42.friend);
    
     o41.say();
    

如此,我們就少進行了一個父類建構函式的執行,但是這還是有問題的

  o41 instanceof Cat4; //true
  o41 instanceof Animal; //true
  o41.constructor;//
  //ƒ Animal(name){
  //  this.name = name;
  //  this.age = 15;
  //  this.friend = [1,2,3];
  //}

也就是無法通過instanceof來確認例項物件是由父類構造還是子類構造。

  1. 原型+建構函式+優化2
    由於instanceof的本質就是在原型鏈上進行constructor屬性的查詢 ,我們可以做如下優化
 function Cat5(name){
   Animal.call(this, name);
   this.catName = name;
 }

  Cat5.prototype = Object.create(Animal.prototype);
  Cat5.prototype.constructor = Cat5;

  let o51 = new Cat5('o51');
  o51.constructor;
  //ƒ Cat5(name){
  //Animal.call(this, name);
  //this.catName = name;
  //}

以上是個人總結的繼承相關的知識點,歡迎老鐵們在評論區進行補充。