Prototype and Constructor in JavaScript
我想 prototype 在 JavaScript 中算是一個十分核心且重要的特色,但總覺得一直處於似懂非懂的狀態。剛好最近在查閱相關的資料,就藉此整理在這邊。
這篇文章假設你看得懂基本的 JavaScript 語法,只是對何謂 prototype 與 constructor 不太瞭解。如果你對如何建立、操作 object 與 function 等基礎語法還不太熟悉,這篇文章恐怕幫不上你的忙。
本篇會在內容附上規格書 ECMAScript® Language Specification(5.1 Edition)對應的 section number(像是 §a.b.c),供想要仔細求證的人參考。
Object
在規範(§8)中,除了 5 種 primitive types(Number、String、Boolean、Null、Undefined,在 ES6 還多了一種 Symbol)以外的值都是 Object。
object 本質上是一組 property[1] 的集合(§8.6),它可以像這樣被定義與使用:
var rect = {
length: 3,
width: 5,
area: function() {
return this.length * this.width;
}
};
console.log(rect.length + ' * ' + rect.width + ' = ' + rect.area()); // 3 * 5 = 15
上例的 object rect
具有三個 properties:length
、width
與 area
。
Object Factory
你可能會想透過 function 來建立多個擁有相同 properties 的 objects:
var createRect = function(length, width) {
return {
length: length,
width: width,
area: function() {
return this.length * this.width;
}
};
},
rect1 = createRect(3, 5),
rect2 = createRect(5, 5);
console.log(rect1.length + ' * ' + rect1.width + ' = ' + rect1.area()); // 3 * 5 = 15
console.log(rect2.length + ' * ' + rect2.width + ' = ' + rect2.area()); // 5 * 5 = 25
不過這樣做的缺點是,對於每個產生的 object,都得重新生一個一模一樣的 method。像是上例中的 rect1.area
與 rect2.area
這兩個 method,因為是在建立 object 的時候獨立產生,所以是不同的:
console.log(rect1.area === rect2.area); // false
Object Factory with Shared Property Value
針對先前 object factory 的問題,一個可行的解決方法是先產生好一個 method,然後重複使用它:
var area = function() {
return this.length * this.width;
},
createRect = function(length, width) {
return {
length: length,
width: width,
area: area
};
},
rect1 = createRect(3, 5),
rect2 = createRect(5, 5);
於是,現在生成的兩個 objects rect1
與 rect2
能夠「共用」它們的 area
property 了:
console.log(rect1.area === rect2.area); // true
Prototype
另一個讓多個 object 共享 properties 方法是:將這些 properties 擺進 object 的 prototype。
根據定義,prototype 是為其它 objects 提供共享 properties 的 object(§4.3.5)。也就是說,多個擁有相同 prototype 的 objects 將共享相同的一組 properties。
舉例來說,用 object literal { ... }
建立的 objects 都擁有相同的 prototype:Object.prototype
。這點可以用 Object.getPrototypeOf()
(§15.2.3.2)取得 object 的 prototype 加以驗證:
console.log(Object.getPrototypeOf(rect1) === Object.prototype); // true
console.log(Object.getPrototypeOf(rect2) === Object.prototype); // true
console.log(Object.getPrototypeOf({}) === Object.prototype); // true
Object.prototype
定義了 toString
、valueOf
等 properties,供所有擁有此 prototype 的 objects 使用(§15.2.4):
// Object.getOwnPropertyNames() 會回傳自身所有 property names(§15.2.3.4)。
// 由於不同瀏覽器可能會在 Object.prototype 上追加獨有的 properties,
// 因此得到的結果可能略有不同。
console.log(Object.getOwnPropertyNames(Object.prototype));
// ["toString", "toLocaleString", "valueOf", "hasOwnProperty", …]
根據規範,當 object 在自身找不到欲使用的 property 時,會往上在其 prototype 上尋找(§8.12.2)。因此,即使 rect1
與 rect2
沒有直接定義這些 properties,依舊能存取到它們:
console.log(rect1.toString()); // [object Object]
console.log(rect2.hasOwnProperty('weight')); // true
Creating Object with Specified Prototype
那麼,要如何建立擁有指定 prototype 的 object 呢?一個簡便的方法是使用 Object.create()
[2],它會將它的第一個 argument 設為生成 object 的 prototype(§15.2.3.5):
var rectPrototype = {
area: function() {
return this.length * this.width;
}
},
rect = Object.create(rectPrototype);
rect.length = 3;
rect.width = 5;
console.log(rect.area()); // 15
console.log(Object.getPrototypeOf(rect) === rectPrototype); // true
Prototype Chain
如同先前 section 提到的:object 若是在自身無法找到 property 的時候,會往上一層到 object 的 prototype 上尋找。由於 prototype 本身也是 object,所以這一點對它當然也適用。也就是說,當在 object 上尋找 property 時,會遵循以下步驟(§8.12.2):
- 先查看自身的 instance properties。
- 若 property 不存在,則往上搜尋其 prototype。
- 不斷重複,直到找到 property,或是已無再上層的 prototype(prototype 為
null
)為止。
寫成程式的話大概像這樣:
var getProperty = function(obj, name) {
var prop;
do {
// 實際上還要檢查 obj 是否為 object,但在這裡我們便宜行事 :P
// Object.prototype.hasOwnProperty() 能用以判斷 property
// 是否定義在 object 自身,而非其 prototype(§15.2.4.5)。
if (obj.hasOwnProperty(name) {
// Object.getOwnPropertyDescriptor() 會取得 property
// 的 attributes(§15.2.3.3)。
// Property 的 attributes 列表詳見 §8.6.1。
prop = Object.getOwnPropertyDescriptor(obj, name);
break;
}
obj = Object.getPrototypeOf(obj);
} while (obj !== null);
return prop;
};
以前面 rectPrototype
的例子來說:
rect
的 prototype 為rectPrototype
(透過Object.create()
指定)。rectPrototype
的 prototype 為Object.prototype
(因其由 object literal 定義)。Object.prototype
的 prototype 為null
(§15.2.4)。
於是這些 objects 就由 prototype 的關聯而形成一個 prototype chain:
rect → rectPrototype → Object.prototype → null
object rect
也因此能使用 area()
(定義在 rectPrototype
)、valueOf()
與 toString()
(定義在 Object.prototype
)等 methods:
console.log(rect.area()); // 15
console.log(rect.toString()); // [object Object]
在 Object.prototype
也定義了 isPrototypeOf()
method,可以用以檢查某個 object 是不是在另一個 object 的 prototype chain 上(§15.2.4.6):
console.log(rectPrototype.isPrototypeOf(rect)); // true
console.log(Object.prototype.isPrototypeOf(rectPrototype)); // true
console.log(Object.prototype.isPrototypeOf(rect)); // true
Object Factory with Prototype
回過頭來看剛剛 object factory 的例子。我們已經知道多個 rect objects 共享的 properties(也就是 area
)可以定義在其 prototype 上,因此先前的 object factory 可以改寫成這樣:
var rectPrototype = {
area: function() {
return this.length * this.width;
}
},
createRect = function(length, width) {
var rect = Object.create(rectPrototype);
rect.length = length;
rect.width = width;
return rect;
},
rect1 = createRect(3, 5),
rect2 = createRect(5, 5);
console.log(rect1.length + ' * ' + rect1.width + ' = ' + rect1.area()); // 3 * 5 = 15
console.log(rect2.length + ' * ' + rect2.width + ' = ' + rect2.area()); // 5 * 5 = 25
console.log(rect1.area === rect2.area); // true
這樣做還有一個特點,就是可以動態修改 prototype 的 properties,而這些改變會反映在生成的 object 身上:
rectPrototype.perimeter = function() {
return (this.length + this.width) * 2;
};
console.log(rect1.perimeter()); // 16
console.log(rect2.perimeter()); // 20
Constructor
除了使用 Object.create()
以外,使用 new
operator 與 constructor 是另一種能夠建立擁有指定 prototype 的 object 的方式。
什麼是 constructor 呢?constructor 是用以生成並初始化 object 的 function(§4.3.4),但是它與「一般的 function」又有什麼不同?讓我們直接看個 constructor 的例子:
var Rect = function(length, width) {
this.length = length,
this.width = width,
this.area = function() {
return this.length * this.width;
};
},
rect = new Rect(3, 5);
console.log(rect.length + ' * ' + rect.width + ' = ' + rect.area()); // 3 * 5 = 15
在上例可以注意到幾點:
- function 作為 constructor 使用時,須透過
new
operator 呼叫。 - 在 constructor 中(function 以
new
operator 呼叫的情況下),this
代表新建立的 object(§11.2.2、§13.2.2 step 8)。 - constructor 不需回傳新生成的 object(註:constructor 也可以有 return value。但只有在 return value 為 object 時,constructor 才會「真的」回傳它,否則 constructor 回傳的一律是透過
new
operator 建立的 object〔§11.2.2、§13.2.2 step 9-10〕)。
prototype
Property of Constructor
除了上面幾點之外,還有一點值得注意的地方。使用 new
operator 與 constructor 建立的 objects,都會擁有相同的 prototype:constructor 的 prototype
property(§11.2.2、§13.2.2 step 5-6)[3]。
(注意:為了區隔起見,object 的 prototype 在本文會直接寫作「prototype」,且不作任何字體變化。而 constructor 的 prototype
property 會在其後加上「property」,且「prototype
」會採用不同的字體。)
console.log(Object.getPrototypeOf(rect) === Rect.prototype); // true
於是,objects 共享的 properties 就能夠直接像這樣定義在 prototype
property 上面:
var Rect = function(length, width) {
this.length = length;
this.width = width;
},
rect1,
rect2;
Rect.prototype.area = function() {
return this.length * this.width;
};
rect1 = new Rect(3, 5);
rect2 = new Rect(5, 5);
console.log(rect1.length + ' * ' + rect1.width + ' = ' + rect1.area()); // 3 * 5 = 15
console.log(rect2.length + ' * ' + rect2.width + ' = ' + rect2.area()); // 5 * 5 = 25
console.log(rect1.area === rect2.area); // true
Prototype of prototype
Property
在預設情況下,function 的 prototype
property 是以 new Object()
建立的 object(§13.2 step 16),因此其 prototype 為 Object.prototype
:
console.log(Object.getPrototypeOf(Rect.prototype) === Object.prototype); // true
也就是說,上例 rect
的 prototype chain 如下所示:
rect → Rect.prototype → Object.prototype → null
constructor
Property of prototype
Property
此外,每個 function 預設的 prototype
property 都會具有一個 constructor
property,其值為這個 function 本身(§13.2 step 17):
console.log(Rect.prototype.constructor === Rect); // true
console.log(rect.constructor === Rect); // true
一般來說,我們通常會因此假設 object prototype 的 constructor
property 即是生成這個 object 的 constructor。因此在變更 constructor 的 prototype
property 時,一般也會確保 constructor
property 的值仍是 constructor 本身[4]。
instanceof
Operator
最後,讓我們看看 instanceof
operator。這個 operator 本質上是用以判斷 constructor 的 prototype
property 是否在 object 的 prototype chain 上(§11.8.6、§15.3.5.3)。也就是說:
obj instanceof Constructor;
相當於:
Constructor.prototype.isPrototypeOf(obj);
因此:
console.log(rect1 instanceof Rect); // true
console.log(Rect.prototype instanceof Object); // true
console.log(rect1 instanceof Object); // true
Summary
- object 是一組 property 的集合(§8.6)。
- prototype 是為其它 objects 提供共享 properties 的 object(§4.3.5)。
- 可以使用
Object.create()
建立擁有指定 prototype 的 object(§15.2.3.5)。 - objects 由 prototype 的關聯形成一個 prototype chain。
- prototype chain 的末端為
null
。 - 對 object 存取 property 時,會從 object 自身開始,沿著 prototype chain 向上尋找(§8.12.2)。
- prototype chain 的末端為
- constructor 是透過
new
operator 呼叫,用以生成並初始化 object 的 function(§4.3.4)。 - 對於 constructor 建立的 object,其 prototype 會被指定為 constructor 的
prototype
property(§11.2.2、§13.2.2 step 5-6)。 - 預設情況下,function 的
prototype
property- 以
new Object()
建立(§13.2 step 16),因此其 prototype 為Object.prototype
。 - 具有一個
constructor
property,其值為這個 function 本身(§13.2 step 17)。
- 以
instanceof
operator 能用以判斷 constructor 的prototype
property 是否在 object 的 prototype chain 上(§11.8.6、§15.3.5.3)。
規範中一共定義了三種不同的 property,但本文想著重說明 prototype 與 constructor,因此並不打算對此多加說明。如果想要詳細瞭解如何定義並使用 object 的 properties,可以參考 Object properties in JavaScript 這篇文章。 ↩︎
相較於早已存在的
new
operator 與 constructor,Object.create()
其實是在 ES5.1 才正式標準化。本文只是為了方便說明,才在new
與 constructor 之前先介紹它。 ↩︎前提是:constructor 的
prototype
property 必須要為 object。若否,則new
出來的 object prototype 會被指定為Object.prototype
〔§13.2.2 step 7〕。) ↩︎至於這個 property 有何用途,或許可以參考 What's up with the "constructor" property in JavaScript? 這篇文章。 ↩︎