JS中的繼承與原型鏈

對於原型咱們經過[[prototype]]、proto 以及 prototype 這三個概念理解其實現繼承的思路。數組

[[prototype]]

在 ECMAScript 標準中規定每一個對象都有一個內置的屬性[[prototype]],它指向一個對象的原型對象。當查找一個對象的屬性或方法時,若是在當前對象找不到,則在其原型對象上尋找,若是原型對象上也沒有,則在原型對象的原型對象上尋找,如此繼續直到一個對象的原型對象爲 null(null 沒有原型)。能夠看到,這樣一種層層向上的查找是一種鏈式查找,在每一層上的對象都有一個指向其原型對象的連接([[prototype]]),由這些連接組成的整個鏈條就叫作原型鏈。瀏覽器

如圖 1 所示,原型鏈查找的思路大體爲:bash

  • 當前對象 object1 在查找一個屬性時,首先查找本身擁有的屬性,若是沒有,則在 object1 對象的__proto__([[prototype]])中查找,此時__proto__指向 object2。
  • 若是對象 object2 中沒有要查找的屬性,則在 object2 對象的__proto__中查找,若是沒有則繼續向上查找。
  • 直到 Object 對象的 prototype,此時 Object 對象的__proto__爲 null,則再也不查找。

鏈式查找示意圖
圖1. 原型鏈查找示意圖

說明: 圖中 builts-in 爲構建內置函數好比 toString()、valueOf 等。函數

__proto__

前述中的[[Prototype]]是一個內置屬性,咱們並不能直接獲取,爲了操做屬性的便利性不少瀏覽器都實現了 Object.prototype.__proto__,所以能夠經過 obj.__proto__ 來訪問對象的原型對象[[Prototype]],因此__proto__[[Prototype]]本質上是一個東西,都指向一個對象的原型對象。 另外一方面,設置[[Prototype]]是一個緩慢的操做,影響性能,所以使用 __proto__ 是有爭議的,更推薦使用 Object.getPrototypeOf 和 Object.setPrototypeOf 來訪問原型對象(儘管如此,若是性能是個問題,也應儘可能避免使用)。性能

prototype

prototype 是構造函數(一個擁有 [[Construct]] 內部方法的對象)纔有的屬性,好比函數(非箭頭函數),實例對象是沒有這個屬性的。這個所謂的 prototype,其實能夠認爲是構造函數內部一個普通的對象(或者說指向一個普通對象),只是很不幸地也叫作 prototype(原型對象)而已,當構造函數執行時,會自動將構造函數的 prototype 賦值給 __proto__(構造函數內部沒有顯示返回對象的狀況下),這樣在新的實例上經過原型鏈就能夠共享構造函數 prototype 及其原型鏈上的屬性了。prototype 和前述的__proto__[[Prototype]]是徹底不一樣的概念,咱們一般的混淆,主要就來自於用原型對象一詞來指代了不一樣的它們。ui

__proto__與prototype關係示意圖
圖2. __proto__與prototype關係示意圖

來看下面的例子: 函數 Animal 經過 new 實例化的對象可以訪問到函數 prototype 屬性的 food 和 eat,這是如何作到的呢?this

var Animal = function(name) {
  this.name = name;
};
Animal.prototype.food = 'meat';
Animal.prototype.eat = function() {
  console.log(this.name + ' eat ' + this.food);
};
var panda = new Animal('panda');
var dog = new Animal('dog');
console.log(panda.eat()); // panda eat meat
console.log(dog.eat()); // dog eat meat
console.log(panda.__proto__=== Animal.prototype); // true
複製代碼

以下圖所示,實例對象 panda 和 dog 之因此可以訪問 Animal 原型上的 food 和 eat 屬性是由於在調用構造函數時 Animal 的 prototype 對象賦值給了實例對象的 __proto__ 屬性,實例對象在訪問本身的方法(panda.eat)時,若是沒有找到,則在__proto__對象中尋找,而這個對象正好是 Animal 的 prototype 對象,它擁有 eat 方法,因此能夠成功訪問 eat 方法。spa

prototype繼承示意圖
圖3. panda/dog實例繼承示意圖

來看另外一個例子: 以下將函數 Fish 的 prototype 賦值爲 Animal,以此,經過 fish 的實例來訪問 Animal 原型 prototype 上的方法,可結果是 Uncaught TypeError: nimo.eat is not a function,爲何會這樣呢?之因此會出現這樣的錯誤,是由於咱們錯誤的把原型對象(__proto__)當原型對象(prototype)。前述咱們已經知道繼承是經過原型鏈來實現的,而原型鏈又是經過 __proto__來串聯的。當函數 Fish 的 prototype 賦值爲 Animal 後,生成的實例對象 nimo 的 __proto__ 爲 Animal,因此訪問 nimo.eat 會先在 Animal 上尋找 eat 方法,如圖 3,Animal 函數並無 eat 方法,從而經過 Animal 的__proto__ 繼續向上尋找,直到頂層對象 Object,結果仍是沒有,所以報錯。prototype

var Animal = function(name) {
  this.name = name;
};
Animal.prototype.food = 'meat';
Animal.prototype.eat = function() {
  console.log('I can eat' + this.food);
};

var Fish = function(name) {
  this.name = name;
};
Fish.prototype = Animal;

var nimo = new Fish('nimo');
console.log(nimo.eat()); // Uncaught TypeError: nimo.eat is not a function
複製代碼

經過不一樣的方法來建立對象和生成原型鏈

  • 語法結構建立對象code

    • 對象字面量

      經過對象字面量建立的對象其原型鏈爲 obj --> Object.prototype --> null

      var obj = { a: 1 };
      複製代碼
    • 數組字面量

      經過數組字面量建立的對象其原型鏈爲 arr --> Array.prototype --> Object.prototype --> null

      var arr = [1, 2];
      複製代碼
    • 函數字面量

      經過函數字面量建立的對象其原型鏈爲 f --> Function.prototype --> Object.prototype --> null

      function f(){ console.log('func');}
      複製代碼
  • 構造器建立對象

    經過構造函數建立的對象其原型鏈爲 instance --> func.prototype --> Object.prototype --> null

    var Animal = function(name) {
        this.name = name;
      };
      Animal.prototype.food = 'meat';
      Animal.prototype.eat = function() {
        console.log('I can eat' + this.food);
      };
      //實例對象panda的__proto__指向Animal.prototype
      var panda = new Animal('panda');
    複製代碼
  • Object.create 建立對象

    在 ES5 中引入了一個新的方法來建立對象,就是 Object.create,新對象的原型就是該方法傳入的第一個參數。

    var a = { x: 1 };
      // a --> Object.prototype --> null
    
      var b = Object.create(a);
      // b --> a --> Object.prototype --> null
      console.log(b.__proto__ === a); // true
      console.log(b.x); // 1
    
      var c = Object.create(b);
      // c --> b --> a --> Object.prototype --> null
      console.log(c.__proto__ === b); // true
    複製代碼

總結

  • 任何對象均可以成爲其餘對象的原型對象(__proto__指向的對象)。
  • [[Prototype]]爲一個對象的指向原型對象的內置屬性,不能直接訪問。
  • __proto__ 爲一個非標準的,只是爲了方便訪問原型對象而實現的一個屬性,它和[[Prototype]]本質上同樣都 指向原型對象,是全部對象都有的屬性。
  • prototype 爲擁有 [[construct]] 內部方法的對象纔有的屬性,它自己只是一個普通對象,只是正好叫作原型對象,它的做用是在構造函數生成新的實例時將這個所謂的原型對象賦值給實例的 __proto__ 屬性,這樣新的實例就能夠經過 __proto__ 來繼承構造函數原型裏的方法。能夠看到,prototype 和 __proto__ 所指的原型對象是徹底不一樣的概念。
  • 實例對象沒有 prototype 屬性,
相關文章
相關標籤/搜索