One of the last major pieces of the ECMAScript standard we are going to touch on is two metaprogramming objects. Metaprogramming is the technique of having code that generates code. This could be for things such as compilers or parsers. It could also be for self-changing code. It can even be for runtime evaluation of another language (interpreting) and doing something with this. While this is probably the main feature that reflection and proxies give us, it also gives us the ability to listen to events on an object.
In the previous chapter, we talked about listening to events and we created a CustomEvent to listen for events on our object. Well, we can change that code and utilize proxies for that behavior. The following is some basic code to handle basic events on an object:
const item = new Proxy({}, {
get: function(obj, prop) {
console.log('getting the following property', prop);
return Reflect.has(obj, prop) ? obj[prop] : null;
},
set: function(obj, prop, value) {
console.log('trying to set the following prop with the following
value', prop, value);
if( typeof value === 'string' ) {
obj[prop] = value;
} else {
throw new Error('Value type is not a string!');
}
}
});
item.one = 'what';
item.two = 'is';
console.log(item.one);
console.log(item.three);
item.three = 12;
What we have done is add some basic logging for the get and set methods on this object. We have extended the functionality of this object by also making the set method only take string values. With this, we have created an object that can be listened to and we can respond to those events.
Another interesting property of proxies is revocable methods. This is a proxy that we can eventually say is revoked and this will throw a TypeError when we try to use it after this method call. This can be very useful for anyone trying to implement the RAII pattern with objects. Instead of trying to null out the reference, we can revoke the proxy and we will no longer be able to utilize it.
An example of this is shown here with a modified version of the preceding code:
const isPrimitive = function(item) {
return typeof item === 'string' || typeof item === 'number' || typeof
item === 'boolean';
}
const item2 = Proxy.revocable({}, {
get: function(obj, prop) {
return Reflect.has(obj, prop) ? obj[prop] : null
},
set: function(obj, prop, value) {
if( isPrimitive(value) ) {
obj[prop] = value;
} else {
throw new Error('Value type is not a primitive!');
}
}
});
const item2Proxy = item2.proxy;
item2Proxy.one = 'this';
item2Proxy.two = 12;
item2Proxy.three = true;
item2.revoke();
(function(obj) {
console.log(obj.one);
})(item2Proxy);
Now, instead of just throwing TypeErrors on the set, we also will throw a TypeError once we revoke the proxy. This can be of great use to us when we decide to write code that will protect itself. We also no longer need to write a bunch of guard clauses in our code when we are utilizing objects. If we utilize proxies and revocables instead, we are able to guard our sets.
Besides proxies, the Reflect API is a bunch of static methods that mirror the proxy handlers. We can utilize them in place of some familiar systems such as the Function.prototype.apply method. We can instead utilize the Reflect.apply method, which can be a bit clearer when writing our code. This looks like the following:
Math.max.apply(null, [1, 2, 3]);
Reflect.apply(Math.max, null, [1, 2, 3]);
item3 = {};
if( Reflect.set(item3, 'yep', 12) {
console.log('value was set correctly!');
} else {
console.log('value was not set!');
}
Reflect.defineProperty(item3, 'readonly', {value : 42});
if( Reflect.set(item3, 'readonly', 'nope') ) {
console.log('we set the value');
} else {
console.log('value should not be set!');
}
As we can see, we set a value on our object the first time and it was successful. But, the second property was first defined and it was set to non-writable (the default when we use defineProperty ), and so we were not able to set a value on it.
With both of these APIs, we can write some nice functionality for accessing objects and even making mutations as safe as possible. We can utilize the RAII pattern very easily with these two APIs and we can even do some cool metaprogramming along with it.