原型繼承(翻譯 vjeux 文章)

Zwe1發表於2018-03-30

Javascript——原型繼承是如何實現的

在網頁上我們隨處可見說javascript是一種原型繼承。然而javascript僅僅預設地在使用new操作符的這種特殊情況下提供了原型繼承。因此,許多js的解讀都讓人感到很迷惑。 這篇文章用於闡述什麼是原型繼承以及如何在js中使用原型繼承。

原型繼承的目的:在 js 中實現一種物件之間的資源(物件屬性)共享。

原型繼承的定義

關於js的原型繼承,你常會看到如下定義:

當訪問一個物件的屬性時,js會沿著原型鏈向上遍歷直到尋找到匹配的屬性。

大多數js實現使用 proto 屬性來代表原型鏈中的下一層物件。在這篇文章中我們會看到 proto 和 prototype 究竟有什麼不同之處。

注意: proto 是一種非標準表示方式,你不應該將其用在你的程式碼中。在這篇文章中我們用它來描述js繼承是如何實現的。

下面程式碼顯示了js 引擎如何檢索屬性(讀取)。

  function getProperty(obj, prop) {
    if (obj.hasOwnProperty(prop)) {
      return obj[prop] 
    } else if (obj.__proto__ !== null) {
      return getProperty(obj.__proto__, prop)
    } else {
      return undefined
    }
  }
複製程式碼

讓我們來看一個經典的案例: 一個2D點元素。一個點有兩個座標 x 和 y 以及一個方法 print。
使用之前我們所寫的原型繼承方法,我們將建立一個 Point 物件以及它的三個屬性: x, y, 和 point。要建立一個新的點,我們只需要使用設定在 Point物件上的 proto 來建立一個新的物件。

  var Point = {
    x: 0,
    y: 0,
    print: function () { console.log(this.x, this.y); }
  };

  var p = {x: 10, y: 20, __proto__: Point};
  p.print(); // 10 20
複製程式碼

Javascript奇怪的原型繼承

讓人迷惑的是每個教授js原型繼承的人都給了這樣的定義卻沒有給如上的程式碼。他們所給的程式碼像下面這樣:

  function Point(x, y) {
    this.x = x;
    this.y = y;
  }
  Point.prototype = {
    print: function () { console.log(this.x, this.y); }
  };
  var p = new Point(10, 20);
  p.print(); // 10 20
複製程式碼

這兩份程式碼完全不同。上面的程式碼中 Point 是一個函式,我們新增了一個 prototype 屬性,使用了 new 操作符。這是什麼鬼東西?

new 是如何工作的

Brendan Eich 想要將 Javascript 設計成一種傳統的物件導向的程式語言,如同 Java h和 C++ 那樣。在這些語言中,我們使用 new 操作符來建立一個類的例項。所以他為 Javascript 新增了 new 操作符。

C++ 有建構函式( constructor )的概念,用於初始化例項屬性。因此,new 操作符必須的作用物件必須是一個函式。
我們必須把物件的方法放在某一處。由於我們使用的是一種原型語言,讓我們把它置於函式的原型屬性中。

new 操作符建立一個帶引數的 F 函式: new F(arguments...)。通過如下三步來實現:

  1. 建立類的例項: 這是一個將 proto 屬性設定在 F.prototype 上的空物件。
  2. 初始化例項: F函式呼叫時接受傳入的引數並且將 this 指向這個例項。
  3. 返回例項
    現在我們理解了 new 操作符是怎樣工作的,我們可以在 js 中實現它。
function New (f) {
    ①  var n = { '__proto__': f.prototype };
       return function () {
    ②    f.apply(n, arguments);
    ③    return n;
       };
     }
複製程式碼

我們來測試一下它是如何工作的:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype = {
  print: function () { console.log(this.x, this.y); }
};
 
var p1 = new Point(10, 20);
p1.print(); // 10 20
console.log(p1 instanceof Point); // true
 
var p2 = New (Point)(10, 20);
p2.print(); // 10 20
console.log(p2 instanceof Point); // true
複製程式碼

js中真正的原型繼承

js規範僅為我們提供了 new 操作符可以使用。Douglas Crockford 找到了一種利用 new 操作符來實現原型繼承的方法!他創造了 Object.create 函式。

Object.create = function (parent) {
  function F() {}
  F.prototype = parent;
  return new F();
};
複製程式碼

這種寫法看起來很奇怪但它的工作原理很簡單。它僅僅根據設定在這個函式上的原型來建立了一個新的物件。如果我們可以使用 proto 它的寫法將會是這個樣子:

Object.create = function (parent) {
  return { '__proto__': parent };
};
複製程式碼

下面的程式碼是我們使用真正的原型繼承來建立的一個點。

var Point = {
  x: 0,
  y: 0,
  print: function () { console.log(this.x, this.y); }
};
 
var p = Object.create(Point);
p.x = 10;
p.y = 20;
p.print(); // 10 20
複製程式碼

結論

我們看到了究竟什麼是原型繼承並且看到了 js 僅僅是用了一種特別的方法來實現它。
然而,使用原型繼承(Object.create and proto)也存在一些缺點:

  • 非標準proto 不是標準化的東西甚至被棄用。原生的 Object.create 和 Douglas Crockford 的實現也不完全相同。
  • 未優化:與 new 建構函式(原生或自定義)相比Object.create 還沒有被深度嚴格優化, 後者甚至慢十倍。

另外

如果你能夠通過下面的圖理解原型繼承是如何工作的,Congratulations!
原型繼承(翻譯 vjeux 文章)

Vjeux英文原文連結

相關文章