get
, set
)get
, set
)get
)set
)enumerate
trap?Proxies enable us to intercept and customize operations performed on objects (such as getting properties). They are a metaprogramming feature.
In the following example:
proxy
is an empty object.
handler
can intercept operations that are performed on proxy
, by implementing certain methods.
If the handler does not intercept an operation, it is forwarded to target
.
We are only intercepting one operation – get
(getting properties):
const logged = [];
const target = {size: 0};
const handler = {
get(target, propKey, receiver) {
logged.push('GET ' + propKey);
return 123;
}
};
const proxy = new Proxy(target, handler);
When we get the property proxy.size
, the handler intercepts that operation:
See the reference for the complete API for a list of operations that can be intercepted.
Before we can get into what Proxies are and why they are useful, we first need to understand what metaprogramming is.
In programming, there are levels:
Base and meta level can be different languages. In the following meta program, the metaprogramming language is JavaScript and the base programming language is Java.
Metaprogramming can take different forms. In the previous example, we have printed Java code to the console. Let’s use JavaScript as both metaprogramming language and base programming language. The classic example for this is the eval()
function, which lets us evaluate/compile JavaScript code on the fly. In the interaction below, we use it to evaluate the expression 5 + 2
.
Other JavaScript operations may not look like metaprogramming, but actually are, if we look closer:
// Base level
const obj = {
hello() {
console.log('Hello!');
},
};
// Meta level
for (const key of Object.keys(obj)) {
console.log(key);
}
The program is examining its own structure while running. This doesn’t look like metaprogramming, because the separation between programming constructs and data structures is fuzzy in JavaScript. All of the Object.*
methods can be considered metaprogramming functionality.
Reflective metaprogramming means that a program processes itself. Kiczales et al. [2] distinguish three kinds of reflective metaprogramming:
Let’s look at examples.
Example: introspection. Object.keys()
performs introspection (see previous example).
Example: self-modification. The following function moveProperty
moves a property from a source to a target. It performs self-modification via the bracket operator for property access, the assignment operator and the delete
operator. (In production code, we’d probably use property descriptors for this task.)
function moveProperty(source, propertyName, target) {
target[propertyName] = source[propertyName];
delete source[propertyName];
}
This is how moveProperty()
is used:
const obj1 = { color: 'blue' };
const obj2 = {};
moveProperty(obj1, 'color', obj2);
assert.deepEqual(
obj1, {});
assert.deepEqual(
obj2, { color: 'blue' });
ECMAScript 5 doesn’t support intercession; Proxies were created to fill that gap.
Proxies bring intercession to JavaScript. They work as follows. There are many operations that we can perform on an object obj
– for example:
prop
of an object obj
(obj.prop
)obj
has a property prop
('prop' in obj
)Proxies are special objects that allow us to customize some of these operations. A Proxy is created with two parameters:
handler
: For each operation, there is a corresponding handler method that – if present – performs that operation. Such a method intercepts the operation (on its way to the target) and is called a trap – a term borrowed from the domain of operating systems.target
: If the handler doesn’t intercept an operation, then it is performed on the target. That is, it acts as a fallback for the handler. In a way, the Proxy wraps the target.Note: The verb form of “intercession” is “to intercede”. Interceding is bidirectional in nature. Intercepting is unidirectional in nature.
In the following example, the handler intercepts the operations get
and 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);
If we get a property (line A) or use the in
operator (line B), the handler intercepts those operations:
assert.equal(proxy.age, 123); // (A)
assert.equal('hello' in proxy, true); // (B)
assert.deepEqual(
logged, [
'GET age',
'HAS hello',
]);
The handler doesn’t implement the trap set
(setting properties). Therefore, setting proxy.age
is forwarded to target
and leads to target.age
being set:
If the target is a function, two additional operations can be intercepted:
apply
: Making a function call. Triggered via:
proxy(···)
proxy.call(···)
proxy.apply(···)
construct
: Making a constructor call. Triggered via:
new proxy(···)
The reason for only enabling these traps for function targets is simple: Otherwise, we wouldn’t be able to forward the operations apply
and construct
.
If we want to intercept method calls via a Proxy, we are facing a challenge: There is no trap for method calls. Instead, a method call is viewed as a sequence of two operations:
get
to retrieve a functionapply
to call that functionTherefore, if we want to intercept method calls, we need to intercept two operations:
get
and return a function.The following code demonstrates how that is done.
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);
}
We are not using a Proxy for the second interception; we are simply wrapping the original method in a function.
Let’s use the following object to try out 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',
]);
Even the call this.multiply()
inside obj.squared()
is traced! That’s because this
keeps referring to the Proxy.
This is not the most efficient solution. One could, for example, cache methods. Furthermore, Proxies themselves have an impact on performance.
Proxies can be revoked (switched off):
After we call the function revoke
for the first time, any operation we apply to proxy
causes a TypeError
. Subsequent calls of revoke
have no further effect.
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$/
);
A Proxy proto
can become the prototype of an object obj
. Some operations that begin in obj
may continue in proto
. One such operation is 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'
The property weight
can’t be found in obj
, which is why the search continues in proto
and the trap get
is triggered there. There are more operations that affect prototypes; they are listed at the end of this chapter.
Operations whose traps the handler doesn’t implement are automatically forwarded to the target. Sometimes there is some task we want to perform in addition to forwarding the operation. For example, intercepting and logging all operations, without preventing them from reaching the target:
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.*
For each trap, we first log the name of the operation and then forward it by performing it manually. JavaScript has the module-like object Reflect
that helps with forwarding.
For each trap:
Reflect
has a method:
If we use Reflect
, the previous example looks as follows.
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
}
Now what each of the traps does is so similar that we can implement the handler via a 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);
};
},
});
For each trap, the Proxy asks for a handler method via the get
operation and we give it one. That is, all of the handler methods can be implemented via the single meta-method get
. It was one of the goals for the Proxy API to make this kind of virtualization simple.
Let’s use this Proxy-based handler:
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'
A Proxy object can be seen as intercepting operations performed on its target object – the Proxy wraps the target. The Proxy’s handler object is like an observer or listener for the Proxy. It specifies which operations should be intercepted by implementing corresponding methods (get
for reading a property, etc.). If the handler method for an operation is missing then that operation is not intercepted. It is simply forwarded to the target.
Therefore, if the handler is the empty object, the Proxy should transparently wrap the target. Alas, that doesn’t always work.
this
Before we dig deeper, let’s quickly review how wrapping a target affects this
:
const target = {
myMethod() {
return {
thisIsTarget: this === target,
thisIsProxy: this === proxy,
};
}
};
const handler = {};
const proxy = new Proxy(target, handler);
If we call target.myMethod()
directly, this
points to target
:
If we invoke that method via the Proxy, this
points to proxy
:
That is, if the Proxy forwards a method call to the target, this
is not changed. As a consequence, the Proxy continues to be in the loop if the target uses this
, e.g., to make a method call.
Normally, Proxies with empty handlers wrap targets transparently: we don’t notice that they are there and they don’t change the behavior of the targets.
If, however, a target associates information with this
via a mechanism that is not controlled by Proxies, we have a problem: things fail, because different information is associated depending on whether the target is wrapped or not.
For example, the following class Person
stores private information in the WeakMap _name
(more information on this technique is given in JavaScript for impatient programmers):
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
Instances of Person
can’t be wrapped transparently:
const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');
const proxy = new Proxy(jane, {});
assert.equal(proxy.name, undefined);
jane.name
is different from the wrapped proxy.name
. The following implementation does not have this problem:
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');
Instances of most built-in constructors also use a mechanism that is not intercepted by Proxies. They therefore can’t be wrapped transparently, either. We can see that if we use an instance of Date
:
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
assert.throws(
() => proxy.getFullYear(),
/^TypeError: this is not a Date object\.$/
);
The mechanism that is unaffected by Proxies is called internal slots. These slots are property-like storage associated with instances. The specification handles these slots as if they were properties with names in square brackets. For example, the following method is internal and can be invoked on all objects O
:
In contrast to properties, accessing internal slots is not done via normal “get” and “set” operations. If .getFullYear()
is invoked via a Proxy, it can’t find the internal slot it needs on this
and complains via a TypeError
.
For Date
methods, the language specification states:
Unless explicitly defined otherwise, the methods of the Date prototype object defined below are not generic and the
this
value passed to them must be an object that has a[[DateValue]]
internal slot that has been initialized to a time value.
As a work-around, we can change how the handler forwards method calls and selectively set this
to the target and not the 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);
The drawback of this approach is that none of the operations that the method performs on this
go through the Proxy.
In contrast to other built-ins, Arrays can be wrapped transparently:
const p = new Proxy(new Array(), {});
p.push('a');
assert.equal(p.length, 1);
p.length = 0;
assert.equal(p.length, 0);
The reason for Arrays being wrappable is that, even though property access is customized to make .length
work, Array methods don’t rely on internal slots – they are generic.
This section demonstrates what Proxies can be used for. That will give us the opportunity to see the API in action.
get
, set
)Let’s assume we have a function tracePropertyAccesses(obj, propKeys)
that logs whenever a property of obj
, whose key is in the Array propKeys
, is set or got. In the following code, we apply that function to an instance of the class 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']);
Getting and setting properties of the traced object p
has the following effects:
Intriguingly, tracing also works whenever Point
accesses the properties because this
now refers to the traced object, not to an instance of Point
:
tracePropertyAccesses()
without ProxiesWithout Proxies we’d implement tracePropertyAccesses()
as follows. We replace each property with a getter and a setter that traces accesses. The setters and getters use an extra object, propData
, to store the data of the properties. Note that we are destructively changing the original implementation, which means that we are metaprogramming.
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;
}
The parameter log
makes it easier to unit-test this function:
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()
with a ProxyProxies give us a simpler solution. We intercept property getting and setting and don’t have to change the implementation.
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
)When it comes to accessing properties, JavaScript is very forgiving. For example, if we try to read a property and misspell its name, we don’t get an exception – we get the result undefined
.
We can use Proxies to get an exception in such a case. This works as follows. We make the Proxy a prototype of an object. If a property isn’t found in the object, the get
trap of the Proxy is triggered:
get
operation to the target (the Proxy gets its prototype from the target).This is an implementation of this approach:
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);
Let’s use PropertyChecker
for an object:
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
as a classIf we turn PropertyChecker
into a constructor, we can use it for classes via 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/);
This is the prototype chain of 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);
If we are worried about accidentally creating properties, we have two options:
set
.obj
non-extensible via Object.preventExtensions(obj)
, which means that JavaScript doesn’t let us add new (own) properties to obj
.get
)Some Array methods let us refer to the last element via -1
, to the second-to-last element via -2
, etc. For example:
Alas, that doesn’t work when accessing elements via the bracket operator ([]
). We can, however, use Proxies to add that capability. The following function createArray()
creates Arrays that support negative indices. It does so by wrapping Proxies around Array instances. The Proxies intercept the get
operation that is triggered by the bracket operator.
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
)Data binding is about syncing data between objects. One popular use case are widgets based on the MVC (Model View Controler) pattern: With data binding, the view (the widget) stays up-to-date if we change the model (the data visualized by the widget).
To implement data binding, we have to observe and react to changes made to an object. The following code snippet is a sketch of how observing changes could work for Arrays.
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'
A Proxy can be used to create an object on which arbitrary methods can be invoked. In the following example, the function createWebService()
creates one such object, service
. Invoking a method on service
retrieves the contents of the web service resource with the same name. Retrieval is handled via a 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);
// ···
});
The following code is a quick and dirty implementation of createWebService
without Proxies. We need to know beforehand what methods will be invoked on service
. The parameter propKeys
provides us with that information; it holds an Array with method names.
function createWebService(baseUrl, propKeys) {
const service = {};
for (const propKey of propKeys) {
service[propKey] = () => {
return httpGet(baseUrl + '/' + propKey);
};
}
return service;
}
With Proxies, createWebService()
is simpler:
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
// Return the method to be called
return () => httpGet(baseUrl + '/' + propKey);
}
});
}
Both implementations use the following function to make HTTP GET requests (how it works is explained in 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();
});
}
Revocable references work as follows: A client is not allowed to access an important resource (an object) directly, only via a reference (an intermediate object, a wrapper around the resource). Normally, every operation applied to the reference is forwarded to the resource. After the client is done, the resource is protected by revoking the reference, by switching it off. Henceforth, applying operations to the reference throws exceptions and nothing is forwarded, anymore.
In the following example, we create a revocable reference for a resource. We then read one of the resource’s properties via the reference. That works, because the reference grants us access. Next, we revoke the reference. Now the reference doesn’t let us read the property, anymore.
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/
);
Proxies are ideally suited for implementing revocable references, because they can intercept and forward operations. This is a simple Proxy-based implementation of createRevocableReference
:
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;
},
};
}
The code can be simplified via the Proxy-as-handler technique from the previous section. This time, the handler basically is the Reflect
object. Thus, the get
trap normally returns the appropriate Reflect
method. If the reference has been revoked, a TypeError
is thrown, instead.
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;
},
};
}
However, we don’t have to implement revocable references ourselves because Proxies can be revoked. This time, the revoking happens in the Proxy, not in the handler. All the handler has to do is forward every operation to the target. As we have seen that happens automatically if the handler doesn’t implement any traps.
function createRevocableReference(target) {
const handler = {}; // forward everything
const { proxy, revoke } = Proxy.revocable(target, handler);
return { reference: proxy, revoke };
}
Membranes build on the idea of revocable references: Libraries for safely running untrusted code wrap a membrane around that code to isolate it and to keep the rest of the system safe. Objects pass the membrane in two directions:
In both cases, revocable references are wrapped around the objects. Objects returned by wrapped functions or methods are also wrapped. Additionally, if a wrapped wet object is passed back into a membrane, it is unwrapped.
Once the untrusted code is done, all of the revocable references are revoked. As a result, none of its code on the outside can be executed anymore and outside objects that it references, cease to work as well. The Caja Compiler is “a tool for making third party HTML, CSS and JavaScript safe to embed in your website”. It uses membranes to achieve this goal.
The browsers’ Document Object Model (DOM) is usually implemented as a mix of JavaScript and C++. Implementing it in pure JavaScript is useful for:
Alas, the standard DOM can do things that are not easily replicated in JavaScript. For example, most DOM collections are live views on the current state of the DOM that change dynamically whenever the DOM changes. As a result, pure JavaScript implementations of the DOM are not very efficient. One of the reasons for adding Proxies to JavaScript was to enable more efficient DOM implementations.
There are more use cases for Proxies. For example:
Remoting: Local placeholder objects forward method invocations to remote objects. This use case is similar to the web service example.
Data access objects for databases: Reading and writing to the object reads and writes to the database. This use case is similar to the web service example.
Profiling: Intercept method invocations to track how much time is spent in each method. This use case is similar to the tracing example.
Immer (by Michel Weststrate) helps with non-destructively updating data. The changes that should be applied are specified by invoking methods, setting properties, setting Array elements, etc. of a (potentially nested) draft state. Draft states are implemented via Proxies.
MobX lets you observe changes to data structures such as objects, Arrays and class instances. That is implemented via Proxies.
Alpine.js (by Caleb Porzio) is a frontend library that implements data binding via Proxies.
on-change (by Sindre Sorhus) observes changes to an object (via Proxies) and reports them.
Env utility (by Nicholas C. Zakas) lets you access environment variables via properties and throws exceptions if they don’t exist. That is implemented via Proxies.
LDflex (by Ruben Verborgh and Ruben Taelman) provides a query language for Linked Data (think Semantic Web). The fluid query API is implemented via Proxies.
In this section, we go deeper into how Proxies work and why they work that way.
Firefox used to support a limited from of interceding metaprogramming for a while: If an object O
had a method named __noSuchMethod__
, it was notified whenever a method was invoked on O
that didn’t exist. The following code demonstrates how that worked:
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');
Thus, __noSuchMethod__
works similarly to a Proxy trap. In contrast to Proxies, the trap is an own or inherited method of the object whose operations we want to intercept. The problem with that approach is that base level (normal methods) and meta level (__noSuchMethod__
) are mixed. Base-level code may accidentally invoke or see a meta level method and there is the possibility of accidentally defining a meta level method.
Even in standard ECMAScript, base level and meta level are sometimes mixed. For example, the following metaprogramming mechanisms can fail, because they exist at the base level:
obj.hasOwnProperty(propKey)
: This call can fail if a property in the prototype chain overrides the built-in implementation. For example, in the following code, obj
causes a failure:
const obj = { hasOwnProperty: null };
assert.throws(
() => obj.hasOwnProperty('width'),
/^TypeError: obj.hasOwnProperty is not a function/
);
These are safe ways of invoking .hasOwnProperty()
:
func.call(···)
, func.apply(···)
: For both methods, problem and solution are the same as with .hasOwnProperty()
.
obj.__proto__
: In plain objects, __proto__
is a special property that lets us get and set the prototype of the receiver. Hence, when we use plain objects as dictionaries, we must avoid __proto__
as a property key.
By now, it should be obvious that making (base level) property keys special is problematic. Therefore, Proxies are stratified: Base level (the Proxy object) and meta level (the handler object) are separate.
Proxies are used in two roles:
As wrappers, they wrap their targets, they control access to them. Examples of wrappers are: revocable resources and tracing via Proxies.
As virtual objects, they are simply objects with special behavior and their targets don’t matter. An example is a Proxy that forwards method calls to a remote object.
An earlier design of the Proxy API conceived Proxies as purely virtual objects. However, it turned out that even in that role, a target was useful, to enforce invariants (which are explained later) and as a fallback for traps that the handler doesn’t implement.
Proxies are shielded in two ways:
Both principles give Proxies considerable power for impersonating other objects. One reason for enforcing invariants (as explained later) is to keep that power in check.
If we do need a way to tell Proxies apart from non-Proxies, we have to implement it ourselves. The following code is a module lib.mjs
that exports two functions: one of them creates Proxies, the other one determines whether an object is one of those Proxies.
// 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);
}
This module uses the data structure WeakSet
for keeping track of Proxies. WeakSet
is ideally suited for this purpose, because it doesn’t prevent its elements from being garbage-collected.
The next example shows how lib.mjs
can be used.
// main.mjs
import { createProxy, isProxy } from './lib.mjs';
const proxy = createProxy({});
assert.equal(isProxy(proxy), true);
assert.equal(isProxy({}), false);
In this section, we examine how JavaScript is structured internally and how the set of Proxy traps was chosen.
In the context of programming languages and API design, a protocol is a set of interfaces plus rules for using them. The ECMAScript specification describes how to execute JavaScript code. It includes a protocol for handling objects. This protocol operates at a meta level and is sometimes called the meta object protocol (MOP). The JavaScript MOP consists of own internal methods that all objects have. “Internal” means that they exist only in the specification (JavaScript engines may or may not have them) and are not accessible from JavaScript. The names of internal methods are written in double square brackets.
The internal method for getting properties is called .[[Get]]()
. If we use double underscores instead of double brackets, this method would roughly be implemented as follows in 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, []);
}
The MOP methods called in this code are:
[[GetOwnProperty]]
(trap getOwnPropertyDescriptor
)[[GetPrototypeOf]]
(trap getPrototypeOf
)[[Get]]
(trap get
)[[Call]]
(trap apply
)In line A we can see why Proxies in a prototype chain find out about get
if a property isn’t found in an “earlier” object: If there is no own property whose key is propKey
, the search continues in the prototype parent
of this
.
Fundamental versus derived operations. We can see that .[[Get]]()
calls other MOP operations. Operations that do that are called derived. Operations that don’t depend on other operations are called fundamental.
The meta object protocol of Proxies is different from that of normal objects. For normal objects, derived operations call other operations. For Proxies, each operation (regardless of whether it is fundamental or derived) is either intercepted by a handler method or forwarded to the target.
Which operations should be interceptable via Proxies?
The upside of doing the latter is that it increases performance and is more convenient. For example, if there weren’t a trap for get
, we’d have to implement its functionality via getOwnPropertyDescriptor
.
A downside of including derived traps is that that can lead to Proxies behaving inconsistently. For example, get
may return a value that is different from the value in the descriptor returned by getOwnPropertyDescriptor
.
Interception by Proxies is selective: we can’t intercept every language operation. Why were some operations excluded? Let’s look at two reasons.
First, stable operations are not well suited for interception. An operation is stable if it always produces the same results for the same arguments. If a Proxy can trap a stable operation, it can become unstable and thus unreliable. Strict equality (===
) is one such stable operation. It can’t be trapped and its result is computed by treating the Proxy itself as just another object. Another way of maintaining stability is by applying an operation to the target instead of the Proxy. As explained later, when we look at how invariants are enfored for Proxies, this happens when Object.getPrototypeOf()
is applied to a Proxy whose target is non-extensible.
A second reason for not making more operations interceptable is that interception means executing custom code in situations where that normally isn’t possible. The more this interleaving of code happens, the harder it is to understand and debug a program. It also affects performance negatively.
get
versus invoke
If we want to create virtual methods via Proxies, we have to return functions from a get
trap. That raises the question: why not introduce an extra trap for method invocations (e.g. invoke
)? That would enable us to distinguish between:
obj.prop
(trap get
)obj.prop()
(trap invoke
)There are two reasons for not doing so.
First, not all implementations distinguish between get
and invoke
. For example, Apple’s JavaScriptCore doesn’t.
Second, extracting a method and invoking it later via .call()
or .apply()
should have the same effect as invoking the method via dispatch. In other words, the following two variants should work equivalently. If there were an extra trap invoke
, then that equivalence would be harder to maintain.
// 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
Some things can only be done if we are able to distinguish between get
and invoke
. Those things are therefore impossible with the current Proxy API. Two examples are: auto-binding and intercepting missing methods. Let’s examine how one would implement them if Proxies supported invoke
.
Auto-binding. By making a Proxy the prototype of an object obj
, we can automatically bind methods:
m
via obj.m
returns a function whose this
is bound to obj
.obj.m()
performs a method call.Auto-binding helps with using methods as callbacks. For example, variant 2 from the previous example becomes simpler:
Intercepting missing methods. invoke
lets a Proxy emulate the previously mentioned __noSuchMethod__
mechanism. The Proxy would again become the prototype of an object obj
. It would react differently depending on how an unknown property prop
is accessed:
obj.prop
, no interception happens and undefined
is returned.obj.prop()
then the Proxy intercepts and, e.g., notifies a callback.Before we look at what invariants are and how they are enforced for Proxies, let’s review how objects can be protected via non-extensibility and non-configurability.
There are two ways of protecting objects:
Non-extensibility protects objects: If an object is non-extensible, we can’t add properties and we can’t change its prototype.
Non-configurability protects properties (or rather, their attributes):
writable
controls whether a property’s value can be changed.configurable
controls whether a property’s attributes can be changed.For more information on this topic, see §10 “Protecting objects from being changed”.
Traditionally, non-extensibility and non-configurability are:
These and other characteristics that remain unchanged in the face of language operations are called invariants. It is easy to violate invariants via Proxies because they are not intrinsically bound by non-extensibility etc. The Proxy API prevents that from happening by checking the target object and the results of handler methods.
The next two subsections describe four invariants. An exhaustive list of invariants is given at the end of this chapter.
The following two invariants involve non-extensibility and non-configurability. These are enforced by using the target object for bookkeeping: results returned by handler methods have to be mostly in sync with the target object.
Object.preventExtensions(obj)
returns true
then all future calls must return false
and obj
must now be non-extensible.
TypeError
if the handler returns true
, but the target object is not extensible.Object.isExtensible(obj)
must always return false
.
TypeError
if the result returned by the handler is not the same (after coercion) as Object.isExtensible(target)
.The following two invariants are enforced by checking return values:
Object.isExtensible(obj)
must return a boolean.
Object.getOwnPropertyDescriptor(obj, ···)
must return an object or undefined
.
TypeError
if the handler doesn’t return an appropriate value.Enforcing invariants has the following benefits:
The next two sections give examples of invariants being enforced.
In response to the getPrototypeOf
trap, the Proxy must return the target’s prototype if the target is non-extensible.
To demonstrate this invariant, let’s create a handler that returns a prototype that is different from the target’s prototype:
Faking the prototype works if the target is extensible:
const extensibleTarget = {};
const extProxy = new Proxy(extensibleTarget, handler);
assert.equal(
Object.getPrototypeOf(extProxy), fakeProto);
We do, however, get an error if we fake the prototype for a non-extensible object.
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",
});
If the target has a non-writable non-configurable property, then the handler must return that property’s value in response to a get
trap. To demonstrate this invariant, let’s create a handler that always returns the same value for properties.
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);
Property target.manufacturer
is not both non-writable and non-configurable, which means that the handler is allowed to pretend that it has a different value:
However, property target.model
is both non-writable and non-configurable. Therefore, we can’t fake its value:
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
trap?ECMAScript 6 originally had a trap enumerate
that was triggered by for-in
loops. But it was recently removed, to simplify Proxies. Reflect.enumerate()
was removed, as well. (Source: TC39 notes)
This section is a quick reference for the Proxy API:
Proxy
Reflect
The reference uses the following custom type:
There are two ways to create Proxies:
const proxy = new Proxy(target, handler)
Creates a new Proxy object with the given target and the given handler.
const {proxy, revoke} = Proxy.revocable(target, handler)
Creates a Proxy that can be revoked via the function revoke
. revoke
can be called multiple times, but only the first call has an effect and switches proxy
off. Afterwards, any operation performed on proxy
leads to a TypeError
being thrown.
This subsection explains what traps can be implemented by handlers and what operations trigger them. Several traps return boolean values. For the traps has
and isExtensible
, the boolean is the result of the operation. For all other traps, the boolean indicates whether the operation succeeded or not.
Traps for all objects:
defineProperty(target, propKey, propDesc): boolean
Object.defineProperty(proxy, propKey, propDesc)
deleteProperty(target, propKey): boolean
delete proxy[propKey]
delete proxy.someProp
get(target, propKey, receiver): any
receiver[propKey]
receiver.someProp
getOwnPropertyDescriptor(target, propKey): undefined|PropDesc
Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target): null|object
Object.getPrototypeOf(proxy)
has(target, propKey): boolean
propKey in proxy
isExtensible(target): boolean
Object.isExtensible(proxy)
ownKeys(target): Array<PropertyKey>
Object.getOwnPropertyPropertyNames(proxy)
(only uses string keys)Object.getOwnPropertyPropertySymbols(proxy)
(only uses symbol keys)Object.keys(proxy)
(only uses enumerable string keys; enumerability is checked via Object.getOwnPropertyDescriptor
)preventExtensions(target): boolean
Object.preventExtensions(proxy)
set(target, propKey, value, receiver): boolean
receiver[propKey] = value
receiver.someProp = value
setPrototypeOf(target, proto): boolean
Object.setPrototypeOf(proxy, proto)
Traps for functions (available if target is a function):
apply(target, thisArgument, argumentsList): any
proxy.apply(thisArgument, argumentsList)
proxy.call(thisArgument, ...argumentsList)
proxy(...argumentsList)
construct(target, argumentsList, newTarget): object
new proxy(..argumentsList)
The following operations are fundamental, they don’t use other operations to do their work: apply
, defineProperty
, deleteProperty
, getOwnPropertyDescriptor
, getPrototypeOf
, isExtensible
, ownKeys
, preventExtensions
, setPrototypeOf
All other operations are derived, they can be implemented via fundamental operations. For example, get
can be implemented by iterating over the prototype chain via getPrototypeOf
and calling getOwnPropertyDescriptor
for each chain member until either an own property is found or the chain ends.
Invariants are safety constraints for handlers. This subsection documents what invariants are enforced by the Proxy API and how. Whenever we read “the handler must do X” below, it means that a TypeError
is thrown if it doesn’t. Some invariants restrict return values, others restrict parameters. The correctness of a trap’s return value is ensured in two ways:
TypeError
.This is the complete list of invariants that are enforced:
apply(target, thisArgument, argumentsList): any
construct(target, argumentsList, newTarget): object
null
or any other primitive value).defineProperty(target, propKey, propDesc): boolean
propDesc
sets the attribute configurable
to false
, then the target must have a non-configurable own property whose key is propKey
.propDesc
sets both attributes configurable
and writable
to false
, then the target must have an own property with the key is propKey
that is non-configurable and non-writable.propKey
, then propDesc
must be compatible with that property: If we redefine the target property with the descriptor, no exception must be thrown.deleteProperty(target, propKey): boolean
propKey
.propKey
.get(target, propKey, receiver): any
propKey
, then the handler must return that property’s value.undefined
.getOwnPropertyDescriptor(target, propKey): undefined|PropDesc
undefined
or an object.getPrototypeOf(target): null|object
null
or an object.has(target, propKey): boolean
isExtensible(target): boolean
target.isExtensible()
.ownKeys(target): Array<PropertyKey>
preventExtensions(target): boolean
target.isExtensible()
is false
.set(target, propKey, value, receiver): boolean
propKey
. In that case, value
must be the value of that property or a TypeError
is thrown.setPrototypeOf(target, proto): boolean
proto
must be the same as the prototype of the target. Otherwise, a TypeError
is thrown.Invariants in the ECMAScript specification
In the spec, the invariants are listed in section “Proxy Object Internal Methods and Internal Slots”.
The following operations of normal objects perform operations on objects in the prototype chain. Therefore, if one of the objects in that chain is a Proxy, its traps are triggered. The specification implements the operations as internal own methods (that are not visible to JavaScript code). But in this section, we pretend that they are normal methods that have the same names as the traps. The parameter target
becomes the receiver of the method call.
target.get(propertyKey, receiver)
target
has no own property with the given key, get
is invoked on the prototype of target
.target.has(propertyKey)
get
, has
is invoked on the prototype of target
if target
has no own property with the given key.target.set(propertyKey, value, receiver)
get
, set
is invoked on the prototype of target
if target
has no own property with the given key.All other operations only affect own properties, they have no effect on the prototype chain.
Internal operations in the ECMAScript specification
In the spec, these (and other) operations are described in section “Ordinary Object Internal Methods and Internal Slots”.
The global object Reflect
implements all interceptable operations of the JavaScript meta object protocol as methods. The names of those methods are the same as those of the handler methods, which, as we have seen, helps with forwarding operations from the handler to the target.
Reflect.apply(target, thisArgument, argumentsList): any
Similar to Function.prototype.apply()
.
Reflect.construct(target, argumentsList, newTarget=target): object
The new
operator as a function. target
is the constructor to invoke, the optional parameter newTarget
points to the constructor that started the current chain of constructor calls.
Reflect.defineProperty(target, propertyKey, propDesc): boolean
Similar to Object.defineProperty()
.
Reflect.deleteProperty(target, propertyKey): boolean
The delete
operator as a function. It works slightly differently, though: It returns true
if it successfully deleted the property or if the property never existed. It returns false
if the property could not be deleted and still exists. The only way to protect properties from deletion is by making them non-configurable. In sloppy mode, the delete
operator returns the same results. But in strict mode, it throws a TypeError
instead of returning false
.
Reflect.get(target, propertyKey, receiver=target): any
A function that gets properties. The optional parameter receiver
points to the object where the getting started. It is needed when get
reaches a getter later in the prototype chain. Then it provides the value for this
.
Reflect.getOwnPropertyDescriptor(target, propertyKey): undefined|PropDesc
Same as Object.getOwnPropertyDescriptor()
.
Reflect.getPrototypeOf(target): null|object
Same as Object.getPrototypeOf()
.
Reflect.has(target, propertyKey): boolean
The in
operator as a function.
Reflect.isExtensible(target): boolean
Same as Object.isExtensible()
.
Reflect.ownKeys(target): Array<PropertyKey>
Returns all own property keys in an Array: the string keys and symbol keys of all own enumerable and non-enumerable properties.
Reflect.preventExtensions(target): boolean
Similar to Object.preventExtensions()
.
Reflect.set(target, propertyKey, value, receiver=target): boolean
A function that sets properties.
Reflect.setPrototypeOf(target, proto): boolean
The new standard way of setting the prototype of an object. The current non-standard way, that works in most engines, is to set the special property __proto__
.
Several methods have boolean results. For .has()
and .isExtensible()
, they are the results of the operation. For the remaining methods, they indicate whether the operation succeeded.
Reflect
besides forwardingApart from forwarding operations, why is Reflect
useful [4]?
Different return values: Reflect
duplicates the following methods of Object
, but its methods return booleans indicating whether the operation succeeded (where the Object
methods return the object that was modified).
Object.defineProperty(obj, propKey, propDesc): object
Object.preventExtensions(obj): object
Object.setPrototypeOf(obj, proto): object
Operators as functions: The following Reflect
methods implement functionality that is otherwise only available via operators:
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
Shorter version of apply()
: If we want to be completely safe about invoking the method apply()
on a function, we can’t do so via dynamic dispatch, because the function may have an own property with the key 'apply'
:
func.apply(thisArg, argArray) // not safe
Function.prototype.apply.call(func, thisArg, argArray) // safe
Using Reflect.apply()
is shorter than the safe version:
No exceptions when deleting properties: the delete
operator throws in strict mode if we try to delete a non-configurable own property. Reflect.deleteProperty()
returns false
in that case.
Object.*
versus Reflect.*
Going forward, Object
will host operations that are of interest to normal applications, while Reflect
will host operations that are more low-level.
This concludes our in-depth look at the Proxy API. One thing to be aware of is that Proxies slow down code. That may matter if performance is critical.
On the other hand, performance is often not crucial and it is nice to have the metaprogramming power that Proxies give us.
Acknowledgements:
Allen Wirfs-Brock pointed out the pitfall explained in §18.3.7 “Pitfall: not all objects can be wrapped transparently by Proxies”.
The idea for §18.4.3 “Negative Array indices (get
)” comes from a blog post by Hemanth.HM.
André Jaenisch contributed to the list of libraries that use Proxies.
[1] “On the design of the ECMAScript Reflection API” by Tom Van Cutsem and Mark Miller. Technical report, 2012. [Important source of this chapter.]
[2] “The Art of the Metaobject Protocol” by Gregor Kiczales, Jim des Rivieres and Daniel G. Bobrow. Book, 1991.
[3] “Putting Metaclasses to Work: A New Dimension in Object-Oriented Programming” by Ira R. Forman and Scott H. Danforth. Book, 1999.
[4] “Harmony-reflect: Why should I use this library?” by Tom Van Cutsem. [Explains why Reflect
is useful.]