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...)。通過如下三步來實現:
- 建立類的例項: 這是一個將 proto 屬性設定在 F.prototype 上的空物件。
- 初始化例項: F函式呼叫時接受傳入的引數並且將 this 指向這個例項。
- 返回例項
現在我們理解了 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!