理解 JavaScript 的prototype
屬性不太容易。你也許知道它同物件導向程式設計(OOP)和物件繼承有關,但未必對其技術原理非常清楚。
原型繼承
物件導向程式設計可以通過很多途徑實現。其他的語言,比如 Java,使用基於類的模型實現: 類及物件例項區別對待。但在 JavaScript 中沒有類的概念,取而代之的是一切皆物件。JavaScript 中的繼承通過原型繼承實現:一個物件直接從另一物件繼承。物件中包含其繼承體系中祖先的引用——物件的 prototype 屬性。
class
關鍵字是在 ES6 中首次引入 JavaScript 的。其實,它並沒有為物件導向繼承引入新模型,class
關鍵字通過語法糖,實現了本文介紹的原型特性和建構函式。
JavaScript 實現繼承的語言特性
以下語言特性共同實現了 JavaScript 繼承。
- 當嘗試訪問 JavaScript 物件中不存在的屬性時,解析器會查詢匹配的物件原型。例如呼叫
car.toString()
,如果car
沒有toString
方法,就會呼叫car
物件的原型。 這個查詢過程會一直遞迴, 直到查詢到匹配的原型或者繼承鏈盡頭。 - 呼叫
new Car()
會建立一個新的物件,並初始化為Car.prototype
。 這樣就允許為新物件設定原型鏈。需要注意的是,new Car()
只有當Car
是函式時才有意義。 此類函式即所謂建構函式。 - 呼叫物件的一個成員函式時,
this
的值被繫結為當前物件。例如呼叫"abc".toString()
,this
的值被設定為"abc"
,然後呼叫toString
函式。該技術支援程式碼重用:同樣的程式碼,可在this
為各種不同的值時呼叫。物件的成員函式,也被稱為物件的方法。
舉個例子
我們用物件導向程式設計,實現一個計算矩形周長的例子。
1 2 3 4 5 6 7 8 9 10 11 |
function Rectangle(x, y) { this.x = x; this.y = y; } Rectangle.prototype.perimeter = function() { return 2 * (this.x + this.y); } var rect = new Rectangle(1, 2); console.log(rect.perimeter()); // outputs '6' |
首先,我們定義建構函式 Rectangle
。 按照規範,我們大寫建構函式名首字母,表明它可以用 new
呼叫,以示與其他常規函式的區別。建構函式自動將 this
賦值為一空物件,然後程式碼中用 x
和 y
屬性填充它,以備後用。
然後, Rectangle.prototype
新增一個通過 x
和 y
屬性計算周長成員函式。 注意 this
的使用,在不同的物件中,this
會有不同的值,這些程式碼都可以正常工作。
最後, 一個名為 rect
的物件建立出來了。 它繼承了 Rectangle.prototype
, 我們可以呼叫 rect.perimeter()
, 然後將結果列印到控制檯。
prototype 屬性名稱帶來的誤解
有一些關於 JavaScript 的原型的誤解。 一個物件的原型與物件的 prototype
屬性並非一回事。 前者用於在原型鏈中匹配不存在的屬性。後者用於通過 new
關鍵字建立物件,它將作為新建立物件的原型。 理解二者的差異,將幫助你徹底理解 JavaScript 中的原型特性。
在我們的例子中, Rectangle.prototype
是用 new Rectangle()
建立出來物件的原型, 而 Rectangle
的原型實際上是 JavaScript 的 Function.prototype
。(子物件的原型是父物件的 prototype 屬性)
物件中儲存原型的變數,也被稱之為內部原型引用(the internal prototype link),歷史上也曾稱之為 __proto__ ,對這個稱謂始終存在一些爭議。 更精確的,它可以被稱為 Object.getPrototypeOf(...)
的返回值。