JavaScript原型鏈與繼承

1、ES5中的繼承

1. 原型鏈

  • 原型鏈javascript

    • 基本思想:利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法前端

    • 原型對象:每一個構造函數在建立時都會有一個prototype屬性指向這個函數的原型對象,而原型對象會得到一個constructor屬性指向構造函數。當調用構造函數建立實例後,實例都包含一個指向構造函數的原型對象(不是指向構造函數)的內部指針,Firefox、Safiri、Chrome用__proto__表示這個指針。java

    • 原型鏈:若是一個實例的原型對象等於另外一個類型的實例,而那個實例的原型對象又等於其餘類型的實例,如此層層遞進,就構成了實例與原型的鏈條,這就是所謂的原型鏈。es6

      function A() {}
      function B() {}
      B.prototype = new A();
      var c = new B();
      複製代碼
    • 原型搜索機制:讀取一個實例屬性時,首先會在事例中搜索該屬性。若是沒有找到,則會繼續搜索實例的原型。在經過原型鏈實現繼承的狀況下,搜索過程得以沿着原型鏈繼續向上。在找不到屬性或方法的狀況下,搜索過程老是要一環一環地前行到原型鏈末端(Object.prototype)纔會停下來。瀏覽器

  • 肯定原型與實例之間的關係app

    • instanceoffrontend

      c instanceof B     // true
      c instanceof A     // true
      c instanceof Object  // true
      複製代碼
    • isPrototypeOf()函數

      B.prototype.isPrototypeOf(c)    // true
      A.prototype.isPrototypeOf(c)    // true
      Object.prototype.isPrototypeOf(c)   // true
      複製代碼
  • 謹慎定義方法ui

    • 子類型給原型添加方法的代碼必定要放在替換原型的語句以後。如:
      • 重寫超類型中的某個方法;
      • 添加超類型中不存在的某個方法。
    • 在經過原型鏈實現繼承時,不能使用對象字面量建立原型方法。由於這樣作會重寫原型鏈。
  • 原型鏈的問題:this

    • 包含引用類型值的原型,全部實例屬性會共享該引用類型的屬性。
    • 在建立子類型的實例時,沒有辦法在不影響全部對象實例的狀況下 ,向超類型的構造函數中傳遞參數 。

2. 借用構造函數(僞造對象或經典繼承)

function A() {}
function B() {
    A.call(this);
}
var c = new B();
複製代碼
  • 基本思想:
    • 在子類型構造函數的內部調用超類型構造函數
  • 優點:
    • 能夠在子類型構造函數中向超類型構造函數傳遞參數。
      • A.call(this, name, sex, ...)
  • 問題:
    • 方法在構造函數中定義,沒法實現函數複用。
    • 在超類型的原型中定義的方法,對子類型而言是不可見的,結果全部類型都只能使用構造函數模式。

3. 組合繼承(僞經典繼承)

function A() {}
function B() {
    // 繼承屬性
    A.call(this, name, ...);
}
// 繼承方法
B.prototype = new A();
B.prototype.constructor = B;
複製代碼
  • 基本思想:
    • 使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數實現對實例屬性的繼承
  • 優勢:
    • 既實現了函數複用,又保證了每一個實例都有本身的屬性。
    • 可使用instanceofisPrototypeOf()識別基於組合繼承建立的對象。
    • JavaScript最經常使用的繼承模式。

4. 原型式繼承

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}
var a = {};
var b = object(a);
複製代碼
  • 基本思想:
    • 藉助原型基於已有的對象建立新對象
  • 本質:
    • object()對傳入其中的對象執行了一次淺複製。
  • ES5經過新增Object.create()方法規範了原型式繼承:
    • 這個方法接收兩個參數:一個用做新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。
    • 第二個參數定義的屬性會覆蓋原型對象上的同名屬性。
  • 缺點:
    • 和使用原型模式同樣,包含引用類型值得屬性始終會共享相應的值。

5. 寄生式繼承

function A(original) {
    var clone = object(original);
    clone.sayHi = function() {
        alert('Hi');
    };
    return clone;
}

var a = {};
var b = A(a);
複製代碼
  • 基本思想:建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後返回對象
  • 適用場景:主要考慮對象而不是自定義類型和構造函數的狀況。

6. 寄生組合式繼承

  • 組合繼承的不足:不管什麼狀況下,都會調用兩次超類型構造函數,一次是建立子類型原型的時候,另外一次是子類型構造函數內部。

  • 寄生組合式繼承的基本模式:

    function inherite(subType, superType) {
        var f = object(superType.prototype);  // 建立superType的實例f
        f.constructor = subType;
        subType.prototype = f;
    }
    
    function SuperType() {}
    
    function SubType() {
        SuperType.call(this);
    }
    
    inherite(SubType, SuperType);
    複製代碼
  • 基本思想:經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。其實是在組合繼承的基礎上,用超類型原型的副本代替調用超類型的構造函數給子類型指定原型

  • 本質上,是使用寄生式繼承來繼承超類型的原型,而後再將結果指定給子類型的原型。

  • 優勢:只調用了一次超類型的構造函數,而且所以避免了在子類型的原型上建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變。所以,還可以正常使用instanceofisPrototypeOf()。廣泛認爲寄生組合式繼承是引用類型最理想的繼承方式。

2、new操做符

1. 使用new操做符調用構造函數

  • 建立一個新對象;
  • 將新對象連接到構造函數的原型上;
  • 將構造函數的做用域賦給新對象(綁定this);
  • 執行構造函數中的代碼(爲這個新對象添加屬性);
  • 返回新對象。

2. new操做符的實現

function create() {
    let obj = {};                         // 建立一個新對象
    let Con = [].shift.call(arguments);   // 獲取構造函數
    obj.__proto__ = Con.prototype;        // 連接到原型
    let result = Con.apply(obj, arguments);     // 綁定this,執行構造函數
    return typeof result === 'object' ? result : obj;  // 確保new出來的是個對象
}

// 調用
function F() {}
var f = create(F);
複製代碼

3. new操做符的優先級問題

function Foo() {
    return this;
}

Foo.getName = function() {
    console.log(1);
}

Foo.prototype.getName = function() {
    console.log(2);
}
複製代碼
代碼 執行結果 執行過程
new Foo
new Foo()
Foo {}
new Foo.getName
new (Foo.getName)
new Foo.getName()
new (Foo.getName)()
1
Foo.getName {}
1. 執行var f = Foo.getName
2. 執行new fnew f()
(new Foo).getName
(new Foo()).getName
new Foo().getName
f() {
console.log(2);
}
1. 執行var f = new Foo()
2. 訪問f.getName
(new Foo).getName()
(new Foo()).getName()
new Foo().getName()
2 1. 執行var f = new Foo()
2. 調用f.getName()

優先級:.()相等,大於new操做符的優先級

3、ES6繼承

1. Class

  • Class可看做構造函數的另外一種寫法

    • 類的數據類型是函數,類自己指向構造函數
    • 類的全部方法都定義在類的prototype屬性上面
      • 在類的實例上調用方法,其實就是調用原型上的方法
      • 能夠經過實例的__proto__屬性爲類添加方法,可是不推薦使用,會影響到全部實例
    • 類的內部定義的全部方法,都是不可枚舉的(與 ES5 行爲不一致)
    class Point {
        constructor() {}
        toString() {}
    }
    
    typeof Point    // "function"
    Point === Point.prototype.constructor   // true
    var b = new Point();   // 使用new操做符建立實例
    
    b.constructor === Point.prototype.constructor  // true
    b.constructor === b.__proto__.constructor   // true
    
    Object.keys(Point.prototype)    // []
    Object.getOwnPropertyNames(Point.prototype)   // ["constructor", "toString"]
    
    var Spot = function() {}  // 使用ES5改寫Class
    Spot.prototype.toString = function() {}
    
    Object.keys(Spot.prototype)    // ["toString"]
    Object.getOwnPropertyNames(Spot.prototype)    // ["constructor", "toString"]
    複製代碼
  • 注意點

    • 類和模塊的內部,默認是嚴格模式
    • 類不存在變量提高,聲明前使用會報錯
    • 類的方法內部若是含有this,它默認指向類的實例
      • 將類的方法提取出來單獨使用時,方法內的this會指向該方法運行時所在的環境 => 能夠在構造函數中使用bind或箭頭函數讓this指向實例對象
  • 其餘特性

    • 靜態方法:使用static修飾,表示該方法不會被實例繼承,而是直接經過類來調用。
      • 靜態方法中的this指的是類,不是實例。
      • 父類的靜態方法,能夠被子類繼承。
    • 實例屬性:能夠定義在constructor()方法裏的this上面,也能夠定義在類的最頂層。
    • new.target:用在構造函數或Class中,返回new命令做用於的那個構造函數或Class
      • 若是構造函數不是經過new命令或Reflect.construct()調用的,new.target會返回undefined
      • 在函數外部使用new.target會報錯。

2. Class的繼承

  • 使用extends實現繼承,子類繼承了父類的全部屬性和方法。

    • 子類必須在constructor方法中調用super方法,不然新建實例時會報錯。
    • 在子類的構造函數中,只有調用super以後,才能使用this關鍵字。
  • 與ES5繼承機制的區別:

    • ES5的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面。

      Parent.apply(this)
      複製代碼
    • ES6的繼承,實質是先經過super()方法將父類實例對象的屬性和方法,加到this上面,而後再用子類的構造函數修改this

  • super關鍵字:既能夠當作函數使用,也能夠看成對象使用

    • super做爲函數調用時,表明父類的構造函數,返回子類的實例。只能用在子類的構造函數中,用在其餘地方會報錯。

      class A {}
      
      class B extends A {
          constructor() {
              super();   // 至關於 A.prototype.constructor.call(this)
          }
      }
      複製代碼
    • super做爲對象使用時:

      (1) 在子類的普通方法中經過super調用父類的方法時,super指向父類的原型對象

      • 定義在父類實例上的方法或屬性,沒法經過super調用
      • 父類方法內部的this指向當前的子類實例
      • 經過super對某個屬性賦值時,super就是this,賦值的屬性會變成子類實例的屬性
      • 經過super訪問某個屬性時,super依舊指向父類的原型對象

      (2) 在子類的靜態方法中經過super調用父類的方法時,super指向父類

      • 父類方法內部的this指向當前的子類
    • 使用super的時候,必須顯示指定是做爲函數仍是做爲對象使用,不然會報錯,如:

      console.log(super);    // 報錯
      複製代碼
    • 因爲對象老是繼承自其餘對象,因此能夠在任何一個對象中,使用super關鍵字

  • Class的prototype屬性和__proto__屬性

    • 大多數瀏覽器的ES5實現中,每個對象都有__proto__屬性,指向對應的構造函數的prototype屬性。

    • ES6的Class中,同時有prototype屬性和__proto__屬性,所以同時存在兩條繼承鏈:

      • 子類的__proto__屬性指向父類
      • 子類prototype屬性的__proto__屬性指向父類的prototype屬性
      class A {}
      
      class B extends A {}
      
      B.__proto__ = A   // true
      B.prototype.__proto__ = A.prototype   // true
      複製代碼
    • 緣由,類的繼承是按照下面的模式實現的:

      class A {}
      
      class B {}
      
      // B的實例繼承A的實例
      Object.setPrototypeOf(B.protottype, A.prototype);
      // let a = new A();
      // let b = new B();
      // B.prototype === b.__proto__
      // A.prototype === a.__proto__
      
      // B繼承A的靜態屬性
      Object.setPrototypeOf(B, A);
      
      // Object.setPrototypeOf的實現
      Object.setPrototypeOf = function(obj, proto) {
          obj.__proto__ = proto;
          return obj;
      }
      複製代碼
  • 原生構造函數的繼承

    • ES5中,沒法繼承原生構造函數。緣由是ES5先構建子類的實例對象this,再將父類的屬性添加到子類上,因爲父類的內部屬性沒法獲取,致使沒法繼承原生的構造函數。
    • ES6容許繼承原生構造函數定義子類,由於ES6先構建父類的實例對象this,而後再用子類的構造函數修飾this,使得父類的全部行爲均可以繼承。

參考:

  1. JavaScript高級程序設計(第三版),第6章。
  2. 前端進階之道:yuchengkai.cn/docs/fronte…
  3. 【阮一峯】ES6入門:es6.ruanyifeng.com/#docs/class
相關文章
相關標籤/搜索