- Metaprogramming in ES6: Part 3 – Proxies
- By Keith Cirkel
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: yoyoyohamapi
- Proofreader: Caoyi0905 PCAaron
Metaprogramming in ES6: Proxies
This is the third and final installment of my ES6 metaprogramming series, and I started writing this article a year ago. I promised it wouldn’t take a year to finish, but it has. In this final article, we’ll take a look at perhaps the coolest reflection feature in ES6: proxies. Since reflection is part of this article, if you haven’t read the previous article on the ES6 Symbols API and the earlier one on ES6 Symbols, go back and read it to get a better understanding of this article. As with other sections, LET me start with a quote from part I:
- Symbols is Reflection Within Implementation — you apply Symbols to your existing classes and objects to change their behavior.
- Reflect is Reflection through introspection — often used to explore very low-level code information.
- A Proxy implements Reflection through intercession — wraps an object and intercepts its behavior through an interception.
So Proxy is a new global constructor (like Date or Number) that you can pass an object to, along with hooks, that return you a new object that wraps the old object with those magic hooks. Now that you have the agency, I hope you enjoy it, and I’m glad you’re back in the series.
There is much to be said about agents. But for starters, let’s look at how to create an agent.
To create the agent
The Proxy constructor takes two arguments, the initial object you want to Proxy, and a set of handler hooks. Let’s ignore the second hook argument and see how to create a proxy for an existing object. The clue is in the name of the proxy: they maintain a reference to the object you created, but if you have a reference to the original object, any interaction you have with the original object will affect the proxy, and similarly, any changes you make to the proxy will in turn affect the original object. In other words, the Proxy returns a new object that wraps the incoming object, but anything you do to both affects each other. To verify this, take a look at the code:
var myObject = {};
var proxiedMyObject = new Proxy(myObject, {/* and a series of processing hooks */}); assert(myObject ! == proxiedMyObject); myObject.foo =true;
assert(proxiedMyObject.foo === true);
proxiedMyObject.bar = true;
assert(myObject.bar === true);Copy the code
So far, we have achieved nothing, and the proxy does not provide any additional benefits over using the proxied object directly. Only with processing hooks can we do interesting things with the agent.
The agent’s processing hook
Handling hooks is a series of functions, each given a specific name for the agent to recognize, and each hook also controls how you interact with the agent (and, therefore, how you interact with the wrapped object). Handling hooks hooks into JavaScript’s “built-in methods,” which if you’re familiar with, are because we covered built-in methods in our last article on the Reflect API.
It’s time to roll out and talk about agency. The important reason I put agents in the last part of the series is that since agents and reflection are like a star-crossed couple, we need to know how reflection works first. As you can see, each proxy hook corresponds to a reflection method, and vice versa, each reflection method has a proxy hook. The complete reflection method and corresponding proxy handling hooks are as follows:
apply
(in athis
Parameters and a series ofarguments
(Sequence of arguments) call functionconstruct
(With a series ofarguments
And an optional constructor that specifies the stereotype calls a class function or constructor.)defineProperty
Define a property on an object and declare meta information such as the iterability of the object in that property.getOwnPropertyDescriptor
Get a property’s “property descriptor” : The descriptor contains meta information such as the iterability of the object.deleteProperty
(Delete an attribute from an object)getPrototypeOf
(Get a prototype of an instance)setPrototypeOf
(Set the prototype of an instance)isExtensible
(To determine whether an object is “extensible,” that is, whether attributes can be added to it)preventExtensions
(Prevents objects from being extended)get
(Get a property of the object)set
(Sets a property of the object)has
(In the case of assert, determine whether an object has an attribute.)ownKeys
(Get all the keys of an object, excluding the keys of its prototype)
In the reflection section (again, if you haven’t seen it, go see it), we’ve gone through all of the above methods (with examples). The agent implements each method with the same set of arguments. In fact, the default behavior of the proxy already implements a reflection call to each handler hook (the internal mechanism may differ for different JavaScript engines, but for unstated hooks, we just need to assume that it behaves the same as the corresponding reflection method). This also means that any hook you don’t specify will behave as if it had never been propped:
// We created the new agent and defined the same behavior as the default creation
proxy = new Proxy({}, {
apply: Reflect.apply,
construct: Reflect.construct,
defineProperty: Reflect.defineProperty,
getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor,
deleteProperty: Reflect.deleteProperty,
getPrototypeOf: Reflect.getPrototypeOf,
setPrototypeOf: Reflect.setPrototypeOf,
isExtensible: Reflect.isExtensible,
preventExtensions: Reflect.preventExtensions,
get: Reflect.get,
set: Reflect.set,
has: Reflect.has,
ownKeys: Reflect.ownKeys,
});Copy the code
At this point, I can drill down into the details of how each proxy hook works, but basically copy and paste the reflection example (with very few changes). It would not be fair to the agent to just describe the functionality of each hook, since the agent is there to implement some cool use cases. So, the rest of this article will show you some cool things that can be done with an agent, even things you wouldn’t be able to do without an agent.
Also, to make the content more interactive, I created a small library for each example to show the corresponding functionality. I’ll give links to the code repository for each example.
Use the proxy to……
Build a chainable API
Building on the previous example — we still use [[Get]] trap: With a little more magic, we can build an API with countless methods that, when you finally call one of them, will return all the values you linked to. The Fluent API builds individual urls for Web requests, and testing frameworks like Chai combine individual English word links into highly readable test assertions, showing how useful an infinite linkable API can be.
To implement the API, we need the hook to hook [[Get]] and store the fetched property in an array. The Proxy wraps a function, returns all supported arrays retrieved, and empties the Array so it can be reused. We will also check [[HasProperty]] because we want to tell the API user that any property exists.
function urlBuilder(domain) {
var parts = [];
var proxy = new Proxy(function () {
var returnValue = domain + '/' + parts.join('/');
parts = [];
return returnValue;
}, {
has: function () {
return true;
},
get: function (object, prop) {
parts.push(prop);
returnproxy; }});return proxy;
}
var google = urlBuilder('http://google.com');
assert(google.search.products.bacon.and.eggs() === 'http://google.com/search/products/bacon/and/eggs')Copy the code
You can also implement the Fluent API for tree traversal using the same pattern, which is similar to the selectors you see in jQuery or React:
function treeTraverser(tree) {
var parts = [];
var proxy = new Proxy(function (parts) {
let node = tree; // Start at the root of the tree
for (part of parts) {
if(! node.props || ! node.props.children || node.props.children.length ===0) {
throw new Error(`Node ${node.tagName} has no more children`);
}
// If the part is a child node, go down to the child node for the next traversal
let index = node.props.children.findIndex((child) = > child.tagName == part);
if(index === - 1) {
throw new Error(`Cannot find child: ${part} in ${node.tagName}`);
}
node = node.props.children[index];
}
return node.props;
}, {
has: function () {
return true;
},
get: function () {
parts.push(prop);
returnproxy; }});return proxy;
}
var myDomIsh = treeTraverserExample({
tagName: 'body'.props: {
children: [{tagName: 'div'.props: {
className: 'main'.children: [{tagName: 'span'.props: {
className: 'extra'.children: [{tagName: 'i'.props: { textContent: 'Hello'}}, {tagName: 'b'.props: { textContent: 'World'}},]}}]}}]}}); assert(myDomIsh.div.span.i().textContent ==='Hello');
assert(myDomIsh.div.span.b().textContent === 'World');Copy the code
I have released a more reusable version to github.com/keithamus/p… There is also a package of the same name on NPM.
Implement a method missing hook
Many other programming languages allow you to override the behavior of a class using a built-in reflection method, such as __call in PHP, method_missing in Ruby, and __getattr__ in Python. JavaScript lacks this mechanism, but now we have a proxy to implement it.
Before we get into the implementation of the proxy, let’s take a look at what Ruby does to get some inspiration:
class Foo
def bar(a)
print "you called bar. Good job!"
end
def method_missing(method)
print "you called `#{method}` but it doesn't exist!"
end
end
foo = Foo.new
foo.bar()
#=> you called bar. Good job!
foo.this_method_does_not_exist()
#=》 you called this_method_does_not_exist but it doesn't exist!Copy the code
For any existing method, bar in this case, that method can be executed as expected. Methods that do not exist, such as foo or this_method_does_not_exist, will be replaced by method_missing when called. In addition, method_missing accepts the method name as the first argument, which is useful for determining user intent.
We can do something similar by mixing in the ES6 Symbol: wrap the class with a function that returns the agent that uses get ([[get]]) to trap itself, or that intercepts the behavior of get:
function Foo() {
return new Proxy(this, {
get: function (object, property) {
if (Reflect.has(object, property)) {
return Reflect.get(object, property);
} else {
return function methodMissing() {
console.log('you called ' + property + ' but it doesn\'t exist! '); }}}}); } Foo.prototype.bar =function () {
console.log('you called bar. Good job! ');
}
foo = new Foo();
foo.bar();
// you called bar. Good job!
foo.this_method_does_not_exist();
// you called this_method_does_not_exist but it doesn't exist!Copy the code
This is useful when you have several methods that have very similar functions, and you can infer the differences from the function names. Moving function differentiation from arguments to function names leads to better readability. As an example of this — you can quickly and easily create a unit conversion API, such as currency or base conversion:
const baseConvertor = new Proxy({}, {
get: function baseConvert(object, methodName) {
var methodParts = methodName.match(/base(\d+)toBase(\d+)/);
var fromBase = methodParts && methodParts[1];
var toBase = methodParts && methodParts[2];
if(! methodParts || fromBase >36 || toBase > 36 || fromBase < 2 || toBase < 2) {
throw new Error('TypeError: baseConvertor' + methodName + ' is not a function');
}
return function (fromString) {
return parseInt(fromString, fromBase).toString(toBase); }}}); baseConvertor.base16toBase2('deadbeef') = = ='11011110101011011011111011101111';
baseConvertor.base2toBase16('11011110101011011011111011101111') = = ='deadbeef';Copy the code
Of course, you can create methods that total 1296 combined cases manually, or create them through a separate loop, but both require more code to do.
A more specific example is ActiveRecord in Ruby on Rails, which comes from “Dynamic Finders.” ActiveRecord basically implements “method_missing” to allow you to query a table by column. Using the function name as the query key avoids creating a query by passing a complex object:
Users.find_by_first_name('Keith'); # [ Keith Cirkel, Keith Urban, Keith David ]
Users.find_by_first_name_and_last_name('Keith', 'David'); # [ Keith David ]Copy the code
In JavaScript, we can do something similar:
function RecordFinder(options) {
this.attributes = options.attributes;
this.table = options.table;
return new Proxy({}, function findProxy(methodName) {
var match = methodName.match(new RegExp('findBy((? :And)' + this.attributes.join('|') + ') '));
if(! match){throw new Error('TypeError: ' + methodName + ' is not a function'); }}); });Copy the code
As with the other examples, I’ve written a library about this and put it at github.com/keithamus/p… Packages of the same name are also available on NPM.
fromgetOwnPropertyNames
,Object.keys
,in
All attributes are hidden in all iterative methods
We can use proxies to hide all attributes of an object, except to get the value of the attribute. Here’s a list of all the JavaScript ways you can tell if a property is present in an object:
Reflect.has
,Object.hasOwnProperty
,Object.prototype.hasOwnProperty
As well asin
All operators are used[[HasProperty]]
. The proxy can be accessed throughhas
Intercept it.Object.keys
/Object.getOwnPropertyNames
use[[OwnPropertyKeys]]
. The proxy can be accessed throughownKeys
Intercept.Object.entries
(an upcoming ES2017 feature), also used[[OwnPropertyKeys]]
, the agent can still passownKeys
Intercept.Object.getOwnPropertyDescriptor
Using the[[GetOwnProperty]]
. And what’s really, really exciting is that the agent can get throughgetOwnPropertyDescriptor
Intercept.
var example = new Proxy({ foo: 1.bar: 2 }, {
has: function () { return false; },
ownKeys: function () { return []; },
getOwnPropertyDescriptor: function () { return false; }}); assert(example.foo ===1);
assert(example.bar === 2);
assert('foo' in example === false);
assert('bar' in example === false);
assert(example.hasOwnProperty('foo') = = =false);
assert(example.hasOwnProperty('bar') = = =false);
assert.deepEqual(Object.keys(example), [ ]);
assert.deepEqual(Object.getOwnPropertyNames(example), [ ]);Copy the code
To be honest, I don’t find this model particularly useful either. However, I created a library for this and put it at github.com/keithamus/p… , which allows you to make one property invisible individually, rather than making all properties invisible at once.
Implement an observer pattern, also known as Object.observe
Those who have been keenly following the additions to the new specification may have noticed that Object.Observe is starting to be considered for ES2016. Object.observe advocates are already planning to draft a proposal that includes Object.observe, and they have a very good reason for doing so: It was originally intended to help framework authors solve Data Binding issues. Now, with the release of React and Polymer 1.0, data binding frameworks have cooled down and immutable data has become popular.
Fortunately, proxies make specifications such as Object.observe redundant, and we can now implement a much lower-level Object.observe via proxies. In order to get closer to the characteristics of Object.observe, We need to hook the built-in methods [[Set]], [[PreventExtensions]], [[Delete]] and [[DefineOwnProperty]] – the proxy can use them separately Set, preventExtensions, deleteProperty and defineProperty
function observe(object, observerCallback) {
var observing = true;
const proxyObject = new Proxy(object, {
set: function (object, property, value) {
var hadProperty = Reflect.has(object, property);
var oldValue = hadProperty && Reflect.get(object, property);
var returnValue = Reflect.set(object, property, value);
if (observing && hadProperty) {
observerCallback({ object: proxyObject, type: 'update'.name: property, oldValue: oldValue });
} else if(observing) {
observerCallback({ object: proxyObject, type: 'add'.name: property });
}
return returnValue;
},
deleteProperty: function (object, property) {
var hadProperty = Reflect.has(object, property);
var oldValue = hadProperty && Reflect.get(object, property);
var returnValue = Reflect.deleteProperty(object, property);
if (observing && hadProperty) {
observerCallback({ object: proxyObject, type: 'delete'.name: property, oldValue: oldValue });
}
return returnValue;
},
defineProperty: function (object, property, descriptor) {
var hadProperty = Reflect.has(object, property);
var oldValue = hadProperty && Reflect.getOwnPropertyDescriptor(object, property);
var returnValue = Reflect.defineProperty(object, property, descriptor);
if (observing && hadProperty) {
observerCallback({ object: proxyObject, type: 'reconfigure'.name: property, oldValue: oldValue });
} else if(observing) {
observerCallback({ object: proxyObject, type: 'add'.name: property });
}
return returnValue;
},
preventExtensions: function (object) {
var returnValue = Reflect.preventExtensions(object);
if (observing) {
observerCallback({ object: proxyObject, type: 'preventExtensions'})}returnreturnValue; }});return { object: proxyObject, unobserve: function () { observing = false}}; }var changes = [];
var observer = observe({ id: 1 }, (change) => changes.push(change));
var object = observer.object;
var unobserve = observer.unobserve;
object.a = 'b';
object.id++;
Object.defineProperty(object, 'a', { enumerable: false });
delete object.a;
Object.preventExtensions(object);
unobserve();
object.id++;
assert.equal(changes.length, 5);
assert.equal(changes[0].object, object);
assert.equal(changes[0].type, 'add');
assert.equal(changes[0].name, 'a');
assert.equal(changes[1].object, object);
assert.equal(changes[1].type, 'update');
assert.equal(changes[1].name, 'id');
assert.equal(changes[1].oldValue, 1);
assert.equal(changes[2].object, object);
assert.equal(changes[2].type, 'reconfigure');
assert.equal(changes[2].oldValue.enumerable, true);
assert.equal(changes[3].object, object);
assert.equal(changes[3].type, 'delete');
assert.equal(changes[3].name, 'a');
assert.equal(changes[4].object, object);
assert.equal(changes[4].type, 'preventExtensions');Copy the code
As you can see, we implemented a relatively complete Object.Observe in a small piece of code. The difference between this implementation and the specification is that object.observe can change an Object, whereas the proxy returns a new Object, and the unobserver function is not global.
As with the other examples, I’ve also written a library about this and put it at github.com/keithamus/p… And on NPM.
Bonus level: revocable agent
One final trick for agents: some agents can be revoked. To create an undoable Proxy, you need to use proxy.revocable (target, handler) (instead of new Proxy(target, handler)) and, Instead of returning a proxy object directly, a {proxy, REVOKE ()} object is eventually returned. Examples are as follows:
function youOnlyGetOneSafetyNet(object) {
var revocable = Proxy.revocable(object, {
get(target, property) {
if (Reflect.has(target, property)) {
return Reflect.get(target, property);
} else {
revocable.revoke();
return 'You only get one safety net'; }}});return revocable.proxy;
}
var myObject = youOnlyGetOneSafetyNet({ foo: true });
assert(myObject.foo === true);
assert(myObject.foo === true);
assert(myObject.foo === true);
assert(myObject.bar === 'You only get one safety net');
myObject.bar // TypeError
myObject.bar // TypeError
Reflect.has(myObject, 'bar') // TypeErrorCopy the code
Unfortunately, as you can see on the right side of the last line in the example, if the proxy has been revoked, any operations on the proxy object will raise TypeError — even if the operation handles have not yet been proxied. I think it might be the ability to revocable proxy. This feature would be even more useful if all the operations could be returned with the corresponding Reflect (which would make the proxy redundant and make the object behave as if the proxy had never been set). This feature is at the end of this article because I’m not really sure about the specific use cases for retractable proxies either.
conclusion
I hope this article has made you realize that proxies are an incredibly powerful tool that fills in the gaps inside JavaScript. In many ways, symbols, Reflections, and proxies open a new chapter for JavaScript — just as const and let, classes, and arrow functions do. Const and let no longer make code messy, classes and arrow functions make code more concise, and Symbol, Reflect, and Proxy start to give developers low-level metaprogramming in JavaScript.
These new metaprogramming tools won’t slow things down any time soon: New versions of EcamScript are getting better and adding more interesting behaviors, such as reflect.isCallable and Reflect.isconstructor proposals, or stage 0 proposals for reflect.type, Either a proposal for the function.sent meta-property, or a proposal that contains more function meta-properties. These new apis have also inspired some interesting discussions about new features, such as the proposal to add Reflection.parse, which led to a discussion about creating an AST standard.
What do you think of the new Proxy API? Have you planned to use it in your project? Drop me a line on Twitter and let me know what you think. I’m @Keithamus.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, React, front-end, back-end, product, design and other fields. If you want to see more high-quality translation, please continue to pay attention to the Project, official Weibo, Zhihu column.