代理

Proxy 对象使您能够为另一个对象创建代理,该代理可以拦截并重新定义该对象的基本操作。

描述

Proxy 对象允许您创建一个对象,该对象可以代替原始对象使用,但可以重新定义基本 Object 操作,如获取、设置和定义属性。代理对象通常用于记录属性访问、验证、格式化或清理输入等。

您可以使用两个参数创建 Proxy

  • target:您想要代理的原始对象
  • handler:一个对象,用于定义将拦截哪些操作以及如何重新定义拦截的操作。

例如,此代码为 target 对象创建了一个代理。

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

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

因为处理程序为空,所以此代理的行为与原始目标完全相同

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

要自定义代理,我们在处理程序对象上定义函数

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

const handler2 = {
  get(target, prop, receiver) {
    return "world";
  },
};

const proxy2 = new Proxy(target, handler2);

在这里,我们提供了 get() 处理程序的实现,该处理程序拦截尝试访问目标中的属性。

处理程序函数有时称为陷阱,大概是因为它们会捕获对目标对象的调用。上面 handler2 中的非常简单的陷阱重新定义了所有属性访问器

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

代理通常与 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

定义对应对象内部方法行为的函数。(这类似于操作系统中陷阱的概念。)

target

代理虚拟化的对象。它通常用作代理的后端存储。关于对象不可扩展性或不可配置属性的不变式(语义保持不变)针对目标进行验证。

invariants

在实现自定义操作时保持不变的语义。如果您的陷阱实现违反了处理程序的不变式,则会抛出TypeError

对象内部方法

对象是属性的集合。但是,该语言没有提供任何机制来直接操作存储在对象中的数据——而是,对象定义了一些内部方法,指定了如何与之交互。例如,当您读取 obj.x 时,您可能期望发生以下情况

  • x 属性沿着原型链向上搜索,直到找到它。
  • 如果 x 是数据属性,则返回属性描述符的 value 属性。
  • 如果 x 是访问器属性,则调用 getter,并返回 getter 的返回值。

此过程中在语言中没有任何特殊之处——这仅仅是因为普通对象默认情况下具有一个 [[Get]] 内部方法,该方法使用此行为定义。obj.x 属性访问语法只是在对象上调用 [[Get]] 方法,并且对象使用其自己的内部方法实现来确定要返回什么。

再举一个例子,数组与普通对象不同,因为它们有一个神奇的length 属性,当修改该属性时,会自动分配空插槽或从数组中删除元素。同样,添加数组元素会自动更改 length 属性。这是因为数组具有一个 [[DefineOwnProperty]] 内部方法,该方法知道在写入整数索引时更新 length,或者在写入 length 时更新数组内容。其内部方法与普通对象具有不同实现的此类对象称为奇异对象Proxy 使开发人员能够定义自己的奇异对象,并具有全部功能。

所有对象都具有以下内部方法

内部方法 对应陷阱
[[GetPrototypeOf]] getPrototypeOf()
[[SetPrototypeOf]] setPrototypeOf()
[[IsExtensible]] isExtensible()
[[PreventExtensions]] preventExtensions()
[[GetOwnProperty]] getOwnPropertyDescriptor()
[[DefineOwnProperty]] defineProperty()
[[HasProperty]] has()
[[Get]] get()
[[Set]] set()
[[Delete]] deleteProperty()
[[OwnPropertyKeys]] ownKeys()

函数对象还具有以下内部方法

内部方法 对应陷阱
[[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() 处理程序。

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 对象或任何具有内部槽的对象。有关更多信息,请参阅无私有属性转发

无私有属性转发

代理仍然是另一个具有不同标识的对象——它是代理,在包装对象和外部之间运行。因此,代理无法直接访问原始对象的私有属性

js
class Secret {
  #secret;
  constructor(secret) {
    this.#secret = secret;
  }
  get secret() {
    return this.#secret.replace(/\d+/, "[REDACTED]");
  }
}

const aSecret = new Secret("123456");
console.log(aSecret.secret); // [REDACTED]
// Looks like a no-op forwarding...
const proxy = new Proxy(aSecret, {});
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(aSecret, {
  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 aSecret = new Secret();
const proxy = new Proxy(aSecret, {
  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]]的内部槽,用于存储映射的键值对。因此,您无法简单地为映射创建转发代理。

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

您必须使用上面说明的“this恢复”代理来解决此问题。

验证

使用Proxy,您可以轻松验证传递给对象的值。此示例使用set()处理程序。

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属性的对象的代理。代理处理程序定义了set()处理程序。

当我们将 HTML 元素分配给view.selected时,该元素的'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'

完整的陷阱列表示例

现在,为了创建完整的示例traps列表,出于教学目的,我们将尝试代理化一个特别适合此类操作的非原生对象:由一个简单的 Cookie 框架创建的docCookies全局对象。

js
/*
  const docCookies = ... get the "docCookies" object here:
  https://reference.codeproject.com/dom/document/cookie/simple_document.cookie_framework
*/

const docCookies = new Proxy(docCookies, {
  get(target, key) {
    return target[key] ?? target.getItem(key) ?? undefined;
  },
  set(target, key, value) {
    if (key in target) {
      return false;
    }
    return target.setItem(key, value);
  },
  deleteProperty(target, key) {
    if (!(key in target)) {
      return false;
    }
    return target.removeItem(key);
  },
  ownKeys(target) {
    return target.keys();
  },
  has(target, key) {
    return key in target || target.hasItem(key);
  },
  defineProperty(target, key, descriptor) {
    if (descriptor && "value" in descriptor) {
      target.setItem(key, descriptor.value);
    }
    return target;
  },
  getOwnPropertyDescriptor(target, key) {
    const value = target.getItem(key);
    return value
      ? {
          value,
          writable: true,
          enumerable: true,
          configurable: false,
        }
      : undefined;
  },
});

/* Cookies test */

console.log((docCookies.myCookie1 = "First value"));
console.log(docCookies.getItem("myCookie1"));

docCookies.setItem("myCookie1", "Changed value");
console.log(docCookies.myCookie1);

规范

规范
ECMAScript 语言规范
# sec-proxy-objects

浏览器兼容性

BCD 表格仅在启用 JavaScript 的浏览器中加载。

另请参阅