JS中的繼承與原型鏈

林晨熙發表於2019-04-09

在物件導向程式設計中,類之間的共享是通過繼承實現的,而在JavaScript中萬物皆物件,並沒有類的概念(ES6中類僅僅是一個語法糖),物件之間的共享是通過一個叫做原型的東西實現的。

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

[[prototype]]

在 ECMAScript 標準中規定每個物件都有一個內建的屬性[[prototype]],它指向一個物件的原型物件。當查詢一個物件的屬性或方法時,如果在當前物件找不到,則在其原型物件上尋找,如果原型物件上也沒有,則在原型物件的原型物件上尋找,如此繼續直到一個物件的原型物件為 null(null 沒有原型)。可以看到,這樣一種層層向上的查詢是一種鏈式查詢,在每一層上的物件都有一個指向其原型物件的連結([[prototype]]),由這些連結組成的整個鏈條就叫做原型鏈。

如下圖所示,原型鏈查詢的思路大致為:

  • 當前物件 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(原型物件)而已,我們可以叫他 baseObj,當呼叫建構函式時,會自動將 baseObj 賦值給 __proto__,這樣在新的例項上通過原型鏈就可以共享建構函式 baseObj 及其原型鏈上的屬性了。prototype 和前述的__proto__[[Prototype]]是完全不同的概念,我們通常的混淆,主要就來自於用原型物件一詞來指代了不同的它們。

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

來看下面的例子: 函式 Animal 通過 new 例項化的物件能夠訪問到函式 prototype 屬性的 food 和 eat,這是如何做到的呢?

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 方法。

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

來看另一個例子: 如下將函式 Fish 的 prototype 賦值為 Animal,以此,通過 fish 的例項來訪問 Animal 原型 prototype 上的方法,可結果是 Uncaught TypeError: nimo.eat is not a function,為什麼會這樣呢?之所以會出現這樣的錯誤,是因為我們錯誤的把原型物件(__proto__)當原型物件(prototype)。前述我們已經知道繼承是通過原型鏈來實現的,而原型鏈又是通過 __proto__(指向內建物件[[prototype]]])來串聯的。當函式 Fish 的 prototype 賦值為 Animal 後,生成的例項物件 nimo 的 __proto__ 為 Animal,所以訪問 nimo.eat 會先在 Animal 上尋找 eat 方法,如圖 3,Animal 函式並沒有 eat 方法,從而繼續向上尋找,直到頂層物件 Object,結果還是沒有,因此報錯。

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
複製程式碼

通過不同的方法來建立物件和生成原型鏈

  • 語法結構建立物件

    • 物件字面量 通過物件字面量建立的物件其原型鏈為 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 --> Function.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 屬性,

相關文章