;
In computing terms, a proxy sits between you and the thing you are communicating with. The term is most often applied to a proxy server — a device between the web browser (Chrome, Firefox, Safari, Edge etc.) and the web server (Apache, NGINX, IIS etc.) where a page is located. The proxy server can modify requests and responses. For example, it can increase efficiency by caching regularly-accessed assets and serving them to multiple users.
ES6 proxies sit between your code and an object. A proxy allows you to perform meta-programming operations such as intercepting a call to inspect or change an object’s property.
The following terminology is used in relation to ES6 proxies:
target
The original object the proxy will virtualize. This could be a JavaScript object such as the jQuery library or native objects such as arrays or even another proxies.
handler
An object which implements the proxy’s behavior using…
traps
Functions defined in the handler which provide access to the target when specific properties or methods are called.
It’s best explained with a simple example. We’ll create a target object named target
which has three properties:
var target = {
a: 1,
b: 2,
c: 3
};
We’ll now create a handler object which intercepts all get operations. This returns the target’s property when it’s available or 42 otherwise:
var handler = {
get: function(target, name) {
return (
name in target ? target[name] : 42
);
}
};
We now create a new Proxy by passing the target and handler objects. Our code can interact with the proxy rather than accessing the target
object directly:
var proxy = new Proxy(target, handler);
console.log(proxy.a); // 1
console.log(proxy.b); // 2
console.log(proxy.c); // 3
console.log(proxy.meaningOfLife); // 42
Let’s expand the proxy handler further so it only permits single-character properties from a
to z
to be set:
var handler = {
get: function(target, name) {
return (name in target ? target[name] : 42);
},
set: function(target, prop, value) {
if (prop.length == 1 && prop >= 'a' && prop <= 'z') {
target[prop] = value;
return true;
}
else {
throw new ReferenceError(prop + ' cannot be set');
return false;
}
}
};
var proxy = new Proxy(target, handler);
proxy.a = 10;
proxy.b = 20;
proxy.ABC = 30;
// Exception: ReferenceError: ABC cannot be set
We’ve seen the get
and set
in action which are likely to be the most useful traps. However, there are several other trap types you can use to supplement proxy handler code:
new
operator.
Object.get()
and must return the property’s value.
Object.set()
and must set the property value. Return true
if successful. In strict mode, returning false
will throw a TypeError exception.
delete
operation on an objects property. Must return either true
or false
.
in
operators and must return either true
or false
.
for ... in
statements and must return an iterator object.
Object.getOwnPropertyNames()
and must return an enumerable object.
Object.getPrototypeOf()
and must return the prototype’s object or null.
Object.setPrototypeOf()
to set the prototype object — no value is returned.
Object.isExtensible()
which determines whether an object can have new properties added. Must return either true
or false
.
Object.preventExtensions()
which prevents new properties from being added to an object. Must return either true
or false
.
Object.getOwnPropertyDescriptor()
which returns undefined or a property descriptor object with attributes for value
, writable
, get
, set
, configurable
and enumerable
.
Object.defineProperty()
which defines or modifies an object property. Must return true
if the target property was successfully defined or false
if not.
Proxies allow you to create generic wrappers for any object without having to change the code within the target objects themselves.
In this example we’ll create a profiling proxy which counts the number of times a property is accessed. First, we require a makeProfiler
factory function which returns the Proxy object and retains the count state:
// create a profiling Proxy
function makeProfiler(target) {
var
count = {},
handler = {
get: function(target, name) {
if (name in target) {
count[name] = (count[name] || 0) + 1;
return target[name];
}
}
};
return {
proxy: new Proxy(target, handler),
count: count
}
};
We can now apply this proxy wrapper to any object or another proxy, e.g.
var myObject = {
h: 'Hello',
w: 'World'
};
// create a myObject proxy
var pObj = makeProfiler(myObject);
// access properties
console.log(pObj.proxy.h); // Hello
console.log(pObj.proxy.h); // Hello
console.log(pObj.proxy.w); // World
console.log(pObj.count.h); // 2
console.log(pObj.count.w); // 1
While this is a trivial example, imagine the effort involved if you had to perform property access counts in several different objects without using proxies.
Data binding synchronizes objects. It’s typically used in JavaScript MVC libraries to update an internal object when the DOM changes and vice versa.
Presume we have an input field with an id of inputname
:
<input type="text" id="inputname" value="" />
We also have a JavaScript object named myUser
with an id
property which references this input:
// internal state for #inputname field
var myUser = {
id: 'inputname',
name: ''
};
Our first objective is to update myUser.name
when a user changes the input value. This can be achieved with an onchange
event handler on the field:
inputChange(myUser);
// bind input to object
function inputChange(myObject) {
if (!myObject || !myObject.id) return;
var input = document.getElementById(myObject.id);
input.addEventListener('onchange', function(e) {
myObject.name = input.value;
});
}
Our next objective is to update the input field when we modify myUser.name
within JavaScript code. This is not as simple but proxies offer a solution:
// proxy handler
var inputHandler = {
set: function(target, prop, newValue) {
if (prop == 'name' && target.id) {
// update object property
target[prop] = newValue;
// update input field value
document.getElementById(target.id).value = newValue;
return true;
}
else return false;
}
}
// create proxy
var myUserProxy = new Proxy(myUser, inputHandler);
// set a new name
myUserProxy.name = 'Craig';
console.log(myUserProxy.name); // Craig
console.log(document.getElementById('inputname').value); // Craig
This is not be the most efficient data-binding option but proxies allow you to alter the behavior of many existing objects without changing their code.
Hemanth.HM’s article Negative Array Index in JavaScript suggests using proxies to implement negative array indexes, e.g. arr[-1]
returns the last element, arr[-2]
returns the second-to-last element, etc.
Nicholas C. Zakas’ article Creating type-safe properties with ECMAScript 6 proxies illustrates how proxies can be used to implement type safety by validating new values. In the example above, we could verify myUserProxy.name
was always set to a string and throw and error otherwise.
The power of proxies may not be immediately obvious but they offer powerful meta-programming opportunities. Brendan Eich, the creator of JavaScript, thinks Proxies are Awesome!
As of end 2015, basic proxy support is implemented in Edge and Firefox 18+ although not all traps can be used. Experimental support is available in Node 4.0+ if you run Node with the --harmony-proxies
flag but use it at your own risk.
Unfortunately, it’s not possible to polyfill or transpile ES6 proxy code using tools such as Babel because they’re powerful and have no ES5 equivalent. A little more waiting may be necessary.