函式 prototype 特性



建構式(Constructor) 中看過一個例子:
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.toString = function() {
        return '[' + this.name + ', ' + this.age + ']';
    };
}

Person函式中使用了函式實字建立函式實例,並指定給toString特性,每一次呼叫建構式時,都會建立一次函式實例:
js> function Person(name, age) {
  >     this.name = name;
  >     this.age = age;
  >     this.toString = function() {
  >         return '[' + this.name + ', ' + this.age + ']';
  >     };
  > }
js> var p1 = new Person('Justin', 35);
js> var p2 = new Person('Momor', 32);
js> p1.toString == p2.toString;
false
js>


但函式內容並沒有綁定特定資源。為了節省記憶體,你也許可以這麼撰寫:
function toString() {
    return '[' + this.name + ', ' + this.age + ']';
}

function Person(name, age) {

    this.name = name;
    this.age = age;
    this.toString = toString;
}

這可以解決重複建立函式實例的問題,但在全域範圍(物件)上多了個toString名稱。如果你知道函式在定義時,都有個prototype特性,則可以如下:
js> function Person(name, age) {
  >     this.name = name;
  >     this.age = age;
  > }
js> Person.prototype.toString = function() {
  >     return '[' + this.name + ', ' + this.age + ']';
  > };
function () {
    return "[" + this.name + ", " + this.age + "]";
}

js> var p1 = new Person('Justin', 35);
js> var p2 = new Person('Momor', 32);
js> p1
[Justin, 35]
js> p2
[Momor, 32]
js>


使用new關鍵字時,JavaScript會先建立一個空物件,接著設定物件的原型為函式的prototype特性所參考的物件,然後呼叫建構式並將所建立的空物件設為this。

JavaScript在尋找特性名稱時,會先在實例上找尋有無特性,以上例而言,p1 上會有name與age特性,所以你可以直接取得對應的值。如果物件上沒有該特性,會到物件的原型上去尋找,以上例而言,p1上沒有toString特 性,所以會到p1的原型上尋找,而p1的原型物件此時也就是Person.prototype所參考的物件,這個物件上有toString特性,所以可以 找到toString所參考的函式並執行。

要注意的是,只有在查找特性,而物件上不具該特性時才會使用原型,如果你對物件設定某個特性,是直接在物件上設定了特性,而不是對原型設定了特性。例如:
js> function Some() {}
js> Some.prototype.data = 10;
10
js> var s = new Some();
js> s.data;
10
js> s.data = 20;
20
js> s.data;
20
js> Some.prototype.data;
10
js>


在上例中你可以看到,你對s所參考的物件設定了data特性,但並不影響Some.prototype.data的值。

你可以在任何時間點對函式的prototype新增特性,由於原型查找的機制,透過函式而建構的所有實例,都可以找到該特性,即使實例建立之後,特性才被添加到原型中。例如:

js> function Some() {}
js> var s = new Some();
js> print(s.data);
undefined
js> Some.prototype.data = 10;
10
js> print(s.data);
10
js>


建構式(Constructor) 中有提過,每個透過new建構的物件,都會有個constructor特性,參考至當初建構它的函式。事實上,每個Function實例建立時,都會在Function實例上以空物件建立prototype,然而在空物件上設定constructor特性,也因此每個new建構的物件,都可以找到constructor特性。例如:
js> function Some() {}
js> Some.prototype.constructor;

function Some() {
}

js>


每 個函式的實例,其prototype特性預設參考至Object的實例,實例上有個constructor特性。根據原型尋找原則,如果 prototype上也找不到,由於prototype是Object實例,也就是prototype的原型預設是參考至 Object.prototype,所以又會到Object.prototype上尋找,如果找到就使用,如果沒有找到就是undefined,這就是JavaScript的原型鏈尋找特性機制。

例如:

js> Object.prototype.xyz = 10;
10
js> function Some() {}
js> var s = new Some();
js> s.xyz;
10
js> s.__proto__ == Some.prototype;
true
js> s.__proto__.__proto__ == Object.prototype;
true
js>


上例中__proto__是Rhino中一個非標準特性,可以取得物件建立時被設定的原型,預設就是建構式的prototype所參考的物件。雖然Some實例或Some.prototype都沒有定義xyz,但根據原型鏈查找,最後在Object.prototype可以找到xyz(在Object.prototype上添加特性是非常不建議的,因為它會影響所有JavaScript中的物件,這邊只是為了示範原型鏈查找)。

你也可以使用isPrototypeOf()來確定物件是否為另一物件的原型。例如:

js> var arr = [];
js> Array.prototype.isPrototypeOf(arr);
true
js> Function.prototype.isPrototypeOf(Array);
true
js> Object.prototype.isPrototypeOf(Array.prototype);
true
js>


for in在列舉物件特性時,會循著原型鏈一路找出所有可列舉特性。