December 27, 2015 · JavaScript

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(NumberStringBooleanNullUndefined,在 ES6 還多了一種 Symbol)以外的值都是 Object

object 本質上是一組 property 的集合(§8.6),它可以像這樣被定義與使用:

(註:規範中一共定義了三種不同的 property,但本文想著重說明 prototype 與 constructor,因此並不打算對此多加說明。如果想要詳細瞭解如何定義並使用 object 的 properties,可以參考 Object properties in JavaScript 這篇文章。)

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:lengthwidtharea

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.arearect2.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 rect1rect2 能夠「共用」它們的 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 定義了 toStringvalueOf 等 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)。因此,即使 rect1rect2 沒有直接定義這些 properties,依舊能存取到它們:

console.log(rect1.toString()); // [object Object]  
console.log(rect2.hasOwnProperty('weight')); // true  

Creating Object with Specified Prototype

那麼,要如何建立擁有指定 prototype 的 object 呢?一個簡便的方法是使用 Object.create(),它會將它的第一個 argument 設為生成 object 的 prototype(§15.2.3.5):

(註:相較於早已存在的 new operator 與 constructor,Object.create() 其實是在 ES5.1 才正式標準化。本文只是為了方便說明,才在 new 與 constructor 之前先介紹它。)

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):

  1. 先查看自身的 instance properties。
  2. 若 property 不存在,則往上搜尋其 prototype。
  3. 不斷重複,直到找到 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 的例子來說:

於是這些 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 operatorconstructor 是另一種能夠建立擁有指定 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  

在上例可以注意到幾點:

prototype Property of Constructor

除了上面幾點之外,還有一點值得注意的地方。使用 new operator 與 constructor 建立的 objects,都會擁有相同的 prototype:constructor 的 prototype property(§11.2.2、§13.2.2 step 5-6)。

(註:上述的前提是,constructor 的 prototype property 必須要為 object。若否,則 new 出來的 object prototype 會被指定為 Object.prototype〔§13.2.2 step 7〕。)

(注意:為了區隔起見,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 本身。

(註:至於這個 property 有何用途,或許可以參考 What's up with the "constructor" property in JavaScript? 這篇文章。)

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

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket
Comments powered by Disqus