Javascript面向對象與繼承

衆所周知,Javascript是一門面向對象的語言,若是說針對面向對象來發問的話,我會想到兩個問題,在js中,類與實例對象是如何建立的,類與實例對象又是如何實現繼承的。es6

面向對象

如何聲明一個類

ES5中,尚未類的概念,而是經過函數來聲明;到了ES6,有了class關鍵字,則經過class來聲明app

// 類的聲明
      var Animal = function () {
          this.name = 'Animal';
      };

      
      // es6中class的聲明
      class Animal2 {
          constructor () {
              this.name = 'Animal2';
          }

如何建立對象

1.字面量對象
2.顯示的構造函數
3.Object.create函數

// 第一種方式:字面量
      var o1 = {name: 'o1'};
      var o2 = new Object({name: 'o2'});
      // 第二種方式:構造函數
      var M = function (name) { this.name = name; };
      var o3 = new M('o3');
      // 第三種方式:Object.create
      var p = {name: 'p'};
      var o4 = Object.create(p);

類與繼承

如何實現繼承?
繼承的本質就是原型鏈優化

藉助構造函數實現繼承

/**
       * 藉助構造函數實現繼承
       */
      function Parent1 () {
          this.name = 'parent1';
      }
      Parent1.prototype.say = function () {

      };
      function Child1 () {
          Parent1.call(this); // 或Parent1.apply(this,arguments)
          this.type = 'child1';
      }
      console.log(new Child1(), new Child1().say());

重點是這句:Parent1.call(this); 在子類的構造函數裏執行父類的構造函數,經過call/apply改變this指向,從而致使父類構造函數執行時的這些屬性都會掛載到子類實例上去。
問題: 只能繼承父類構造函數中聲明的實例屬性,並無繼承父類原型的屬性和方法this

藉助原型鏈實現繼承

/**
       * 藉助原型鏈實現繼承
       */
      function Parent2 () {
          this.name = 'parent2';
          this.play = [1, 2, 3];
      }
      function Child2 () {
          this.type = 'child2';
      }
      Child2.prototype = new Parent2();

      var s1 = new Child2();
      var s2 = new Child2();
      console.log(s1.play, s2.play);
      s1.play.push(4);

重點就是這句: Child2.prototype = new Parent2(); 就是說 new 一個父類的實例,而後賦給子類的原型 也就是說 new Child2().__proto__ === Child2.prototype === new Parent2()當咱們在new Child2()中找不到屬性/方法,順着原型鏈就能找到new Parent2(),這樣就實現了繼承。
問題: 原型鏈中的原型對象是共用的,子類沒法經過父類建立私有屬性
好比當你new兩個子類s一、s2的時候,改s1的屬性,s2的屬性也跟着改變prototype

組合式繼承

/**
       * 組合方式
       */
      function Parent3 () {
          this.name = 'parent3';
          this.play = [1, 2, 3];
      }
      function Child3 () {
          Parent3.call(this); // 父類構造函數執行了
          this.type = 'child3';
      }
      Child3.prototype = new Parent3(); // 父類構造函數執行了
      var s3 = new Child3(); 
      var s4 = new Child3();
      s3.play.push(4);
      console.log(s3.play, s4.play);

組合式就是原型鏈+構造函數繼承,解決了前兩種方法的問題,但也有不足:子類實例化時,父類構造函數執行了兩次,因此有了下面的組合繼承的優化1code

組合繼承的優化1

/**
       * 組合繼承的優化1
       * @type {String}
       */
      function Parent4 () {
          this.name = 'parent4';
          this.play = [1, 2, 3];
      }
      function Child4 () {
          Parent4.call(this);
          this.type = 'child4';
      }
      Child4.prototype = Parent4.prototype;
      var s5 = new Child4();
      var s6 = new Child4();
      console.log(s5, s6);

      console.log(s5 instanceof Child4, s5 instanceof Parent4);
      console.log(s5.constructor);

其實就是把原型鏈繼承的那句 Child4.prototype = new Parent4(); 改成 Child4.prototype = Parent4.prototype; 這樣雖然父類構造函數只執行了一次了,但又有了新的問題: 沒法判斷s5是Child4的實例仍是Parent4的實例 由於Child4.prototype.constructor指向了Parent4的實例;若是直接加一句 Child4.prototype.constructor = Child4 也不行,這樣Parent4.prototype.constructor也指向Child4,就沒法區分父類實例了。對象

若要判斷a是A的實例 用constructor
a.__proto__.constructor === A
用instanceof則不許確, instanceof 判斷 實例對象的__proto__ 是否是和 構造函數的prototype 是同一個引用。若A 繼承 B, B 繼承 C 在該原型鏈上的對象 用instanceof判斷都返回ture

組合繼承的優化2(推薦)

/**
       * 組合繼承的優化2
       */
      function Parent5 () {
          this.name = 'parent5';
          this.play = [1, 2, 3];
      }
      function Child5 () {
          Parent5.call(this);
          this.type = 'child5';
      }
      //注意此處,用到了Object.creat(obj)方法,該方法會對傳入的obj對象進行淺拷貝
      //這個方法做爲一個橋樑,達到父類和子類的一個隔離
      Child5.prototype = Object.create(Parent5.prototype);
      //修改構造函數指向
      Child5.prototype.constructor = Child5

構造函數屬性繼承和創建子類和父類原型的連接繼承

ES6實現繼承

引入了class、extends、super關鍵字,在子類構造函數裏調用super()方法來調用父類的構造函數。
在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,是基於對父類實例加工,只有super方法才能返回父類實例。ip

class Child6 extends Parent6 {
      constructor(x, y, color) {
        super(x, y); // 調用父類的constructor(x, y)
        this.color = color;
      }
      toString() {
        return this.color + ' ' + super.toString(); // super表明父類原型,調用父類的toString()
      }
    }

class實現原理

Class充當了ES5中構造函數在繼承實現過程當中的做用
有prototype屬性,有__proto__屬性,這個屬性在ES6中的指向有一些主動的修改。
同時存在兩條繼承鏈:一條實現屬性繼承,一條實現方法繼承。

class A extends B {}
A.__proto__ === B;  //繼承屬性
A.prototype.__proto__ === B.prototype;  //繼承方法

ES6的子類的__proto__是父類,子類的原型的__proto__是父類的原型。
可是在ES5中 A.__proto__是指向Function.prototype的,由於每個構造函數其實都是Function這個對象構造的,ES6中子類的__proto__指向父類能夠實現屬性的繼承。

只有函數有prototype屬性,只有對象有__proto__屬性 ;但函數也有__proto__屬性,由於函數也是一個對象,函數的__proto__等於 Function.prototype。

extends實現原理

//原型鏈接
Man.prototype = Object.create(Person.prototype); 
// B繼承A的靜態屬性
Object.setPrototypeOf(Man, Person);
//綁定this
Person.call(this);

前兩句實現了原型鏈上的繼承,最後一句實現構造函數上的繼承。

相關文章
相關標籤/搜索