函式 prototype 特性

July 26, 2022

在〈隱藏諸多細節的建構式〉中看過一個例子:

function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.toString = toString;
}

這可以解決重複建立函式實例的問題,但在全域範圍(物件)上多了個 toString 名稱,雖然可以如下避免這個問題:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.toString = function() {
        return '[' + this.name + ', ' + this.age + ']';
    };
}

Person 函式中使用了函式實字建立了函式實例,並指定給 toString 特性,不過每次呼叫建構式時,都會建立一次函式實例。

prototype

如果你知道函式在定義時,都有個 prototype 特性,則可以如下:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p1 = new Person('Justin', 35);
var p2 = new Person('Momor', 32);

console.log(p1.toString());   // [Justin, 35]
console.log(p2.toString());   // [Momor, 32]

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

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

如果使用 ECMAScript 5,可以透過 Object.getPrototypeOf 來取得實例的原型物件。例如:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p = new Person('Justin', 35);

console.log(Person.prototype === Object.getPrototypeOf(p));   // true

ES6 之前過去有個非標準特性 __proto__,許多瀏覽器、Node.js 等支援這個特性,可以設定或取得實例建立時被設定的原型物件,然而在 ES6 以後,__proto__ 被加入了附錄。

如果不想要修改 __proto__ 來指定原型,ES6 以後提供了 Object.setPrototypeOf 函式。

然而,若要用模擬的方式來說明 new Person('Justin', 35) 時做了什麼事,大概像是這樣:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p = {};
p.__proto__ = Person.prototype;
Person.call(p, 'Justin', 35);

console.log(p.toString());         // [Justin,35]
console.log(p instanceof Person);  // true

要注意的是,只有在查找特性,而物件上不具該特性時才會使用原型,如果你對物件設定某個特性,是直接在物件上設定了特性,而不是對原型設定了特性。例如:

function Some() {}
Some.prototype.data = 10;

var s = new Some();
console.log(s.data);                 // 10

s.data = 20;
console.log(s.data);                 // 20
console.log(Some.prototype.data);    // 10

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

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

function Some() {}

var s = new Some();
console.log(s.data);       // undefined

Some.prototype.data = 10;
console.log(s.data);       // 10

先前在談建構式時有提過,每個透過 new 建構的物件,都會有個 constructor 特性,參考至當初建構它的函式。事實上,每個函式實例建立時,都會在函式實例上以空物件建立 prototype,然後在空物件上設定 constructor 特性,也因此每個 new 建構的物件,都可以找到 constructor 特性。例如:

function Some() {}
console.log(Some.prototype.constructor);  // [Function: Some]

原型鏈

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

例如:

Object.prototype.xyz = 10;

function Some() {}

var s = new Some();
console.log(s.xyz); // 10

console.log(Object.getPrototypeOf(s) === Some.prototype);          // true

var protoOfS = Object.getPrototypeOf(s);
console.log(Object.getPrototypeOf(protoOfS) === Object.prototype); // true

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

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

console.log(Array.prototype.isPrototypeOf([]));               // true
console.log(Function.prototype.isPrototypeOf(Array));         // true
console.log(Object.prototype.isPrototypeOf(Array.prototype)); // true

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

如果要建立一個實例,想要令其循著某個原型鏈查找,例如,想要建立一個類似陣列的物件,但要其可循著 Array 原型鏈查找,以利用 Array 定義的特性,若使用非標準 __proto__ 特性的話,可以如下:

var arrayLike = {
    '0' : 10,
    '1' : 20,
    '2' : 30,
    length : 3
};

arrayLike.__proto__ = [];

arrayLike.map(function(elem) {
                return elem * 10
            })
            .forEach(function(elem) {
                console.log(elem);
            });

ECMAScript 5 有個 Object.create 函式可建立新物件,物件的原型將被設為呼叫 Object.create 時指定的原型物件:

var arrayLike = Object.create(Array.prototype, {
    '0'    : {value : 10},
    '1'    : {value : 20},
    '2'    : {value : 30},
    length : {value : 3}
});

arrayLike.map(function(elem) {
                return elem * 10
            })
            .forEach(function(elem) {
                console.log(elem);
            });

Object.create 第一個參數接受原型物件,第二個參數接受描述器(Descriptor),其內部大致是做了以下這些事(Ben Newman 寫的範例):

Object.create = function(proto, props) {
    var ctor = function(ps) {
        if(ps) {
            Object.defineProperties( this, ps );
        }
    };
    ctor.prototype = proto;
    return new ctor(props);
};

因此,作為一個有趣的練習,先前有個範例使用了 p.__proto__ = Person.prototype 這段程式碼,以下將之改為使用 Object.create

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.toString = function() {
    return '[' + this.name + ', ' + this.age + ']';
};

var p = Object.create(Person.prototype);
Person.call(p, 'Justin', 35);

console.log(p.toString());         // [Justin,35]
console.log(p instanceof Person);  // true

分享到 LinkedIn 分享到 Facebook 分享到 Twitter