函式 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
上會有 name
與 age
特性,所以可以直接取得對應的值。如果物件上沒有該特性,會到物件的原型上去尋找,以上例而言,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
上找不到,由於 prototype
是 Object
實例,也就是 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