This blog post is outdated. Please read chapter “Metaprogramming with proxies” in “Exploring ES6”.
This blog post explains the ECMAScript 6 (ES6) feature proxies. Proxies enable you to intercept and customize operations performed on objects (such as getting properties). They are a meta programming feature.
The code in this post occasionally uses other ES6 features. Consult “Using ECMAScript 6 today” for an overview of all of ES6.
Before we can get into what proxies are and why they are useful, we first need to understand what meta programming is.
In programming, there are levels:
Base and meta level can be diffent languages. In the following meta program, the meta programming language is JavaScript and the base programming language is Java.
let str = 'Hello' + '!'.repeat(3);
console.log('System.out.println("'+str+'")');
Meta programming can take different forms. In the previous example, we have printed Java code to the console. Let’s use JavaScript as both meta programming language and base programming language. The classic example for this is the eval()
function, which lets you evaluate/compile JavaScript code on the fly. There are very few actual use cases for eval()
. In the interaction below, we use it to evaluate the expression 5 + 2
.
> eval('5 + 2')
7
Other JavaScript operations may not look like meta programming, but actually are, if you look closer:
// Base level
let obj = {
hello() {
console.log('Hello!');
}
};
// Meta level
for (let key of Object.keys(obj)) {
console.log(key);
}
The program is examining its own structure while running. This doesn’t look like meta programming, because the separation between programming constructs and data structures is fuzzy in JavaScript. All of the Object.*
methods can be considered meta programming functionality.
Reflective meta programming means that a program processes itself. Kiczales et al. [1] distinguish three kinds of reflective meta programming:
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, you’d probably use property descriptors for this task.)
function moveProperty(source, propertyName, target) {
target[propertyName] = source[propertyName];
delete source[propertyName];
}
Using moveProperty()
:
> let obj1 = { prop: 'abc' };
> let obj2 = {};
> moveProperty(obj1, 'prop', obj2);
> obj1
{}
> obj2
{ prop: 'abc' }
JavaScript doesn’t currently support intercession, proxies were created to fill that gap.
ECMAScript 6 proxies bring intercession to JavaScript. They work as follows. There are many operations that you can perform on an object obj
. For example:
prop
(via obj.prop
)Object.keys(obj)
)Proxies are special objects that allow you to provide custom implementations for 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.In the following example, the handler intercepts the operation get
(getting properties).
let target = {};
let handler = {
get(target, propKey, receiver) {
console.log('get ' + propKey);
return 123;
}
};
let proxy = new Proxy(target, handler);
When we get property foo
, the handler intercepts that operation:
> proxy.foo
get foo
123
The handler doesn’t implement the trap set
(setting properties). Therefore, setting proxy.bar
is forwarded to target
and leads to target.bar
being set.
> proxy.bar = 'abc';
> target.bar
'abc'
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: You wouldn’t be able to forward the operations apply
and construct
, otherwise.
ECMAScript 6 lets you create proxies that can be revoked (switched off):
let {proxy, revoke} = Proxy.revocable(target, handler);
On the left hand side of the assignment operator (=
), we are using destructuring to access the properties proxy
and revoke
of the object returned by Proxy.revocable()
.
After you call the function revoke
for the first time, any operation you apply to proxy
causes a TypeError
. Subsequent calls of revoke
have no further effect.
let target = {}; // Start with an empty object
let handler = {}; // Don’t intercept anything
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
console.log(proxy.foo); // 123
revoke();
console.log(proxy.foo); // TypeError: 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
.
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.bla; // Output: GET bla
The property bla
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 post.
Operations whose traps the handler doesn’t implement are automatically forwarded to the target. Sometimes there is some task you want to perform in addition to forwarding the operation. For example, a handler that intercepts all operations and logs them, but doesn’t prevent them from reaching the target:
let 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
}
For each trap, we first log the name of the operation and then forward it by performing it manually. ECMAScript 6 has the module-like object Reflect
that helps with forwarding: for each trap
handler.trap(target, arg_1, ···, arg_n)
Reflect
has a method
Reflect.trap(target, arg_1, ···, arg_n)
If we use Reflect
, the previous example looks as follows.
let 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:
let handler = new Proxy({}, {
get(target, trapName, receiver) {
// Return the handler method named trapName
return function (...args) {
// Slice away target object in args[0]
console.log(trapName.toUpperCase()+' '+args.slice(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:
> let target = {};
> let proxy = new Proxy(target, handler);
> proxy.foo = 123;
SET foo,123,[object Object]
> proxy.foo
GET foo,[object Object]
123
The following interaction confirms that the set
operation was correctly forwarded to the target:
> target.foo
123
This section demonstrates what proxies can be used for. That will also give you the opportunity to see the API in action.
The browser 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 easy to replicate 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 help write more efficient DOM implementations.
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 an ECMAScript 6 promise.
let service = createWebService('http://example.com/data');
// Read JSON data in http://example.com/data/employees
service.employees().then(json => {
let employees = JSON.parse(json);
···
});
The following code is a quick and dirty implementation of createWebService
in ECMAScript 5. Because we don’t have 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) {
let service = {};
propKeys.forEach(function (propKey) {
Object.defineProperty(service, propKey, {
get: function () {
return httpGet(baseUrl+'/'+propKey);
}
});
});
return service;
}
The ECMAScript 6 implementation of createWebService
can use proxies and is simpler:
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return httpGet(baseUrl+'/'+propKey);
}
});
}
Both implementations use the following function to make HTTP GET requests (how it works is explained in the 2ality blog post on promises).
function httpGet(url) {
return new Promise(
(resolve, reject) => {
let request = new XMLHttpRequest();
Object.assign(request, {
onreadystatechange() {
if (this.status === 200) {
// Success
resolve(this.response);
} else {
// Something went wrong (404 etc.)
reject(new Error(this.statusText));
}
},
onerror() {
reject(new Error(
'XMLHttpRequest Error: '+this.statusText));
}
});
request.open('GET', url);
request.send();
});
}
The example in this section is inspired by Brendan Eich’s talk “Proxies are Awesome”: We want to trace when a given set of properties is read or changed. To demonstrate how that works, let’s create a class for points and trace accesses to the properties of an instance.
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`
let p = new Point(5, 7);
p = tracePropAccess(p, ['x', 'y']);
Getting and setting properties of p
now has the following effects:
> p.x
GET x
5
> p.x = 21
SET x=21
21
Intriguingly, tracing also works whenever Point
accesses the properties, because this
now refers to the proxy, not to an instance of Point
.
> p.toString()
GET x
GET y
'Point(21,7)'
In ECMAScript 5, you’d implement tracePropAccess()
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 meta programming.
function tracePropAccess(obj, propKeys) {
// Store the property data here
let 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 () {
console.log('GET '+propKey);
return propData[propKey];
},
set: function (value) {
console.log('SET '+propKey+'='+value);
propData[propKey] = value;
},
});
});
return obj;
}
In ECMAScript 6, we can use a simpler, proxy-based solution. We intercept property getting and setting and don’t have to change the implementation.
function tracePropAccess(obj, propKeys) {
let propKeySet = new Set(...propKeys);
return new Proxy(obj, {
get(target, propKey, receiver) {
if (propKeySet.has(propKey)) {
console.log('GET '+propKey);
}
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
if (propKeySet.has(propKey)) {
console.log('SET '+propKey+'='+value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}
When it comes to accessing properties, JavaScript is very forgiving. For example, if you try to read a property and misspell its name, you don’t get an exception, you get the result undefined
. You 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. If the property doesn’t even exist in the prototype chain after the proxy, it really is missing and we throw an exception. Otherwise, we return the value of the inherited property. We do so by forwarding the get
operation to the target, whose prototype is the prototype of the proxy.
let PropertyChecker = new Proxy({}, {
get(target, propKey, receiver) {
if (!(propKey in target)) {
throw new ReferenceError('Unknown property: '+propKey);
}
return Reflect.get(target, propKey, receiver);
}
});
Let’s use PropertyChecker
for an object that we create:
> let obj = { __proto__: PropertyChecker, foo: 123 };
> obj.foo // own
123
> obj.fo
ReferenceError: Unknown property: fo
> obj.toString() // inherited
'[object Object]'
If we turn PropertyChecker
into a constructor, we can use it for ECMAScript 6 classes via extends
:
function PropertyChecker() { }
PropertyChecker.prototype = new Proxy(···);
class Point extends PropertyChecker {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
let p = new Point(5, 7);
console.log(p.x); // 5
console.log(p.z); // ReferenceError
If you are worried about accidentally creating properties, you have two options: You can either create a proxy that traps set
. Or you can make an object obj
non-extensible via Object.preventExtensions(obj)
, which means that JavaScript doesn’t let you add new (own) properties to obj
.
Some array methods let you refer to the last element via -1
, to the second-to-last element via -2
, etc. For example:
> ['a', 'b', 'c'].slice(-1)
[ 'c' ]
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) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
// Sloppy way of checking for negative indices
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
// Wrap a proxy around an array
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
console.log(arr[-1]); // c
Acknowledgement: The idea for this example comes from a blog post by hemanth.hm.
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 you change the model (the data visualized by the widget).
To implement data binding, you have to observe and react to changes made to an object. In the following code snippet, I sketch how that could work for an array.
let array = [];
let observedArray = new Proxy(array, {
set(target, propertyKey, value, receiver) {
console.log(propertyKey+'='+value);
target[propertyKey] = value;
}
});
observedArray.push('a');
Output:
0=a
length=1
Data binding is a complex topic. Given its popularity and concerns over proxies not being performant enough, a dedicated mechanism has been created for data binding: Object.observe()
. It will probably be part of ECMAScript 7 and is already supported by Chrome.
Consult Addy Osmani’s article “Data-binding Revolutions with Object.observe()” for more information on Object.observe()
.
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.
let resource = { x: 11, y: 8 };
let {reference, revoke} = createRevocableReference(resource);
// Access granted
console.log(reference.x); // 11
revoke();
// Access denied
console.log(reference.x); // TypeError: 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('Revoked');
}
return Reflect.get(target, propKey, receiver);
},
has(target, propKey) {
if (!enabled) {
throw new TypeError('Revoked');
}
return Reflect.has(target, propKey);
},
···
}),
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;
let handler = new Proxy({}, {
get(dummyTarget, trapName, receiver) {
if (!enabled) {
throw new TypeError('Revoked');
}
return Reflect[trapName];
}
});
return {
reference: new Proxy(target, handler),
revoke() {
enabled = false;
},
};
}
However, you don’t have to implement revocable references yourself, because ECMAScript 6 lets you create proxies that 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) {
let handler = {}; // forward everything
let { proxy, revoke } = Proxy.revocable(target, handler);
return { reference: proxy, revoke };
}
Membranes build on the idea of revocable references: Environments that are designed to run untrusted code wrap a membrane around that code to isolate it and 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.
Once the untrusted code is done, all of those references are revoked. As a result, none of its code on the outside can be executed anymore and outside objects that it has 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 task.
There are more use cases for proxies. For example:
In this section, we go deeper into how proxies work and why they work that way.
Firefox has allowed you to do some interceptive meta programming for a while: If you define a method whose name is __noSuchMethod__
, it is notified whenever a method is called that doesn’t exist. The following is an example of using __noSuchMethod__
.
let obj = {
__noSuchMethod__: function (name, args) {
console.log(name+': '+args);
}
};
// Neither of the following two methods exist,
// but we can make it look like they do
obj.foo(1); // Output: foo: 1
obj.bar(1, 2); // Output: bar: 1,2
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 and meta level 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 5, base level and meta level are sometimes mixed. For example, the following meta programming 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, it fails if obj
is { hasOwnProperty: null }
. Safe ways to call this method are Object.prototype.hasOwnProperty.call(obj, propKey)
and its abbreviated version {}.hasOwnProperty.call(obj, propKey)
.func.call(···)
, func.apply(···)
: For these two methods, problem and solution are the same as with hasOwnProperty
.obj.__proto__
: In most JavaScript engines, __proto__
is a special property that lets you get and set the prototype of obj
. Hence, when you use objects as dictionaries, you must be careful to 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 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 is 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 you do need a way to tell proxies apart from non-proxies, you have to implement it yourself. The following code is a module lib.js
that exports two functions: one of them creates proxies, the other one determines whether an object is one of those proxies.
// lib.js
let proxies = new WeakSet();
export function createProxy(obj) {
let handler = {};
let proxy = new Proxy(obj, handler);
proxies.add(proxy);
return proxy;
}
export function isProxy(obj) {
return proxies.has(obj);
}
This module uses the ECMAScript 6 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.js
can be used.
// main.js
import { createProxy, isProxy } from './lib.js';
let p = createProxy({});
console.log(isProxy(p)); // true
console.log(isProxy({})); // false
This section examines how JavaScript is structured internally and how the set of proxy traps was chosen.
The term protocol is highly overloaded in computer science. One definition is:
A prototcol is about achieving tasks via an object, it comprises a set of methods plus a set of rules for using them.
Note that this definition is different from viewing protocols as interfaces (as, for example, Objective C does), because it includes rules.
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 pretend that property names with square brackets are legal, this method would roughly be implemented as follows in JavaScript.
// Method definition
[[Get]](propKey, receiver) {
let desc = this.[[GetOwnProperty]](propKey);
if (desc === undefined) {
let parent = this.[[GetPrototypeOf]]();
if (parent === null) return undefined;
return parent.[[Get]](propKey, receiver); // (*)
}
if ('value' in desc) {
return desc.value;
}
let 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 (*) you 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. You 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 is either intercepted by a handler method or forwarded to the target.
What operations should be interceptable via proxies? One possibility is to only provide traps for fundamental operations. The alternative is to include some derived operations. The advantage of derived traps is that they increase performance and are more convenient: If there wasn’t a trap for get
, you’d have to implement its functionality via getOwnPropertyDescriptor
. One problem with derived traps is that they can lead to proxies behaving inconsistently. For example, get
may return a value that is different from the value stored in the descriptor returned by getOwnPropertyDescriptor
.
Intercession by proxies is selective: you 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 intercession. 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 intercession 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.
If you want to create virtual methods via ECMAScript 6 proxies, you 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 was an extra trap invoke
then that equivalence would be harder to maintain.
// Variant 1: call via dynamic dispatch
let result = obj.m();
// Variant 2: extract and call directly
let m = obj.m;
let result = m.call(obj);
Only possible with invoke
. Some things can only be done if you 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.
First, by making a proxy the prototype of an object obj
, you 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:
let boundMethod = obj.m;
let result = boundMethod();
Second, invoke
lets a proxy emulate the previously mentioned __noSuchMethod__
mechanism that Firefox supports. The proxy would again become the prototype of an object obj
. It would react differently depending on how an unknown property foo
is accessed:
obj.foo
, no intercession happens and undefined
is returned.obj.foo()
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. If an object is non-extensible, you can’t add properties and you can’t change its prototype:
'use strict'; // switch on strict mode to get TypeErrors
let obj = Object.preventExtensions({});
console.log(Object.isExtensible(obj)); // false
obj.foo = 123; // TypeError: object is not extensible
Object.setPrototypeOf(obj, null); // TypeError: object is not extensible
Non-configurability. All the data of a property is stored in attributes. A property is like a record and attributes are like the fields of that record. Examples of attributes:
value
holds the value of a property.writable
controls whether a property’s value can be changed.configurable
controls whether a property’s attributes can be changed.Thus, if a property is both non-writable and non-configurable, it is read-only and remains that way:
'use strict'; // switch on strict mode to get TypeErrors
let obj = {};
Object.defineProperty(obj, 'foo', {
value: 123,
writable: false,
configurable: false
});
console.log(obj.foo); // 123
obj.foo = 'a'; // TypeError: Cannot assign to read only property
Object.defineProperty(obj, 'foo', {
configurable: true
}); // TypeError: Cannot redefine property
For more details on these topics (including how Object.defineProperty()
works) consult the following sections in “Speaking JavaScript”:
Traditionally, non-extensibility and non-configurability are:
These and other characteristics that remain unchanged in the face of language operations are called invariants. With proxies, it is easy to violate invariants, as they are not intrinsically bound by non-extensibility etc.
The proxy API prevents proxies from violating invariants by checking the parameters and results of handler methods. Non-extensibility and non-configurability are enforced by using the target object for bookkeeping. The following are a few examples of invariants (for an arbitrary object obj
) and how they are enforced for proxies (an exhaustive list is given at the end of this post):
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.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)
.Enforcing invariants has the following benefits:
The following 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:
let fakeProto = {};
let handler = {
getPrototypeOf(t) {
return fakeProto;
}
};
Faking the prototype works if the target is extensible:
let extensibleTarget = {};
let ext = new Proxy(extensibleTarget, handler);
console.log(Object.getPrototypeOf(ext) === fakeProto); // true
We do, however, get an error if we fake the prototype for a non-extensible object.
let nonExtensibleTarget = {};
Object.preventExtensions(nonExtensibleTarget);
let nonExt = new Proxy(nonExtensibleTarget, handler);
Object.getPrototypeOf(nonExt); // TypeError
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.
let handler = {
get(target, propKey) {
return 'abc';
}
};
let target = Object.defineProperties(
{}, {
foo: {
value: 123,
writable: true,
configurable: true
},
bar: {
value: 456,
writable: false,
configurable: false
},
});
let proxy = new Proxy(target, handler);
Property target.foo
is not both non-writable and non-configurable, which means that the handler is allowed to pretend that it has a different value:
> proxy.foo
'abc'
However, property target.bar
is both non-writable and non-configurable. Therefore, we can’t fake its value:
> proxy.bar
TypeError: Invariant check failed
This section serves as a quick reference for the proxy API: the global objects Proxy
and Reflect
.
There are two ways to create proxies:
proxy = new Proxy(target, handler)
Creates a new proxy object with the given target and the given handler.
{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.foo // propKey = 'foo'
enumerate(target)
→ Iterator
for (x in proxy) ···
get(target, propKey, receiver)
→ any
receiver[propKey]
receiver.foo // propKey = 'foo'
getOwnPropertyDescriptor(target, propKey)
→ PropDesc|Undefined
Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target)
→ Object|Null
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-valued keys)Object.getOwnPropertyPropertySymbols(proxy)
(only uses symbol-valued keys)Object.keys(proxy)
(only uses enumerable string-valued keys; enumerability is checked via Object.getOwnPropertyDescriptor
)preventExtensions(target)
→ boolean
Object.preventExtensions(proxy)
set(target, propKey, value, receiver)
→ boolean
receiver[propKey] = value
receiver.foo = value // propKey = 'foo'
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)
→ 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, for data properties, 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 you 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. Ensuring the correct return value of a trap is ensured in two ways: Normally, an illegal value means that a TypeError
is thrown. But whenever a boolean is expected, coercion is used to convert non-booleans to legal values.
This is the complete list of invariants that are enforced (source: ECMAScript 6 specification):
apply(target, thisArgument, argumentsList)
construct(target, argumentsList)
null
or a primitive value).defineProperty(target, propKey, propDesc)
propDesc
can’t create a property that the target doesn’t already have.propDesc
sets the attribute configurable
to false
then the target must have a non-configurable own property whose key is propKey
.propDesc
was used to (re)define an own property for the target then that must not cause an exception. An exception is thrown if a change is forbidden by the attributes writable
and configurable
.deleteProperty(target, propKey)
enumerate(target)
get(target, propKey, receiver)
propKey
then the handler must return that property’s value.undefined
.getOwnPropertyDescriptor(target, propKey)
undefined
.writable
and configurable
. Therefore, the handler can’t report a non-configurable property as configurable and it can’t report a different value for a non-configurable non-writable property.getPrototypeOf(target)
null
.has(target, propKey)
isExtensible(target)
target.isExtensible()
.ownKeys(target)
preventExtensions(target)
target.isExtensible()
must be false
afterwards.set(target, propKey, value, receiver)
propKey
then value
must be the same as the value of that property (i.e., the property can’t be changed).TypeError
is thrown (i.e., such a property can’t be set).setPrototypeOf(target, proto)
proto
must be the same as the prototype of the target. Otherwise, a TypeError
is thrown.The following operations of normal objects perform operations on objects in the prototype chain (source: ECMAScript 6 specification). 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.enumerate()
target
via getPrototypeOf
. Per object, it retrieves the keys via ownKeys
and examines whether a property is enumerable via getOwnPropertyDescriptor
.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.
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
Function.prototype.apply()
.Reflect.construct(target, argumentsList, newTarget?)
→ Object
new
operator as a function.Reflect.defineProperty(target, propertyKey, propDesc)
→ boolean
Object.defineProperty()
.Reflect.deleteProperty(target, propertyKey)
→ boolean
delete
operator as a function.Reflect.enumerate(target)
→ Iterator
target
. In other words, the iterator returns all values that the for-in
loop would iterate over.Reflect.get(target, propertyKey, receiver?)
→ any
Reflect.getOwnPropertyDescriptor(target, propertyKey)
→ PropDesc|Undefined
Object.getOwnPropertyDescriptor()
.Reflect.getPrototypeOf(target)
→ Object|Null
Object.getPrototypeOf()
.Reflect.has(target, propertyKey)
→ boolean
in
operator as a function.Reflect.isExtensible(target)
→ boolean
Object.isExtensible()
.Reflect.ownKeys(target)
→ Array<PropertyKey>
Reflect.preventExtensions(target)
→ boolean
Object.preventExtensions()
.Reflect.set(target, propertyKey, value, receiver?)
→ boolean
Reflect.setPrototypeOf(target, proto)
→ boolean
__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.
Apart from forwarding operations, why is Reflect
useful [2]?
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
Reflect
methods implement functionality that is otherwise only available via operators:
Reflect.construct(target, argumentsList, newTarget?)
→ Object
Reflect.deleteProperty(target, propertyKey)
→ boolean
Reflect.get(target, propertyKey, receiver?)
→ any
Reflect.has(target, propertyKey)
→ boolean
Reflect.set(target, propertyKey, value, receiver?)
→ boolean
for-in
loop as an iterator: This is rarely useful, but if you need it, you can get an iterator over all enumerable (own and inherited) string property keys of an object.
Reflect.enumerate(target)
→ Iterator
apply
: The only safe way to invoke the built-in function method apply
is via Function.prototype.apply.call(func, thisArg, args)
(or similar). Reflect.apply(func, thisArg, args)
is cleaner and shorter.As usual, Kangax’ ES6 compatibility table is the best way of finding out how well engines support proxies. As of December 2014, Internet Explorer has the most complete support and Firefox supports some of the API (caveats: get
doesn’t work properly, getPrototypeOf
is not supported yet and Reflect
is empty). No other browser or engine currently supports proxies.
This concludes our in-depth look at the proxy API. For each application, you have to take performance into consideration and – if necessary – measure. Proxies may not always be fast enough. On the other hand, performance is often not crucial and it is nice to have the meta programming power that proxies give us. As we have seen, there are numerous use cases they can help with.
Thanks go to Tom Van Cutsem: his paper [3] is the most important source of this blog post and he kindly answered questions about the proxy API that I had.
Technical Reviewers:
“The Art of the Metaobject Protocol” by Gregor Kiczales, Jim des Rivieres and Daniel G. Bobrow. Book, 1991. ↩︎
“Harmony-reflect: Why should I use this library?” by Tom Van Cutsem. ↩︎
“On the design of the ECMAScript Reflection API” by Tom Van Cutsem and Mark Miller. Technical report, 2012. ↩︎