ES2015 Proxy (Chrome 49 以上版本) 提供 JavaScript 存取中斷 API,讓我們能夠擷取或攔截目標物件的所有作業,並修改此目標的運作方式。
Proxy 的用途相當多元,包括:
- 攔截
- 物件虛擬化
- 資源管理
- 剖析或記錄以進行偵錯
- 安全性和存取權控管
- 物件使用合約
Proxy API 包含 Proxy 建構函式,可接收指定的目標物件和處理常式物件。
var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);
Proxy 的行為是由處理常式控制,處理常式可透過一些實用的方式修改 target 物件的原始行為。處理常式包含在 Proxy 上執行對應作業時呼叫的選用封包方法 (例如 .get()
、.set()
、.apply()
)。
攔截
首先,我們先使用純物件,然後使用 Proxy API 對物件新增攔截中介軟體。請記住,傳遞至建構函式的第一個參數是目標 (進行 Proxy 的物件),第二個參數則是處理常式 (Proxy 本身)。您可在此新增 getter、setter 或其他行為的掛鉤。
var target = {};
var superhero = new Proxy(target, {
get: function(target, name, receiver) {
console.log('get was called for:', name);
return target[name];
}
});
superhero.power = 'Flight';
console.log(superhero.power);
在 Chrome 49 執行上述程式碼時,我們收到下列內容:
get was called for: power
"Flight"
如實務上所見,在 Proxy 物件上正確執行屬性 get 或屬性 (attribute),會導致中繼層級呼叫處理常式上對應的陷阱。處理常式作業包括屬性讀取、屬性指派和函式應用程式,這些作業都會轉送至對應的陷阱。
Trap 函式可以選擇性地實作運算 (例如將作業轉送至目標物件)。那麼,如果沒有指定陷阱預設會發生什麼事。舉例來說,以下是不需要人工管理的轉送 Proxy:
var target = {};
var proxy = new Proxy(target, {});
// operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been forwarded
console.log(target.paul);
我們剛剛研究了 Proxy 純物件,但也可以輕鬆代理函式物件,其中函式就是目標。這次我們將使用 handler.apply()
陷阱:
// Proxying a function object
function sum(a, b) {
return a + b;
}
var handler = {
apply: function(target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`);
return target.apply(thisArg, argumentsList);
}
};
var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3
識別 Proxy
您可以使用 JavaScript 等式運算子 (==
和 ===
) 觀察 Proxy 的身分。如我們所知,將這兩個運算子套用至兩個物件時,這些運算子會比較物件身分。下一個範例將說明這個行為。即使基礎目標相同,比較兩個不同的 Proxy 仍會傳回 false。在類似的憑證中,目標物件與任何 Proxy 不同:
// Continuing previous example
var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false
理想情況下,請勿區分 Proxy 與非 Proxy 物件,否則導入 Proxy 並不會真正影響應用程式的結果。這有一個原因,就是 Proxy API 並未提供檢查物件是否為 Proxy 的方法,也無法針對物件的所有作業提供陷阱。
應用情境
如前所述,Proxy 的用途相當多元。上述許多項目 (例如存取權控管和剖析) 屬於「一般包裝函式」:也就是將其他物件納入同一個位址「空間」的 Proxy。也提及虛擬化技術虛擬物件是模擬其他物件的 Proxy,不需要位於同一個位址空間。例如遠端物件 (模擬其他空間中的物件) 和透明的 Future (模擬尚未計算的結果)。
將 Proxy 當做處理常式
Proxy 處理常式常見的用途是先執行驗證或存取控制檢查,然後再對包裝的物件執行作業。只有在檢查成功時,系統才會轉送作業。請參考下列驗證範例:
var validator = {
set: function(obj, prop, value) {
if (prop === 'yearOfBirth') {
if (!Number.isInteger(value)) {
throw new TypeError('The yearOfBirth is not an integer');
}
if (value > 3000) {
throw new RangeError('The yearOfBirth seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
}
};
var person = new Proxy({}, validator);
person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception
較複雜的範例可能會考量所有不同的作業 Proxy 處理常式能夠攔截。可想見,實作作業必須複製每個陷阱的存取檢查和轉送作業模式。
由於每個作業的轉寄方式可能不盡相同,因此這個做法較不容易抽象。在完美情況下,如果所有運算可以統一透過一個陷阱來進行,則處理常式就只需要在單一陷阱中執行一次驗證檢查。如要這麼做,您可以將 Proxy 處理常式本身做為 Proxy 實作。很抱歉,這不在本文的討論範圍內。
物件額外資訊
Proxy 的另一個常見用途是擴充或重新定義物件作業語意。例如,您可以讓處理常式執行記錄作業、通知觀察器、擲回例外狀況而非傳回未定義的例外狀況,或是將作業重新導向至不同的儲存空間目標。在這種情況下,使用 Proxy 可能會與使用目標物件產生不同的結果。
function extend(sup,base) {
var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");
base.prototype = Object.create(sup.prototype);
var handler = {
construct: function(target, args) {
var obj = Object.create(base.prototype);
this.apply(target,obj, args);
return obj;
},
apply: function(target, that, args) {
sup.apply(that,args);
base.apply(that,args);
}
};
var proxy = new Proxy(base, handler);
descriptor.value = proxy;
Object.defineProperty(base.prototype, "constructor", descriptor);
return proxy;
}
var Vehicle = function(name){
this.name = name;
};
var Car = extend(Vehicle, function(name, year) {
this.year = year;
});
Car.prototype.style = "Saloon";
var Tesla = new Car("Model S", 2016);
console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year); // 2016
存取權控管
存取權控管也是 Proxy 的另一個實用用途。比起將目標物件傳遞至不受信任的程式碼,使用者可以將其 Proxy 包裝成一種保護薄膜。一旦應用程式判定不受信任的程式碼完成特定工作,就能撤銷參照,進而從其目標卸離 Proxy。明布將以遞迴方式延伸至可由定義原始目標可連線的所有物件。
透過 Proxy 使用反射
Reflect 是一項新的內建物件,提供可攔截 JavaScript 作業的方法,非常適合使用 Proxy。事實上,Reflect 方法與 Proxy 處理常式的方法相同。
Python 或 C# 等靜態類型語言一直都有反射 API,但 JavaScript 其實不需要做為動態語言。據說,ES5 已有許多反射功能,例如 Array.isArray()
或 Object.getOwnPropertyDescriptor()
,但可視為其他語言的感受。ES2015 推出了 Reflection API,可用來存放此類別的未來方法,方便您進行推論。由於物件的用途是基本原型,而非反射方法的值區,因此這個做法很合理。
透過 Reflect,我們可以改善之前的超級英雄範例,為 get 和 traps 進行適當的場域攔截,如下所示:
// Field interception with Proxy and the Reflect API
var pioneer = new Proxy({}, {
get: function(target, name, receiver) {
console.log(`get called for field: ${name}`);
return Reflect.get(target, name, receiver);
},
set: function(target, name, value, receiver) {
console.log(`set called for field: ${name} and value: ${value}`);
return Reflect.set(target, name, value, receiver);
}
});
pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName
這會輸出:
set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName
另一個例子則是:
將 Proxy 定義納入自訂建構函式中,避免每次我們要使用特定邏輯時手動建立新的 Proxy。
新增「儲存」變更的功能,但前提是資料必須確實修改 (假設儲存作業非常昂貴)。
function Customer() {
var proxy = new Proxy({
save: function(){
if (!this.dirty){
return console.log('Not saving, object still clean');
}
console.log('Trying an expensive saving operation: ', this.changedProperties);
},
}, {
set: function(target, name, value, receiver) {
target.dirty = true;
target.changedProperties = target.changedProperties || [];
if(target.changedProperties.indexOf(name) == -1){
target.changedProperties.push(name);
}
return Reflect.set(target, name, value, receiver);
}
});
return proxy;
}
var customer = new Customer();
customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation: ["name", "surname"]
customer.save();
如需更多 Reflect API 範例,請參閱 Tagtree 中的 ES6 Proxy 一文。
折線 Object.observe()
雖然我們向 Object.observe()
說 goodbye,但現在可以使用 ES2015 Proxy 將 polyfill。Simon Blackwell 最近編寫了以 Proxy 為基礎的 Object.observe() 填充碼,值得一探究竟。Erik Arvidsson 也一直在 2012 年撰寫出相當規格完整的版本。
瀏覽器支援
Chrome 49、Opera、Microsoft Edge 和 Firefox 都支援 ES2015 Proxy。Safari 對這項功能已有多項公開信號,但我們依然樂觀其成。Reflect 目前採用 Chrome、Opera 和 Firefox,且正在開發 Microsoft Edge。
Google 發布 Proxy 專用的 polyfill。這個參數只能用於一般包裝函式,因為這個包裝函式只能在建立 Proxy 時知道的 Proxy 屬性。