get
、set
)get
、set
)get
)set
)enumerate
陷阱在哪裡?Proxy 能讓我們攔截並自訂在物件上執行的運算(例如取得屬性)。它們是元程式設計功能。
在以下範例中
proxy
是個空物件。
handler
可以透過實作特定方法來攔截在 proxy
上執行的運算。
如果 handler 沒有攔截運算,它會轉發到 target
。
我們只攔截一個運算 – get
(取得屬性)
const logged = [];
const target = {size: 0};
const handler = {
get(target, propKey, receiver) {
logged.push('GET ' + propKey);
return 123;
}
};
const proxy = new Proxy(target, handler);
當我們取得屬性 proxy.size
時,handler 會攔截該運算
請參閱 完整 API 參考,以取得可攔截運算的清單。
在我們深入探討 Proxy 是什麼以及它們為何有用的之前,我們首先需要了解什麼是元程式設計。
在程式設計中,有幾個層級
基本層級和元層級可以是不同的語言。在以下元程式中,元程式設計語言是 JavaScript,而基本程式設計語言是 Java。
元程式設計可以有不同的形式。在先前的範例中,我們已將 Java 程式碼印出到主控台。讓我們同時使用 JavaScript 作為元程式設計語言和基本程式設計語言。這方面的經典範例是 eval()
函式,它讓我們動態評估/編譯 JavaScript 程式碼。在以下互動中,我們使用它來評估運算式 5 + 2
。
其他 JavaScript 運算可能看起來不像元程式設計,但如果我們仔細觀察,它們實際上是
// Base level
const obj = {
hello() {
console.log('Hello!');
},
};
// Meta level
for (const key of Object.keys(obj)) {
console.log(key);
}
程式在執行時會檢查自己的結構。這看起來不像元程式設計,因為在 JavaScript 中,程式設計建構和資料結構之間的區別很模糊。所有 Object.*
方法都可以視為元程式設計功能。
反射式元程式設計表示程式會處理自己。 Kiczales 等人 [2] 區分出三種反射式元程式設計
我們來看一些範例。
範例:內省。 Object.keys()
執行內省(請參閱先前的範例)。
範例:自我修改。下列函式 moveProperty
會將一個屬性從來源移至目標。它透過屬性存取的方括號運算子、指定運算子以及 delete
運算子來執行自我修改。(在實際程式碼中,我們可能使用 屬性描述子 來執行這項工作。)
function moveProperty(source, propertyName, target) {
target[propertyName] = source[propertyName];
delete source[propertyName];
}
以下是 moveProperty()
的使用方式
const obj1 = { color: 'blue' };
const obj2 = {};
moveProperty(obj1, 'color', obj2);
assert.deepEqual(
obj1, {});
assert.deepEqual(
obj2, { color: 'blue' });
ECMAScript 5 不支援攔截;Proxy 是為了填補這個缺口而建立的。
Proxy 為 JavaScript 帶來了攔截。它們的運作方式如下。我們可以在物件 obj
上執行許多操作,例如
obj
的屬性 prop
(obj.prop
)obj
是否有屬性 prop
('prop' in obj
)Proxy 是特殊物件,讓我們可以自訂部分這些操作。Proxy 是使用兩個參數建立的
handler
:對於每個操作,都有對應的處理函式,如果存在,就會執行該操作。這種函式會攔截該操作(在傳送至目標的途中),並稱為陷阱,這個術語源自作業系統領域。target
:如果處理函式沒有攔截操作,則會在目標上執行該操作。換句話說,它會作為處理函式的後備。在某種程度上,Proxy 會包裝目標。注意:「攔截」的動詞形式是「intercede」。攔截本質上是雙向的。攔截本質上是單向的。
在以下範例中,處理函式會攔截 get
和 has
操作。
const logged = [];
const target = {};
const handler = {
/** Intercepts: getting properties */
get(target, propKey, receiver) {
logged.push(`GET ${propKey}`);
return 123;
},
/** Intercepts: checking whether properties exist */
has(target, propKey) {
logged.push(`HAS ${propKey}`);
return true;
}
};
const proxy = new Proxy(target, handler);
如果我們取得屬性(A 行)或使用 in
運算子(B 行),處理函式會攔截這些操作
assert.equal(proxy.age, 123); // (A)
assert.equal('hello' in proxy, true); // (B)
assert.deepEqual(
logged, [
'GET age',
'HAS hello',
]);
處理函式沒有實作 set
陷阱(設定屬性)。因此,設定 proxy.age
會轉發至 target
,並導致設定 target.age
如果目標是函式,可以攔截兩個額外操作
apply
:呼叫函式。透過以下方式觸發
proxy(···)
proxy.call(···)
proxy.apply(···)
construct
:呼叫建構函式。透過以下方式觸發
new proxy(···)
僅針對函式目標啟用這些陷阱的原因很簡單:否則,我們將無法轉送運算 apply
和 construct
。
如果我們想要透過 Proxy 攔截方法呼叫,我們會面臨一個挑戰:沒有方法呼叫的陷阱。相反地,方法呼叫被視為兩個運算的順序
get
來擷取函式apply
來呼叫該函式因此,如果我們想要攔截方法呼叫,我們需要攔截兩個運算
get
並傳回函式。以下程式碼示範如何執行此操作。
const traced = [];
function traceMethodCalls(obj) {
const handler = {
get(target, propKey, receiver) {
const origMethod = target[propKey];
return function (...args) { // implicit parameter `this`!
const result = origMethod.apply(this, args);
traced.push(propKey + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
}
};
return new Proxy(obj, handler);
}
我們沒有使用 Proxy 進行第二次攔截;我們只是將原始方法包裝在函式中。
讓我們使用以下物件來嘗試 traceMethodCalls()
const obj = {
multiply(x, y) {
return x * y;
},
squared(x) {
return this.multiply(x, x);
},
};
const tracedObj = traceMethodCalls(obj);
assert.equal(
tracedObj.squared(9), 81);
assert.deepEqual(
traced, [
'multiply[9,9] -> 81',
'squared[9] -> 81',
]);
甚至 obj.squared()
內部的呼叫 this.multiply()
也會被追蹤!這是因為 this
會持續參照 Proxy。
這不是最有效率的解決方案。例如,可以快取方法。此外,Proxy 本身會影響效能。
Proxy 可以撤銷(關閉)
我們第一次呼叫函式 revoke
之後,我們對 proxy
執行的任何運算都會導致 TypeError
。後續呼叫 revoke
沒有進一步的效果。
const target = {}; // Start with an empty object
const handler = {}; // Don’t intercept anything
const {proxy, revoke} = Proxy.revocable(target, handler);
// `proxy` works as if it were the object `target`:
proxy.city = 'Paris';
assert.equal(proxy.city, 'Paris');
revoke();
assert.throws(
() => proxy.prop,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked$/
);
Proxy proto
可以成為物件 obj
的原型。從 obj
開始的一些運算可能會繼續在 proto
中。其中一個運算是 get
。
const proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});
const obj = Object.create(proto);
obj.weight;
// Output:
// 'GET weight'
屬性 weight
無法在 obj
中找到,這就是為什麼搜尋會繼續在 proto
中,並且陷阱 get
會在那裡觸發。還有更多會影響原型的運算;它們列在本章節的結尾。
處理常式未實作其陷阱的運算會自動轉送至目標。有時,我們希望在轉送運算之外執行一些任務。例如,攔截並記錄所有運算,而不阻止它們到達目標
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return delete target[propKey];
},
has(target, propKey) {
console.log('HAS ' + propKey);
return propKey in target;
},
// Other traps: similar
}
Reflect.*
對於每個陷阱,我們首先記錄操作的名稱,然後手動執行它並轉送它。JavaScript 有類似模組的物件 Reflect
,有助於轉送。
對於每個陷阱
Reflect
有方法
如果我們使用 Reflect
,先前的範例如下所示。
const handler = {
deleteProperty(target, propKey) {
console.log('DELETE ' + propKey);
return Reflect.deleteProperty(target, propKey);
},
has(target, propKey) {
console.log('HAS ' + propKey);
return Reflect.has(target, propKey);
},
// Other traps: similar
}
現在每個陷阱所做的都非常相似,我們可以透過 Proxy 實作處理常式
const handler = new Proxy({}, {
get(target, trapName, receiver) {
// Return the handler method named trapName
return (...args) => {
console.log(trapName.toUpperCase() + ' ' + args[1]);
// Forward the operation
return Reflect[trapName](...args);
};
},
});
對於每個陷阱,Proxy 會透過 get
操作要求處理常式方法,而我們給它一個。也就是說,所有處理常式方法都可以透過單一元方法 get
實作。讓這種虛擬化變得簡單,是 Proxy API 的目標之一。
讓我們使用這個基於 Proxy 的處理常式
const target = {};
const proxy = new Proxy(target, handler);
proxy.distance = 450; // set
assert.equal(proxy.distance, 450); // get
// Was `set` operation correctly forwarded to `target`?
assert.equal(
target.distance, 450);
// Output:
// 'SET distance'
// 'GETOWNPROPERTYDESCRIPTOR distance'
// 'DEFINEPROPERTY distance'
// 'GET distance'
Proxy 物件可以視為攔截對其目標物件執行的操作,Proxy 會包裝目標。Proxy 的處理常式物件就像 Proxy 的觀察者或監聽器。它會指定哪些操作應該透過實作對應的方法(例如 get
用於讀取屬性)來攔截。如果缺少操作的處理常式方法,則不會攔截該操作。它會直接轉送至目標。
因此,如果處理常式是空物件,Proxy 應該透明地包裝目標。唉,這並不總是有效。
this
在我們深入探討之前,讓我們快速回顧包裝目標如何影響 this
const target = {
myMethod() {
return {
thisIsTarget: this === target,
thisIsProxy: this === proxy,
};
}
};
const handler = {};
const proxy = new Proxy(target, handler);
如果我們直接呼叫 target.myMethod()
,this
會指向 target
如果我們透過 Proxy 呼叫該方法,this
會指向 proxy
也就是說,如果 Proxy 將方法呼叫轉送至目標,this
就不會變更。因此,如果目標使用 this
(例如呼叫方法),Proxy 會繼續在迴圈中。
通常,具有空處理常式的 Proxies 會透明地包裝目標:我們不會注意到它們的存在,而且它們不會變更目標的行為。
不過,如果目標透過不受 Proxy 控制的機制將資訊與 this
關聯起來,我們就會遇到問題:因為根據目標是否被包裝,關聯的資訊不同,所以事情會失敗。
例如,以下類別 Person
會將私人資訊儲存在 WeakMap _name
中(有關此技術的更多資訊,請參閱 JavaScript for impatient programmers)
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
Person
的執行個體無法透明地包裝
const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
const proxy = new Proxy(jane, {});
assert.equal(proxy.name, undefined);
jane.name
與包裝的 proxy.name
不同。以下實作沒有這個問題
class Person2 {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
}
const jane = new Person2('Jane');
assert.equal(jane.name, 'Jane');
const proxy = new Proxy(jane, {});
assert.equal(proxy.name, 'Jane');
大多數內建建構函式的執行個體也會使用不受 Proxy 攔截的機制。因此,它們也無法透明地包裝。如果我們使用 Date
的執行個體,就可以看到這一點
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
assert.throws(
() => proxy.getFullYear(),
/^TypeError: this is not a Date object\.$/
);
不受 Proxy 影響的機制稱為內部插槽。這些插槽是與執行個體關聯的類似屬性的儲存空間。規範將這些插槽視為名稱以方括號表示的屬性。例如,以下方法是內部的,可以在所有物件 O
上呼叫
與屬性不同,存取內部插槽並非透過一般的「取得」和「設定」操作來完成。如果透過 Proxy 呼叫 .getFullYear()
,它無法在 this
上找到需要的內部插槽,並會透過 TypeError
抱怨。
對於 Date
方法,語言規範說明
除非另有明確定義,否則以下定義的 Date 原型物件的方法並非一般方法,傳遞給它們的
this
值必須是具有已初始化為時間值的[[DateValue]]
內部插槽的物件。
作為解決方法,我們可以變更處理常式轉送方法呼叫的方式,並選擇性地將 this
設定為目標,而不是 Proxy
const handler = {
get(target, propKey, receiver) {
if (propKey === 'getFullYear') {
return target.getFullYear.bind(target);
}
return Reflect.get(target, propKey, receiver);
},
};
const proxy = new Proxy(new Date('2030-12-24'), handler);
assert.equal(proxy.getFullYear(), 2030);
這種方法的缺點是方法在 this
上執行的操作都不會經過 Proxy。
與其他內建函式不同,陣列可以透明地包裝
const p = new Proxy(new Array(), {});
p.push('a');
assert.equal(p.length, 1);
p.length = 0;
assert.equal(p.length, 0);
陣列可以包裝的原因是,即使屬性存取已自訂為讓 .length
運作,陣列方法並不會依賴內部插槽,它們是一般方法。
本節說明 Proxy 的用途。這將讓我們有機會看到 API 的實際運作。
get
、set
)假設我們有一個函式 tracePropertyAccesses(obj, propKeys)
,只要 obj
的屬性(其鍵在陣列 propKeys
中)被設定或取得,就會記錄下來。在以下程式碼中,我們將該函式套用至 Point
類別的實例
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `Point(${this.x}, ${this.y})`;
}
}
// Trace accesses to properties `x` and `y`
const point = new Point(5, 7);
const tracedPoint = tracePropertyAccesses(point, ['x', 'y']);
取得和設定追蹤物件 p
的屬性會產生以下效果
有趣的是,只要 Point
存取屬性,追蹤也會運作,因為 this
現在是指向追蹤物件,而不是 Point
的實例
tracePropertyAccesses()
如果不使用 Proxy,我們會以以下方式實作 tracePropertyAccesses()
。我們將每個屬性替換為一個追蹤存取的 getter 和 setter。setter 和 getter 使用一個額外的物件 propData
來儲存屬性的資料。請注意,我們會破壞性地變更原始實作,這表示我們正在進行元程式設計。
function tracePropertyAccesses(obj, propKeys, log=console.log) {
// Store the property data here
const propData = Object.create(null);
// Replace each property with a getter and a setter
propKeys.forEach(function (propKey) {
propData[propKey] = obj[propKey];
Object.defineProperty(obj, propKey, {
get: function () {
log('GET '+propKey);
return propData[propKey];
},
set: function (value) {
log('SET '+propKey+'='+value);
propData[propKey] = value;
},
});
});
return obj;
}
參數 log
可以更輕鬆地對此函式進行單元測試
const obj = {};
const logged = [];
tracePropertyAccesses(obj, ['a', 'b'], x => logged.push(x));
obj.a = 1;
assert.equal(obj.a, 1);
obj.c = 3;
assert.equal(obj.c, 3);
assert.deepEqual(
logged, [
'SET a=1',
'GET a',
]);
tracePropertyAccesses()
Proxy 為我們提供了更簡單的解決方案。我們攔截屬性的取得和設定,而不必變更實作。
function tracePropertyAccesses(obj, propKeys, log=console.log) {
const propKeySet = new Set(propKeys);
return new Proxy(obj, {
get(target, propKey, receiver) {
if (propKeySet.has(propKey)) {
log('GET '+propKey);
}
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (propKeySet.has(propKey)) {
log('SET '+propKey+'='+value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}
get
、set
)在存取屬性時,JavaScript 非常寬容。例如,如果我們嘗試讀取屬性但拼錯名稱,我們不會收到例外狀況,而是會取得結果 undefined
。
我們可以使用 Proxy 在這種情況下取得例外狀況。運作方式如下。我們讓 Proxy 成為物件的原型。如果在物件中找不到屬性,則會觸發 Proxy 的 get
陷阱
get
作業轉發至目標來執行此動作(Proxy 從目標取得其原型)。以下是此方法的實作
const propertyCheckerHandler = {
get(target, propKey, receiver) {
// Only check string property keys
if (typeof propKey === 'string' && !(propKey in target)) {
throw new ReferenceError('Unknown property: ' + propKey);
}
return Reflect.get(target, propKey, receiver);
}
};
const PropertyChecker = new Proxy({}, propertyCheckerHandler);
讓我們對物件使用 PropertyChecker
const jane = {
__proto__: PropertyChecker,
name: 'Jane',
};
// Own property:
assert.equal(
jane.name,
'Jane');
// Typo:
assert.throws(
() => jane.nmae,
/^ReferenceError: Unknown property: nmae$/);
// Inherited property:
assert.equal(
jane.toString(),
'[object Object]');
PropertyChecker
作為類別如果我們將 PropertyChecker
變成建構函式,我們可以使用 extends
將其用於類別
// We can’t change .prototype of classes, so we are using a function
function PropertyChecker2() {}
PropertyChecker2.prototype = new Proxy({}, propertyCheckerHandler);
class Point extends PropertyChecker2 {
constructor(x, y) {
super();
this.x = x;
this.y = y;
}
}
const point = new Point(5, 7);
assert.equal(point.x, 5);
assert.throws(
() => point.z,
/^ReferenceError: Unknown property: z/);
以下是 point
的原型鏈
const p = Object.getPrototypeOf.bind(Object);
assert.equal(p(point), Point.prototype);
assert.equal(p(p(point)), PropertyChecker2.prototype);
assert.equal(p(p(p(point))), Object.prototype);
如果我們擔心意外建立屬性,我們有兩個選項
set
的物件中。Object.preventExtensions(obj)
將物件 obj
設為不可擴充,這表示 JavaScript 不允許我們將新的(自有)屬性新增至 obj
。get
)有些陣列方法允許我們透過 -1
參照最後一個元素,透過 -2
參照倒數第二個元素,依此類推。例如
唉,當透過方括號運算子([]
)存取元素時,這項功能無法運作。不過,我們可以使用 Proxy 來新增該功能。以下函式 createArray()
會建立支援負索引的陣列。它透過將 Proxy 包裝在陣列實例中來執行此動作。Proxy 會攔截由方括號運算子觸發的 get
作業。
function createArray(...elements) {
const handler = {
get(target, propKey, receiver) {
if (typeof propKey === 'string') {
const index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
}
return Reflect.get(target, propKey, receiver);
}
};
// Wrap a proxy around the Array
return new Proxy(elements, handler);
}
const arr = createArray('a', 'b', 'c');
assert.equal(
arr[-1], 'c');
assert.equal(
arr[0], 'a');
assert.equal(
arr.length, 3);
set
)資料繫結是關於在物件之間同步資料。一個常見的用例是基於 MVC (Model View Controller) 模式的小工具:透過資料繫結,如果我們變更模型 (小工具視覺化的資料),則檢視 (小工具) 會保持最新狀態。
若要實作資料繫結,我們必須觀察並對物件所做的變更做出反應。下列程式碼片段是觀察變更如何對陣列發揮作用的草圖。
function createObservedArray(callback) {
const array = [];
return new Proxy(array, {
set(target, propertyKey, value, receiver) {
callback(propertyKey, value);
return Reflect.set(target, propertyKey, value, receiver);
}
});
}
const observedArray = createObservedArray(
(key, value) => console.log(
`${JSON.stringify(key)} = ${JSON.stringify(value)}`));
observedArray.push('a');
// Output:
// '"0" = "a"'
// '"length" = 1'
可以透過 Proxy 建立一個物件,可以在其上呼叫任意方法。在下列範例中,函式 createWebService()
會建立一個這樣的物件 service
。呼叫 service
的方法會擷取與同名網路服務資源的內容。擷取是透過 Promise 處理。
const service = createWebService('http://example.com/data');
// Read JSON data in http://example.com/data/employees
service.employees().then((jsonStr) => {
const employees = JSON.parse(jsonStr);
// ···
});
下列程式碼是 createWebService
的快速且簡便實作,不使用 Proxy。我們需要事先知道將在 service
上呼叫哪些方法。參數 propKeys
會提供我們這些資訊;它包含一個包含方法名稱的陣列。
function createWebService(baseUrl, propKeys) {
const service = {};
for (const propKey of propKeys) {
service[propKey] = () => {
return httpGet(baseUrl + '/' + propKey);
};
}
return service;
}
使用 Proxy 時,createWebService()
會更簡單
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
// Return the method to be called
return () => httpGet(baseUrl + '/' + propKey);
}
});
}
這兩個實作都使用下列函式來建立 HTTP GET 要求 (其運作方式說明於 JavaScript for impatient programmers 中)。
function httpGet(url) {
return new Promise(
(resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText); // (A)
} else {
// Something went wrong (404, etc.)
reject(new Error(xhr.statusText)); // (B)
}
}
xhr.onerror = () => {
reject(new Error('Network error')); // (C)
};
xhr.open('GET', url);
xhr.send();
});
}
可撤銷參考 的運作方式如下:不允許用戶端直接存取重要資源 (一個物件),只能透過參考 (一個中間物件,資源的包裝器) 存取。通常,套用至參考的每個操作都會轉送至資源。在用戶端完成後,會透過關閉參考來撤銷參考,以保護資源。從此以後,對參考套用操作會擲回例外,而且不再轉送任何內容。
在下列範例中,我們為資源建立一個可撤銷參考。然後,我們透過參考讀取資源的一個屬性。這會運作,因為參考授予我們存取權。接下來,我們撤銷參考。現在,參考不再讓我們讀取屬性。
const resource = { x: 11, y: 8 };
const {reference, revoke} = createRevocableReference(resource);
// Access granted
assert.equal(reference.x, 11);
revoke();
// Access denied
assert.throws(
() => reference.x,
/^TypeError: Cannot perform 'get' on a proxy that has been revoked/
);
Proxy 非常適合實作可撤銷參考,因為它們可以攔截並轉送操作。這是 createRevocableReference
的一個簡單 Proxy-based 實作
function createRevocableReference(target) {
let enabled = true;
return {
reference: new Proxy(target, {
get(target, propKey, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'get' on a proxy that has been revoked`);
}
return Reflect.get(target, propKey, receiver);
},
has(target, propKey) {
if (!enabled) {
throw new TypeError(
`Cannot perform 'has' on a proxy that has been revoked`);
}
return Reflect.has(target, propKey);
},
// (Remaining methods omitted)
}),
revoke: () => {
enabled = false;
},
};
}
可以透過前一節的 Proxy-as-handler 技術簡化程式碼。這次,處理常式基本上是 Reflect
物件。因此,get
陷阱通常會傳回適當的 Reflect
方法。如果參考已被撤銷,則會擲回 TypeError
。
function createRevocableReference(target) {
let enabled = true;
const handler = new Proxy({}, {
get(_handlerTarget, trapName, receiver) {
if (!enabled) {
throw new TypeError(
`Cannot perform '${trapName}' on a proxy`
+ ` that has been revoked`);
}
return Reflect[trapName];
}
});
return {
reference: new Proxy(target, handler),
revoke: () => {
enabled = false;
},
};
}
但是,我們不必自己實作可撤銷參考,因為可以撤銷 Proxy。這次,撤銷會在 Proxy 中發生,而不是在處理常式中。處理常式必須做的就是將每個操作轉送至目標。正如我們所見,如果處理常式未實作任何陷阱,就會自動發生。
function createRevocableReference(target) {
const handler = {}; // forward everything
const { proxy, revoke } = Proxy.revocable(target, handler);
return { reference: proxy, revoke };
}
膜 建立在可撤銷參考的概念上:安全執行不受信任程式碼的函式庫會在該程式碼周圍包裝一個膜,以隔離程式碼並確保系統的其餘部分安全。物件會在兩個方向傳遞膜
在這兩種情況下,可撤銷的參考都會包覆在物件周圍。由包覆函式或方法傳回的物件也會被包覆。此外,如果將包覆的濕物件傳回隔離層,它將會被解開包覆。
一旦不受信任的程式碼完成,所有可撤銷的參考都會被撤銷。因此,它在外部的程式碼都無法再執行,而它所引用的外部物件也將停止運作。 Caja 編譯器 是「一個讓第三方 HTML、CSS 和 JavaScript 能安全嵌入到您的網站中的工具」。它使用隔離層來達成這個目標。
瀏覽器的文件物件模型 (DOM) 通常實作為 JavaScript 和 C++ 的混合。在純 JavaScript 中實作它對於下列用途很有用
唉,標準 DOM 可以執行一些在 JavaScript 中不易複製的事情。例如,大多數 DOM 集合都是對 DOM 當前狀態的即時檢視,只要 DOM 發生變更,它們就會動態變更。因此,DOM 的純 JavaScript 實作並非非常有效率。將 Proxy 加入 JavaScript 的原因之一就是為了啟用更有效率的 DOM 實作。
Proxy 有更多使用案例。例如
遠端處理:本機暫存物件將方法呼叫轉發到遠端物件。這個使用案例類似於網路服務範例。
資料庫的資料存取物件:讀取和寫入物件會讀取和寫入資料庫。這個使用案例類似於網路服務範例。
設定檔:攔截方法呼叫以追蹤在每個方法中花費多少時間。這個使用案例類似於追蹤範例。
Immer (由 Michel Weststrate 製作) 有助於非破壞性地更新資料。應該套用的變更會透過呼叫方法、設定屬性、設定陣列元素等(潛在巢狀)草稿狀態 來指定。草稿狀態是透過 Proxy 實作的。
MobX 讓您可以觀察資料結構的變更,例如物件、陣列和類別實例。這是透過 Proxy 實作的。
Alpine.js (由 Caleb Porzio 製作) 是一個透過 Proxy 實作資料繫結的前端函式庫。
on-change (由 Sindre Sorhus 所撰寫) 觀察物件的變更(透過 Proxy)並回報。
Env utility (由 Nicholas C. Zakas 所撰寫) 讓您可以透過屬性存取環境變數,如果變數不存在,則會擲回例外。這透過 Proxy 來實作。
LDflex (由 Ruben Verborgh 和 Ruben Taelman 所撰寫) 提供連結資料的查詢語言(想想語意網路)。流暢的查詢 API 是透過 Proxy 來實作。
在這個區段,我們將更深入探討 Proxy 的運作方式以及它們為何如此運作。
Firefox 曾經支援一段時間的受限介入式元程式設計:如果物件 O
有名為 __noSuchMethod__
的方法,則每當呼叫 O
上不存在的方法時,就會通知它。以下程式碼示範其運作方式
const calc = {
__noSuchMethod__: function (methodName, args) {
switch (methodName) {
case 'plus':
return args.reduce((a, b) => a + b);
case 'times':
return args.reduce((a, b) => a * b);
default:
throw new TypeError('Unsupported: ' + methodName);
}
}
};
// All of the following method calls are implemented via
// .__noSuchMethod__().
assert.equal(
calc.plus(3, 5, 2), 10);
assert.equal(
calc.times(2, 3, 4), 24);
assert.equal(
calc.plus('Parts', ' of ', 'a', ' string'),
'Parts of a string');
因此,__noSuchMethod__
的運作方式類似 Proxy 陷阱。與 Proxy 相比,陷阱是我們想要攔截其運作的物件的自身方法或繼承方法。這種方法的問題在於基礎層級(一般方法)和元層級(__noSuchMethod__
)會混在一起。基礎層級程式碼可能會意外呼叫或看到元層級方法,而且有意外定義元層級方法的可能性。
即使在標準 ECMAScript 中,基礎層級和元層級有時也會混在一起。例如,以下元程式設計機制可能會失敗,因為它們存在於基礎層級
obj.hasOwnProperty(propKey)
:如果原型鏈中的屬性覆寫內建實作,則此呼叫可能會失敗。例如,在以下程式碼中,obj
會導致失敗
const obj = { hasOwnProperty: null };
assert.throws(
() => obj.hasOwnProperty('width'),
/^TypeError: obj.hasOwnProperty is not a function/
);
以下是呼叫 .hasOwnProperty()
的安全方式
func.call(···)
、func.apply(···)
:這兩個方法的問題和解決方案與 .hasOwnProperty()
相同。
obj.__proto__
:在純粹物件中,__proto__
是讓我們取得和設定接收者的原型的特殊屬性。因此,當我們將純粹物件用作字典時,我們必須 避免將 __proto__
用作屬性金鑰。
現在,應該很明顯地知道讓(基礎層級)屬性金鑰變特殊是有問題的。因此,Proxy 是分層的:基礎層級(Proxy 物件)和元層級(處理常式物件)是分開的。
Proxy 用於兩個角色
作為包裝器,它們包裝其目標,它們控制對目標的存取。包裝器的範例包括:可撤銷資源和透過 Proxy 追蹤。
作為虛擬物件,它們只是具有特殊行為的物件,而其目標並不重要。範例是將方法呼叫轉送至遠端物件的 Proxy。
Proxy API 的早期設計將 Proxy 視為純粹的虛擬物件。然而,事實證明,即使在該角色中,目標也很有用,用於強制不變式(稍後說明)和作為處理常式未實作的陷阱的後備。
Proxy 有兩種防護方式
這兩個原則賦予 Proxy 相當大的能力,可用於偽裝為其他物件。強制執行不變式(稍後說明)的原因之一是為了控制這種能力。
如果我們確實需要一種方法來區分 Proxy 和非 Proxy,我們必須自己實作。下列程式碼是一個模組 lib.mjs
,它匯出兩個函式:其中一個建立 Proxy,另一個判斷物件是否為其中一個 Proxy。
// lib.mjs
const proxies = new WeakSet();
export function createProxy(obj) {
const handler = {};
const proxy = new Proxy(obj, handler);
proxies.add(proxy);
return proxy;
}
export function isProxy(obj) {
return proxies.has(obj);
}
此模組使用資料結構 WeakSet
來追蹤 Proxy。WeakSet
非常適合此目的,因為它不會阻止其元素被垃圾回收。
下一個範例顯示如何使用 lib.mjs
。
// main.mjs
import { createProxy, isProxy } from './lib.mjs';
const proxy = createProxy({});
assert.equal(isProxy(proxy), true);
assert.equal(isProxy({}), false);
在本節中,我們將探討 JavaScript 的內部結構以及 Proxy 陷阱的選擇方式。
在程式語言和 API 設計的背景下,協定是一組介面加上使用它們的規則。ECMAScript 規範說明如何執行 JavaScript 程式碼。它包含一個處理物件的協定。此協定在元層級運作,有時稱為元物件協定 (MOP)。JavaScript MOP 包含所有物件都具有的內部方法。“內部”表示它們只存在於規範中(JavaScript 引擎可能具有或不具有它們),且無法從 JavaScript 存取。內部方法的名稱以雙中括號撰寫。
用於取得屬性的內部方法稱為 .[[Get]]()
。如果我們使用雙底線而不是雙中括號,此方法將大致在 JavaScript 中實作如下。
// Method definition
__Get__(propKey, receiver) {
const desc = this.__GetOwnProperty__(propKey);
if (desc === undefined) {
const parent = this.__GetPrototypeOf__();
if (parent === null) return undefined;
return parent.__Get__(propKey, receiver); // (A)
}
if ('value' in desc) {
return desc.value;
}
const getter = desc.get;
if (getter === undefined) return undefined;
return getter.__Call__(receiver, []);
}
此程式碼中呼叫的 MOP 方法為
[[GetOwnProperty]]
(陷阱 getOwnPropertyDescriptor
)[[GetPrototypeOf]]
(陷阱 getPrototypeOf
)[[Get]]
(陷阱 get
)[[Call]]
(陷阱 apply
)在 A 行中,我們可以看到為什麼原型鏈中的 Proxy 會找出 get
,如果在「較早」的物件中找不到屬性:如果沒有金鑰為 propKey
的自有屬性,則會在 this
的原型 parent
中繼續搜尋。
基本運算與衍生運算。我們可以看到 .[[Get]]()
呼叫其他 MOP 運算。執行此動作的運算稱為衍生運算。不依賴其他運算的運算稱為基本運算。
Proxy 的物件元協定與一般物件不同。對於一般物件,衍生運算會呼叫其他運算。對於 Proxy,每個運算(不論是基本或衍生)都會被處理常式攔截或轉送至目標。
哪些運算應該可以透過 Proxy 攔截?
後者的優點是能提升效能且更方便。例如,如果沒有 get
的陷阱,我們必須透過 getOwnPropertyDescriptor
來實作其功能。
包含衍生陷阱的缺點是可能會導致 Proxy 行為不一致。例如,get
可能會傳回與 getOwnPropertyDescriptor
傳回的描述符中值不同的值。
Proxy 的攔截是選擇性的:我們無法攔截每個語言運算。為什麼有些運算會被排除?我們來看兩個原因。
首先,穩定運算不適合攔截。如果運算總是對相同的引數產生相同的結果,則該運算是穩定的。如果 Proxy 可以攔截穩定的運算,則它可能會變得不穩定,因此不可靠。嚴格相等(===
)就是一種穩定的運算。它無法被攔截,且其結果是透過將 Proxy 本身視為另一個物件來計算的。維持穩定的另一種方式是將運算套用至目標,而非 Proxy。如後續說明,當我們檢視如何對 Proxy 執行不變式時,這會發生在 Object.getPrototypeOf()
套用至目標不可擴充的 Proxy 時。
不讓更多運算可以攔截的第二個原因是,攔截表示在通常不可能的情況下執行自訂程式碼。這種程式碼交錯發生的次數越多,就越難理解和除錯程式。它也會對效能產生負面影響。
get
與 invoke
如果我們想要透過 Proxy 建立虛擬方法,我們必須從 get
陷阱傳回函式。這引發了一個問題:為什麼不為方法呼叫(例如 invoke
)引入額外的陷阱?這將使我們能夠區分
obj.prop
取得屬性(陷阱 get
)obj.prop()
呼叫方法(陷阱 invoke
)有兩個原因不這麼做。
首先,並非所有實作都區分 get
和 invoke
。例如,Apple 的 JavaScriptCore 沒有區分。
其次,提取方法並透過 .call()
或 .apply()
在稍後呼叫它,應該與透過派送呼叫方法具有相同效果。換句話說,下列兩個變體應該等效運作。如果有一個額外的陷阱 invoke
,那麼這種等效性將更難以維持。
// Variant 1: call via dynamic dispatch
const result1 = obj.m();
// Variant 2: extract and call directly
const m = obj.m;
const result2 = m.call(obj);
invoke
的使用案例有些事情只有在我們能夠區分 get
和 invoke
時才能執行。因此,這些事情使用目前的 Proxy API 是不可能的。兩個範例是:自動繫結和攔截遺失的方法。讓我們探討如果 Proxy 支援 invoke
,要如何實作它們。
自動繫結。透過讓 Proxy 成為物件 obj
的原型,我們可以自動繫結方法
obj.m
擷取方法 m
的值,會傳回一個其 this
繫結到 obj
的函式。obj.m()
執行方法呼叫。自動繫結有助於使用方法作為回呼。例如,前一個範例的變體 2 會變得更簡單
攔截遺失的方法。invoke
讓 Proxy 模擬先前提到的 __noSuchMethod__
機制。Proxy 將再次成為物件 obj
的原型。它會根據未知屬性 prop
的存取方式做出不同的反應
obj.prop
讀取該屬性,不會發生攔截,且會傳回 undefined
。obj.prop()
,則 Proxy 會攔截,例如,通知回呼。在我們探討不變式是什麼,以及它們如何對 Proxy 強制執行之前,讓我們回顧如何透過不可擴充性和不可組態性來保護物件。
有兩種保護物件的方式
不可擴充性保護物件:如果物件不可擴充,我們無法新增屬性,也無法變更其原型。
不可組態性保護屬性(或更確切地說,是它們的屬性)
writable
控制屬性的值是否可以變更。configurable
控制屬性的屬性是否可以變更。有關此主題的更多資訊,請參閱 §10「保護物件不被變更」。
傳統上,不可擴充性和不可組態性是
這些以及其他在語言操作中保持不變的特徵稱為不變式。透過 Proxy 很容易違反不變式,因為它們本質上不受不可擴充性等約束。Proxy API 透過檢查目標物件和處理常式方法的結果來防止這種情況發生。
接下來的兩個小節描述四個不變式。本章節的結尾提供了不變式的完整清單。
以下兩個不變式涉及不可擴充性和不可設定性。這些不變式透過使用目標物件進行簿記來強制執行:處理常式傳回的結果必須大多與目標物件同步。
Object.preventExtensions(obj)
傳回 true
,則所有未來的呼叫都必須傳回 false
,且 obj
現在必須不可擴充。
true
,但目標物件不可擴充,則透過擲回 TypeError
來強制執行 Proxy。Object.isExtensible(obj)
必須始終傳回 false
。
Object.isExtensible(target)
(經過強制轉換後)不同,則透過擲回 TypeError
來強制執行 Proxy。以下兩個不變式透過檢查回傳值來強制執行
Object.isExtensible(obj)
必須傳回布林值。
Object.getOwnPropertyDescriptor(obj, ···)
必須傳回物件或 undefined
。
TypeError
來強制執行 Proxy。強制執行不變式具有以下優點
接下來的兩個區段提供強制執行不變式的範例。
針對 getPrototypeOf
陷阱,如果目標不可擴充,則 Proxy 必須傳回目標的原型。
為了展示此不變式,我們來建立一個處理常式,傳回與目標原型不同的原型
如果目標可擴充,則偽造原型會起作用
const extensibleTarget = {};
const extProxy = new Proxy(extensibleTarget, handler);
assert.equal(
Object.getPrototypeOf(extProxy), fakeProto);
然而,如果我們偽造非可延伸物件的原型,我們會收到錯誤。
const nonExtensibleTarget = {};
Object.preventExtensions(nonExtensibleTarget);
const nonExtProxy = new Proxy(nonExtensibleTarget, handler);
assert.throws(
() => Object.getPrototypeOf(nonExtProxy),
{
name: 'TypeError',
message: "'getPrototypeOf' on proxy: proxy target is"
+ " non-extensible but the trap did not return its"
+ " actual prototype",
});
如果目標具有不可寫入且不可設定的屬性,則處理常式必須在回應get
陷阱時傳回該屬性的值。為了展示此不變式,讓我們建立一個處理常式,它總是為屬性傳回相同的值。
const handler = {
get(target, propKey) {
return 'abc';
}
};
const target = Object.defineProperties(
{}, {
manufacturer: {
value: 'Iso Autoveicoli',
writable: true,
configurable: true
},
model: {
value: 'Isetta',
writable: false,
configurable: false
},
});
const proxy = new Proxy(target, handler);
屬性target.manufacturer
既不可寫入也不可設定,這表示處理常式可以假裝它具有不同的值
然而,屬性target.model
既不可寫入也不可設定。因此,我們無法偽造它的值
assert.throws(
() => proxy.model,
{
name: 'TypeError',
message: "'get' on proxy: property 'model' is a read-only and"
+ " non-configurable data property on the proxy target but"
+ " the proxy did not return its actual value (expected"
+ " 'Isetta' but got 'abc')",
});
enumerate
陷阱在哪裡?ECMAScript 6 原本有一個陷阱enumerate
,它會由for-in
迴圈觸發。但它最近被移除,以簡化代理。Reflect.enumerate()
也已移除。(來源:TC39 備忘錄)
本節是代理 API 的快速參考
Proxy
Reflect
參考使用下列自訂類型
有兩種建立代理的方法
const proxy = new Proxy(target, handler)
建立一個新的代理物件,具有指定的目標和指定的處理常式。
const {proxy, revoke} = Proxy.revocable(target, handler)
建立一個代理,它可以透過函式revoke
撤銷。revoke
可以呼叫多次,但只有第一次呼叫會產生作用,並關閉proxy
。之後,對proxy
執行的任何操作都會導致擲出TypeError
。
本小節說明處理常式可以實作哪些陷阱,以及哪些操作會觸發這些陷阱。幾個陷阱會傳回布林值。對於陷阱has
和isExtensible
,布林值是操作的結果。對於所有其他陷阱,布林值表示操作是否成功。
所有物件的陷阱
defineProperty(target, propKey, propDesc): 布林值
Object.defineProperty(proxy, propKey, propDesc)
deleteProperty(target, propKey): 布林值
delete proxy[propKey]
delete proxy.someProp
get(target, propKey, receiver): 任意
receiver[propKey]
receiver.someProp
getOwnPropertyDescriptor(target, propKey): 未定義 | PropDesc
Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target):null 或物件
Object.getPrototypeOf(proxy)
has(target, propKey):布林值
propKey in proxy
isExtensible(target):布林值
Object.isExtensible(proxy)
ownKeys(target):PropertyKey 陣列
Object.getOwnPropertyPropertyNames(proxy)
(僅使用字串鍵)Object.getOwnPropertyPropertySymbols(proxy)
(僅使用符號鍵)Object.keys(proxy)
(僅使用可列舉的字串鍵;可列舉性透過 Object.getOwnPropertyDescriptor
檢查)preventExtensions(target):布林值
Object.preventExtensions(proxy)
set(target, propKey, value, receiver):布林值
receiver[propKey] = value
receiver.someProp = value
setPrototypeOf(target, proto):布林值
Object.setPrototypeOf(proxy, proto)
函數的陷阱(如果 target 是函數,則可用)
apply(target, thisArgument, argumentsList):任何
proxy.apply(thisArgument, argumentsList)
proxy.call(thisArgument, ...argumentsList)
proxy(...argumentsList)
construct(target, argumentsList, newTarget):物件
new proxy(..argumentsList)
下列操作為基本操作,它們不使用其他操作來執行工作:apply
、defineProperty
、deleteProperty
、getOwnPropertyDescriptor
、getPrototypeOf
、isExtensible
、ownKeys
、preventExtensions
、setPrototypeOf
所有其他操作為衍生操作,它們可透過基本操作來實作。例如,get
可透過 getPrototypeOf
遍歷原型鏈,並為每個鏈成員呼叫 getOwnPropertyDescriptor
,直到找到自有屬性或鏈結束為止。
不變性是處理常式的安全約束。本小節說明 Proxy API 如何強制執行不變性,以及如何執行。每當我們在下方讀到「處理常式必須執行 X」時,表示如果沒有執行,就會擲出 TypeError
。有些不變性限制回傳值,有些則限制參數。陷阱回傳值的正確性以兩種方式確保
TypeError
。以下是強制執行的完整不變性清單
apply(target, thisArgument, argumentsList):任何
construct(target, argumentsList, newTarget):物件
null
或任何其他原始值)。defineProperty(target, propKey, propDesc): 布林值
propDesc
將屬性 configurable
設定為 false
,則 target 必須具有非可設定的自有屬性,其鍵為 propKey
。propDesc
將屬性 configurable
和 writable
都設定為 false
,則 target 必須具有鍵為 propKey
的自有屬性,且該屬性不可設定且不可寫入。propKey
的自有屬性,則 propDesc
必須與該屬性相容:如果我們使用描述符重新定義目標屬性,則不得擲回任何例外。deleteProperty(target, propKey): 布林值
propKey
的不可設定自有屬性。propKey
的自有屬性。get(target, propKey, receiver): 任意
propKey
,則處理常式必須傳回該屬性的值。undefined
。getOwnPropertyDescriptor(target, propKey): 未定義 | PropDesc
undefined
或物件。getPrototypeOf(target):null 或物件
null
或物件。has(target, propKey):布林值
isExtensible(target):布林值
target.isExtensible()
相同。ownKeys(target):PropertyKey 陣列
preventExtensions(target):布林值
target.isExtensible()
為 false
,則處理常式只能傳回真值(表示成功變更)。set(target, propKey, value, receiver):布林值
propKey
,則無法變更該屬性。在這種情況下,value
必須是該屬性的值,否則會擲回 TypeError
。setPrototypeOf(target, proto):布林值
proto
必須與目標的原型相同。否則,會擲回 TypeError
。 ECMAScript 規範中的不變式
在規範中,不變式列於章節 “Proxy 物件內部方法和內部插槽” 中。
一般物件的下列運算會對原型鏈中的物件執行運算。因此,如果該鏈中的其中一個物件是 Proxy,則會觸發其陷阱。規範會將運算實作為內部自身方法(JavaScript 程式碼無法看到)。但在本節中,我們假設它們是一般方法,其名稱與陷阱相同。參數 target
會成為方法呼叫的接收者。
target.get(propertyKey, receiver)
target
沒有具有給定金鑰的自身屬性,則會在 target
的原型上呼叫 get
。target.has(propertyKey)
get
類似,如果 target
沒有具有給定金鑰的自身屬性,則會在 target
的原型上呼叫 has
。target.set(propertyKey, value, receiver)
get
類似,如果 target
沒有具有給定金鑰的自身屬性,則會在 target
的原型上呼叫 set
。所有其他運算只會影響自身屬性,它們不會對原型鏈造成影響。
ECMAScript 規範中的內部運算
在規範中,這些(和其他)運算會在章節 “一般物件內部方法和內部插槽” 中描述。
全域物件 Reflect
會將 JavaScript 元物件協定的所有可攔截操作實作為方法。這些方法的名稱與處理方法的名稱相同,如我們所見,這有助於將處理方法的操作轉送至目標。
Reflect.apply(target, thisArgument, argumentsList): any
類似於 Function.prototype.apply()
。
Reflect.construct(target, argumentsList, newTarget=target): object
new
算子作為一個函式。target
是要呼叫的建構函式,可選參數 newTarget
指向開始目前建構函式呼叫鏈的建構函式。
Reflect.defineProperty(target, propertyKey, propDesc): boolean
類似於 Object.defineProperty()
。
Reflect.deleteProperty(target, propertyKey): boolean
delete
算子作為一個函式。不過,它的運作方式略有不同:如果它成功刪除屬性或屬性從未存在,它會傳回 true
。如果無法刪除屬性且屬性仍然存在,它會傳回 false
。保護屬性免於刪除的唯一方法是讓它們不可設定。在草率模式中,delete
算子會傳回相同的結果。但在嚴格模式中,它會擲回 TypeError
,而不是傳回 false
。
Reflect.get(target, propertyKey, receiver=target): any
取得屬性的函式。可選參數 receiver
指向取得動作開始的物件。當 get
在原型鏈中稍後到達 getter 時,它會需要這個參數。然後,它會提供 this
的值。
Reflect.getOwnPropertyDescriptor(target, propertyKey): undefined|PropDesc
與 Object.getOwnPropertyDescriptor()
相同。
Reflect.getPrototypeOf(target): null|object
與 Object.getPrototypeOf()
相同。
Reflect.has(target, propertyKey): boolean
in
算子作為一個函式。
Reflect.isExtensible(target): boolean
與 Object.isExtensible()
相同。
Reflect.ownKeys(target): Array<PropertyKey>
在陣列中傳回所有自有屬性鍵:所有自有可列舉和不可列舉屬性的字串鍵和符號鍵。
Reflect.preventExtensions(target): boolean
類似於 Object.preventExtensions()
。
Reflect.set(target, propertyKey, value, receiver=target): boolean
設定屬性的函式。
Reflect.setPrototypeOf(target, proto): boolean
設定物件原型的標準新方法。目前非標準的方法,在大部分引擎中可行,是設定特殊屬性 __proto__
。
多種方法有布林值結果。對於 .has()
和 .isExtensible()
,它們是運算的結果。對於其餘方法,它們表示運算是否成功。
Reflect
的使用案例除了轉發運算之外,Reflect
有什麼用 [4]?
不同的回傳值:Reflect
重複下列 Object
方法,但其方法回傳布林值,表示運算是否成功(Object
方法回傳已修改的物件)。
Object.defineProperty(obj, propKey, propDesc): 物件
Object.preventExtensions(obj): 物件
Object.setPrototypeOf(obj, proto): 物件
運算子作為函式:下列 Reflect
方法實作功能,否則只能透過運算子取得
Reflect.construct(target, argumentsList, newTarget=target): object
Reflect.deleteProperty(target, propertyKey): boolean
Reflect.get(target, propertyKey, receiver=target): any
Reflect.has(target, propertyKey): boolean
Reflect.set(target, propertyKey, value, receiver=target): boolean
apply()
的較短版本:如果我們想要完全安全地呼叫函式的 apply()
方法,我們無法透過動態調度進行,因為函式可能有自己的屬性,其鍵為 'apply'
func.apply(thisArg, argArray) // not safe
Function.prototype.apply.call(func, thisArg, argArray) // safe
使用 Reflect.apply()
比安全版本短
刪除屬性時沒有例外:如果我們嘗試刪除不可設定的自己的屬性,delete
運算子會在嚴格模式中引發例外。Reflect.deleteProperty()
在這種情況下會回傳 false
。
Object.*
與 Reflect.*
未來,Object
將承載對一般應用程式有用的運算,而 Reflect
將承載較低層級的運算。
這結束了我們對 Proxy API 的深入探討。需要注意的一件事是,Proxy 會讓程式碼變慢。如果效能至關重要,這可能會很重要。
另一方面,效能通常不是至關重要的,而 Proxy 提供給我們的元程式設計能力是很棒的。
致謝
Allen Wirfs-Brock 指出了 §18.3.7 「陷阱:並非所有物件都能透過 Proxy 透明地包裝」 中說明的陷阱。
§18.4.3 「負陣列索引(
的想法來自 Hemanth.HM 的 部落格文章。get
)」
André Jaenisch 貢獻了使用 Proxy 的函式庫清單。
[1] “ECMAScript Reflection API 的設計” by Tom Van Cutsem and Mark Miller。技術報告,2012 年。[本章的重要來源。]
[2] “元物件協定的藝術” by Gregor Kiczales, Jim des Rivieres and Daniel G. Bobrow。書籍,1991 年。
[3] “Putting Metaclasses to Work: A New Dimension in Object-Oriented Programming” 作者為 Ira R. Forman 和 Scott H. Danforth。書籍,1999 年。
[4] “Harmony-reflect:為何我應該使用這個函式庫?” 作者為 Tom Van Cutsem。[說明為何 Reflect
很實用。]