Proxy
Proxy 物件允許你為一個物件建立一個代理,以攔截並重新定義該物件的基本操作。
描述
Proxy 物件允許你建立一個可以替代原始物件使用的物件,但它可以重新定義物件的基本 Object 操作,如獲取、設定和定義屬性。Proxy 物件通常用於記錄屬性訪問、驗證、格式化或清理輸入等。
你建立 Proxy 時需要兩個引數
target:你想要代理的原始物件handler:一個物件,它定義了哪些操作將被攔截以及如何重新定義被攔截的操作。
例如,此程式碼建立了一個 target 物件的代理。
const target = {
message1: "hello",
message2: "everyone",
};
const handler1 = {};
const proxy1 = new Proxy(target, handler1);
因為 handler 是空的,所以這個代理的行為與原始 target 相同
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone
要自定義代理,我們在 handler 物件上定義函式
const target = {
message1: "hello",
message2: "everyone",
};
const handler2 = {
get(target, prop, receiver) {
return "world";
},
};
const proxy2 = new Proxy(target, handler2);
在這裡,我們提供了 get() handler 的實現,它會攔截對 target 中屬性的訪問嘗試。
Handler 函式有時被稱為陷阱 (traps),大概是因為它們會捕獲對目標物件的呼叫。上面 handler2 中的陷阱重新定義了所有屬性訪問器。
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world
Proxy 物件經常與 Reflect 物件一起使用,該物件提供了一些與 Proxy 陷阱同名的方法。Reflect 方法提供了呼叫相應 物件內部方法 的反射語義。例如,如果我們不想重新定義物件的行為,我們可以呼叫 Reflect.get。
const target = {
message1: "hello",
message2: "everyone",
};
const handler3 = {
get(target, prop, receiver) {
if (prop === "message2") {
return "world";
}
return Reflect.get(...arguments);
},
};
const proxy3 = new Proxy(target, handler3);
console.log(proxy3.message1); // hello
console.log(proxy3.message2); // world
Reflect 方法仍然透過物件內部方法與物件互動——如果它在代理上呼叫,它不會“反代理”代理。如果你在代理陷阱中使用 Reflect 方法,並且 Reflect 方法呼叫被陷阱再次攔截,可能會發生無限遞迴。
術語
在討論代理的功能時,會使用以下術語。
Object internal methods
Objects 是屬性的集合。然而,語言本身並不提供任何機制來直接操作儲存在物件中的資料——相反,物件定義了一些內部方法來指定如何與之互動。例如,當你讀取 obj.x 時,你可能期望發生以下情況:
- 在 原型鏈 上向上搜尋
x屬性,直到找到為止。 - 如果
x是資料屬性,則返回屬性描述符的value屬性。 - 如果
x是訪問器屬性,則呼叫 getter,並返回 getter 的返回值。
在這個過程中,語言本身並沒有什麼特別之處——這僅僅是因為普通物件預設具有一個具有此行為的 [[Get]] 內部方法。obj.x 屬性訪問語法只是呼叫物件上的 [[Get]] 方法,而物件使用自己的內部方法實現來確定返回什麼。
再舉個例子,陣列與普通物件不同,因為它們有一個特殊的 length 屬性,當修改該屬性時,它會自動分配空槽或從陣列中刪除元素。同樣,新增陣列元素會自動更改 length 屬性。這是因為陣列有一個 [[DefineOwnProperty]] 內部方法,它知道在寫入整數索引時更新 length,或者在寫入 length 時更新陣列內容。像這樣的內部方法實現與普通物件不同的物件被稱為exotic objects。Proxy 使開發者能夠完全自定義此類物件。
所有物件都具有以下內部方法
| Internal method | Corresponding trap |
|---|---|
[[GetPrototypeOf]] |
getPrototypeOf() |
[[SetPrototypeOf]] |
setPrototypeOf() |
[[IsExtensible]] |
isExtensible() |
[[PreventExtensions]] |
preventExtensions() |
[[GetOwnProperty]] |
getOwnPropertyDescriptor() |
[[DefineOwnProperty]] |
defineProperty() |
[[HasProperty]] |
has() |
[[Get]] |
get() |
[[Set]] |
set() |
[[Delete]] |
deleteProperty() |
[[OwnPropertyKeys]] |
ownKeys() |
函式物件也具有以下內部方法
| Internal method | Corresponding trap |
|---|---|
[[Call]] |
apply() |
[[Construct]] |
construct() |
需要認識到的是,所有與物件的互動最終都會歸結為呼叫其中一個內部方法,並且所有這些方法都可以透過代理進行自定義。這意味著語言中幾乎沒有行為(除了一些關鍵的不變性)是確定的——一切都由物件本身定義。當你執行 delete obj.x 時,無法保證 "x" in obj 之後會返回 false——這取決於物件對 [[Delete]] 和 [[HasProperty]] 的實現。delete obj.x 可能會向控制檯記錄資訊、修改全域性狀態,甚至定義一個新屬性而不是刪除現有屬性,儘管在你的程式碼中應該避免這種語義。
所有內部方法都由語言本身呼叫,並且不能在 JavaScript 程式碼中直接訪問。 Reflect 名稱空間提供的方法除了進行一些輸入歸一化/驗證之外,幾乎不做什麼,只是呼叫內部方法。在每個陷阱的頁面中,我們列出了一些陷阱被呼叫的典型情況,但這些內部方法在大量地方被呼叫。例如,陣列方法透過這些內部方法讀寫陣列,因此像 push() 這樣的方法也會呼叫 get() 和 set() 陷阱。
大多數內部方法都很直接。唯一可能令人混淆的是 [[Set]] 和 [[DefineOwnProperty]]。對於普通物件,前者呼叫 setter;後者不呼叫。(如果不存在現有屬性或屬性是資料屬性,則 [[Set]] 會在內部呼叫 [[DefineOwnProperty]]。)雖然你可能知道 obj.x = 1 語法使用 [[Set]],而 Object.defineProperty() 使用 [[DefineOwnProperty]],但其他內建方法和語法所使用的語義並不明顯。例如,類欄位使用 [[DefineOwnProperty]] 語義,這就是為什麼在派生類上宣告欄位時,不會呼叫在超類中定義的 setter。
建構函式
Proxy()-
建立一個新的
Proxy物件。
注意: 沒有 Proxy.prototype 屬性,因此 Proxy 例項沒有特殊的屬性或方法。
靜態方法
Proxy.revocable()-
建立一個可撤銷的
Proxy物件。
示例
基本示例
在此示例中,當屬性名稱不在物件中時,數字 37 將作為預設值返回。它使用了 get() handler。
const handler = {
get(obj, prop) {
return prop in obj ? obj[prop] : 37;
},
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log("c" in p, p.c); // false, 37
無操作轉發代理
在此示例中,我們使用一個原生的 JavaScript 物件,我們的代理會將應用於該物件的所有操作轉發給它。
const target = {};
const p = new Proxy(target, {});
p.a = 37; // Operation forwarded to the target
console.log(target.a); // 37 (The operation has been properly forwarded!)
請注意,雖然這種“無操作”對於純 JavaScript 物件有效,但對於原生物件(如 DOM 元素、Map 物件或任何具有內部槽的物件)無效。有關更多資訊,請參閱 no private field forwarding。
無私有欄位轉發
代理仍然是另一個具有不同身份的物件——它是包裝物件和外部世界之間操作的代理。因此,代理無法直接訪問原始物件的 私有元素。
class Secret {
#secret;
constructor(secret) {
this.#secret = secret;
}
get secret() {
return this.#secret.replace(/\d+/, "[REDACTED]");
}
}
const secret = new Secret("123456");
console.log(secret.secret); // [REDACTED]
// Looks like a no-op forwarding...
const proxy = new Proxy(secret, {});
console.log(proxy.secret); // TypeError: Cannot read private member #secret from an object whose class did not declare it
這是因為當呼叫代理的 get 陷阱時,this 的值是 proxy 而不是原始的 secret,因此無法訪問 #secret。要解決此問題,請使用原始的 secret 作為 this。
const proxy = new Proxy(secret, {
get(target, prop, receiver) {
// By default, it looks like Reflect.get(target, prop, receiver)
// which has a different value of `this`
return target[prop];
},
});
console.log(proxy.secret);
對於方法來說,這意味著你還需要將方法的 this 值重定向到原始物件。
class Secret {
#x = 1;
x() {
return this.#x;
}
}
const secret = new Secret();
const proxy = new Proxy(secret, {
get(target, prop, receiver) {
const value = target[prop];
if (value instanceof Function) {
return function (...args) {
return value.apply(this === receiver ? target : this, args);
};
}
return value;
},
});
console.log(proxy.x());
一些原生的 JavaScript 物件具有稱為內部槽的屬性,這些屬性無法從 JavaScript 程式碼訪問。例如,Map 物件具有一個名為 [[MapData]] 的內部槽,它儲存 map 的鍵值對。因此,你無法輕易地為 map 建立一個轉發代理。
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy
你必須使用上面說明的“this 恢復”代理來解決這個問題。
驗證
使用 Proxy,你可以輕鬆地驗證傳遞給物件的數值。此示例使用 set() handler。
const validator = {
set(obj, prop, value) {
if (prop === "age") {
if (!Number.isInteger(value)) {
throw new TypeError("The age is not an integer");
}
if (value > 200) {
throw new RangeError("The age seems invalid");
}
}
// The default behavior to store the value
obj[prop] = value;
// Indicate success
return true;
},
};
const person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = "young"; // Throws an exception
person.age = 300; // Throws an exception
操作 DOM 節點
在此示例中,我們使用 Proxy 來切換兩個不同元素的屬性:因此,當我們為一個元素設定屬性時,該屬性會在另一個元素上被取消設定。
我們建立一個 view 物件,它是具有 selected 屬性的物件的代理。代理 handler 定義了 set() handler。
當我們為 view.selected 分配一個 HTML 元素時,該元素的 'aria-selected' 屬性被設定為 true。如果我們隨後為 view.selected 分配另一個元素,則該元素的 'aria-selected' 屬性被設定為 true,而前一個元素的 'aria-selected' 屬性則自動設定為 false。
const view = new Proxy(
{
selected: null,
},
{
set(obj, prop, newVal) {
const oldVal = obj[prop];
if (prop === "selected") {
if (oldVal) {
oldVal.setAttribute("aria-selected", "false");
}
if (newVal) {
newVal.setAttribute("aria-selected", "true");
}
}
// The default behavior to store the value
obj[prop] = newVal;
// Indicate success
return true;
},
},
);
const item1 = document.getElementById("item-1");
const item2 = document.getElementById("item-2");
// select item1:
view.selected = item1;
console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: true
// selecting item2 de-selects item1:
view.selected = item2;
console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: false
console.log(`item2: ${item2.getAttribute("aria-selected")}`);
// item2: true
值校正和一個額外屬性
products 代理物件會評估傳入的值,並在需要時將其轉換為陣列。該物件還支援一個名為 latestBrowser 的額外屬性,既可以作為 getter 也可以作為 setter。
const products = new Proxy(
{
browsers: ["Firefox", "Chrome"],
},
{
get(obj, prop) {
// An extra property
if (prop === "latestBrowser") {
return obj.browsers[obj.browsers.length - 1];
}
// The default behavior to return the value
return obj[prop];
},
set(obj, prop, value) {
// An extra property
if (prop === "latestBrowser") {
obj.browsers.push(value);
return true;
}
// Convert the value if it is not an array
if (typeof value === "string") {
value = [value];
}
// The default behavior to store the value
obj[prop] = value;
// Indicate success
return true;
},
},
);
console.log(products.browsers);
// ['Firefox', 'Chrome']
products.browsers = "Safari";
// pass a string (by mistake)
console.log(products.browsers);
// ['Safari'] <- no problem, the value is an array
products.latestBrowser = "Edge";
console.log(products.browsers);
// ['Safari', 'Edge']
console.log(products.latestBrowser);
// 'Edge'
規範
| 規範 |
|---|
| ECMAScript® 2026 語言規範 # sec-proxy-objects |
瀏覽器相容性
載入中…
另見
- Proxies are awesome Brendan Eich 在 JSConf (2014) 上的演講