Proxy

Baseline 已廣泛支援

此特性已非常成熟,可在多種裝置和瀏覽器版本上使用。自 ⁨2016 年 9 月⁩以來,它已在各大瀏覽器中可用。

Proxy 物件允許你為一個物件建立一個代理,以攔截並重新定義該物件的基本操作。

描述

Proxy 物件允許你建立一個可以替代原始物件使用的物件,但它可以重新定義物件的基本 Object 操作,如獲取、設定和定義屬性。Proxy 物件通常用於記錄屬性訪問、驗證、格式化或清理輸入等。

你建立 Proxy 時需要兩個引數

  • target:你想要代理的原始物件
  • handler:一個物件,它定義了哪些操作將被攔截以及如何重新定義被攔截的操作。

例如,此程式碼建立了一個 target 物件的代理。

js
const target = {
  message1: "hello",
  message2: "everyone",
};

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

因為 handler 是空的,所以這個代理的行為與原始 target 相同

js
console.log(proxy1.message1); // hello
console.log(proxy1.message2); // everyone

要自定義代理,我們在 handler 物件上定義函式

js
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 中的陷阱重新定義了所有屬性訪問器。

js
console.log(proxy2.message1); // world
console.log(proxy2.message2); // world

Proxy 物件經常與 Reflect 物件一起使用,該物件提供了一些與 Proxy 陷阱同名的方法。Reflect 方法提供了呼叫相應 物件內部方法 的反射語義。例如,如果我們不想重新定義物件的行為,我們可以呼叫 Reflect.get

js
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 方法呼叫被陷阱再次攔截,可能會發生無限遞迴。

術語

在討論代理的功能時,會使用以下術語。

handler

傳遞給 Proxy 建構函式的第二個引數物件。它包含了定義代理行為的陷阱。

trap

定義相應 物件內部方法 行為的函式。(這類似於作業系統中的陷阱概念。)

目標

Proxy 虛擬化的物件。它通常用作代理的儲存後端。關於物件不可擴充套件性或不可配置屬性的不變性(sematics that remain unchanged)會針對目標物件進行驗證。

invariants

在實現自定義操作時保持不變的語義。如果你的陷阱實現違反了 handler 的不變性,將會丟擲一個 TypeError

Object internal methods

Objects 是屬性的集合。然而,語言本身並不提供任何機制來直接操作儲存在物件中的資料——相反,物件定義了一些內部方法來指定如何與之互動。例如,當你讀取 obj.x 時,你可能期望發生以下情況:

  • 原型鏈 上向上搜尋 x 屬性,直到找到為止。
  • 如果 x 是資料屬性,則返回屬性描述符的 value 屬性。
  • 如果 x 是訪問器屬性,則呼叫 getter,並返回 getter 的返回值。

在這個過程中,語言本身並沒有什麼特別之處——這僅僅是因為普通物件預設具有一個具有此行為的 [[Get]] 內部方法。obj.x 屬性訪問語法只是呼叫物件上的 [[Get]] 方法,而物件使用自己的內部方法實現來確定返回什麼。

再舉個例子,陣列與普通物件不同,因為它們有一個特殊的 length 屬性,當修改該屬性時,它會自動分配空槽或從陣列中刪除元素。同樣,新增陣列元素會自動更改 length 屬性。這是因為陣列有一個 [[DefineOwnProperty]] 內部方法,它知道在寫入整數索引時更新 length,或者在寫入 length 時更新陣列內容。像這樣的內部方法實現與普通物件不同的物件被稱為exotic objectsProxy 使開發者能夠完全自定義此類物件。

所有物件都具有以下內部方法

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。

js
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 物件,我們的代理會將應用於該物件的所有操作轉發給它。

js
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

無私有欄位轉發

代理仍然是另一個具有不同身份的物件——它是包裝物件和外部世界之間操作的代理。因此,代理無法直接訪問原始物件的 私有元素

js
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

js
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 值重定向到原始物件。

js
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 建立一個轉發代理。

js
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy

你必須使用上面說明的“this 恢復”代理來解決這個問題。

驗證

使用 Proxy,你可以輕鬆地驗證傳遞給物件的數值。此示例使用 set() handler。

js
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

js
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。

js
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

瀏覽器相容性

另見