GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||
GeezerVison is showing developer.mozilla.org — Proxy - JavaScript | MDN 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() |
Function objects also have the following internal methods:
| Internal method | Corresponding trap |
|---|---|
[[Call]] |
apply() |
[[Construct]] |
construct() |
It's important to realize that all interactions with an object eventually boils down to the invocation of one of these internal methods, and that they are all customizable through proxies. This means almost no behavior (except certain critical invariants) is guaranteed in the language — everything is defined by the object itself. When you run delete obj.x, there's no guarantee that "x" in obj returns false afterwards — it depends on the object's implementations of [[Delete]] and [[HasProperty]]. A delete obj.x may log things to the console, modify some global state, or even define a new property instead of deleting the existing one, although these semantics should be avoided in your own code.
All internal methods are called by the language itself, and are not directly accessible in JavaScript code. The Reflect namespace offers methods that do little more than call the internal methods, besides some input normalization/validation. In each trap's page, we list several typical situations when the trap is invoked, but these internal methods are called in a lot of places. For example, array methods read and write to array through these internal methods, so methods like push() would also invoke get() and set() traps.
Most of the internal methods are straightforward in what they do. The only two that may be confusable are [[Set]] and [[DefineOwnProperty]]. For normal objects, the former invokes setters; the latter doesn't. (And [[Set]] calls [[DefineOwnProperty]] internally if there's no existing property or the property is a data property.) While you may know that the obj.x = 1 syntax uses [[Set]], and Object.defineProperty() uses [[DefineOwnProperty]], it's not immediately apparent what semantics other built-in methods and syntaxes use. For example, class fields use the [[DefineOwnProperty]] semantic, which is why setters defined in the superclass are not invoked when a field is declared on the derived class.
Proxy()Creates a new Proxy object.
Note:
There's no Proxy.prototype property, so Proxy instances do not have any special properties or methods.
Proxy.revocable()Creates a revocable Proxy object.
In this example, the number 37 gets returned as the default value when the property name is not in the object. It is using the 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
In this example, we are using a native JavaScript object to which our proxy will forward all operations that are applied to it.
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!)
Note that while this "no-op" works for plain JavaScript objects, it does not work for native objects, such as DOM elements, Map objects, or anything that has internal slots. See no private field forwarding for more information.
A proxy is still another object with a different identity — it's a proxy that operates between the wrapped object and the outside. As such, the proxy does not have direct access to the original object's private elements.
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
This is because when the proxy's get trap is invoked, the this value is the proxy instead of the original secret, so #secret is not accessible. To fix this, use the original secret as 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);
For methods, this means you have to redirect the method's this value to the original object as well:
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());
Some native JavaScript objects have properties called internal slots, which are not accessible from JavaScript code. For example, Map objects have an internal slot called [[MapData]], which stores the key-value pairs of the map. As such, you cannot trivially create a forwarding proxy for a map:
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy
You have to use the "this-recovering" proxy illustrated above to work around this.
With a Proxy, you can easily validate the passed value for an object. This example uses the 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
In this example we use Proxy to toggle an attribute of two different elements: so when we set the attribute on one element, the attribute is unset on the other one.
We create a view object which is a proxy for an object with a selected property. The proxy handler defines the set() handler.
When we assign an HTML element to view.selected, the element's 'aria-selected' attribute is set to true. If we then assign a different element to view.selected, this element's 'aria-selected' attribute is set to true and the previous element's 'aria-selected' attribute is automatically set to 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
The products proxy object evaluates the passed value and converts it to an array if needed. The object also supports an extra property called latestBrowser both as a getter and a 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'
| Specification |
|---|
| ECMAScript® 2027 Language Specification # sec-proxy-objects |