Synthesizing Members with Proxy

In Injecting Multiple Properties we injected the first and last properties into instances of Array so we could fluently get the first and last elements of an array. We came up with property names like first and last at the time of metaprogramming, but on many occasions we may want to devise property or method names at runtime based on the execution context or state of an object. In other words, we may not know the name of a property to be injected at code writing time—it comes to life at runtime.

To practice method synthesis, let’s create an example to use a Map that holds an associative set of keys and values of languages and authors.

 const​ langsAndAuthors = ​new​ Map([
  [​'JavaScript'​, ​'Eich'​], [​'Java'​, ​'Gosling'​]]);
 
 const​ accessLangsMap = ​function​(map) {
  console.log(​`Number of languages: ​${map.size}​`​);
  console.log(​`Author of JavaScript: ​${map.​get​(​'JavaScript'​)}​`​);
  console.log(​`Asking fluently: ​${map.JavaScript}​`​);
 };
 
 accessLangsMap(langsAndAuthors);

The langsAndAuthors map contains the names of two prominent languages as keys and their authors as values.

The accessLangsMap() function receives an instance of Map and prints size—a property—and calls the get() method to get the value for the JavaScript key. It then, in good faith, tries to access the key as if it’s a property.

It would be really nice to access the author’s name by using the dot notation, but sadly that does not yield the desired result:

 Number of languages: 2
 Author of JavaScript: Eich
 Asking fluently: undefined

You’ll now learn to synthesize dynamic properties on a map—they’re dynamic because we may later add other keys like Ruby or Python, for example, to our map. We can’t predict their names at the time of metaprogramming to handle the nonexistent property or method.

Member Synthesis for an Instance

Let’s synthesize properties on the langsAndAuthors instance of Map. In Metaprogramming with Proxy you learned how to trap calls to members using Proxy. We’ll use that technique here to create a trap for the missing property.

Recall that the get() trap in a handler of a proxy is called whenever a field, method, or property is invoked. The trap is called for both existing and nonexisting members. When implementing a trap, we must do a few different things depending on whether the request is for a property or a method.

During synthesis, if a call for an existing property is received, we may want to immediately return the property on the target.

If the call, however, is to an existing method, we may want to return the actual method of the target, but we will have to bind it to the target before returning. Let’s discuss the reason for binding. When a method is called with the syntax obj.someMethod(); JavaScript automatically binds obj as the context object this within the method. However, if we assign the method to a variable—like const methodRef = obj.someMethod;—then the this context will be unbounded if we call methodRef();. To resolve this, we will have to explicitly bind the context object before making the call.

Finally, if the requested member does not exist on the target object, then we can synthesize the behavior for it.

Let’s create a Proxy for the langsAndAuthors object to synthesize dynamic properties.

 const​ handler = {
  get: ​function​(target, propertyName, receiver) {
 if​(Reflect.has(target, propertyName)) {
 const​ property = Reflect.​get​(target, propertyName);
 
 if​(property ​instanceof​ Function) { ​//existing method, bind and return
 return​ property.bind(target);
  }
 
 //existing property, return as-is
 return​ property;
  }
 
 //synthesize property: we assume it is a key
 return​ target.​get​(propertyName);
  }
 };
 
 const​ proxy = ​new​ Proxy(langsAndAuthors, handler);
 
 accessLangsMap(proxy);

In the handler we create a trap for get(). In the trap we first check if the property with the given name, in the variable propertyName, exists on the target, using the has() method of Reflect. If it exists, we then check if it is an instance of Function. If it is a function, we bind the obtained property to target and return. If it is not a function, we return the property value as is.

If has() returns false, telling us that the property does not exist, then it’s time to synthesize a behavior for the call.

Since our objective in this example is to create dynamic properties for keys, in the synthesis part of the trap we assume the given property name is a key and return the value for that key by using the get() method on the target. If the key does not exist, the result will be undefined.

Finally, take a look at the last line, the call to the accessLangsMap() function. Instead of passing langsAndAuthors, we pass the proxy reference that we created. The output from this call is different from when we passed the original map instance:

 Number of languages: 2
 Author of JavaScript: Eich
 Asking fluently: Eich

The output shows that even though Map instances do not have a property named Eich, the proxy on our langsAndAuthors instance was able to respond to it.

Before we declare this solution a great success, let’s discuss one pitfall. A key as a dynamic property is available only when used through a proxy on that one particular instance of Map. It would be nice if the solution worked on all instances as well as directly on the instances of Map instead of a proxy. With some deep understanding of JavaScript and metaprogramming, we can make that happen, as you’ll see next.

Synthesizing Members Directly on an Instance

Rather than calling proxy.key, we should be able to perform anyInstanceOfMap.key. To achieve that, we need to bring two different pieces of knowledge together.

First, when a nonexistent property is requested on an object, JavaScript automatically requests it from the object’s prototype. If the prototype does not have it, then the search continues through the prototype chain—see Understanding Prototypal Inheritance.

Second, a proxy can trap requests for properties, fields, and methods, among other things—in essence, the details we have explored in this chapter.

Let’s combine these two pieces of knowledge to create a powerful synthesis. If a property or a method exists, then we simply want to use it—there is no need to mess with proxy or traps in that case. Let the proxy step in only if a property or a method does not exist. We know that JavaScript will look to an object’s prototype when something does not exist on an object—that’s a good spot to synthesize. Thus, we can place the proxy behind an object, as its prototype, instead of what we did in the previous section, to put the proxy in front of the object—ta-da!

We almost have a solution, but we have to be careful when replacing prototypes. An instance of Map gets its instance methods from its prototype—that is, Map.prototype. Replacing the Map instance’s prototype will unfortunately get rid of those methods—not a good idea. It turns out that Map.prototype’s prototype is an empty object; we can find this by calling

 console.log(Reflect.getPrototypeOf(Map.prototype));

That’s a good candidate to replace with a proxy. This design thought is illustrated in the prototype diagram.

images/mapproxy.png

In the design we created in the previous section, the get() trap of our proxy had to do some extra work: taking care of both existing and nonexisting members. In the new design, the proxy does not have to worry about existing members—the original object, as the receiver of calls, will take care of that. The proxy, now serving as the state-appointed prototype, will be called on only for nonexistent members.

There is one other significant difference between the design of proxies in the previous section and here. In the previous one, the receiver was the proxy since the key was called on the proxy. Here, the receiver is the instance of Map. With those thoughts in mind, let’s implement our new design.

 const​ proxy = ​new​ Proxy(Map.prototype, {
  get: ​function​(target, propertyName, receiver) {
 return​ receiver.​get​(propertyName);
  }
 });
 
 Reflect.setPrototypeOf(Map.prototype, proxy);

We created a new Proxy for Map.prototype—which houses all the instance methods for Map—instead of creating a proxy on one specific instance of Map. In the handler we trap get() and assume the given property represents a key. The trap method is much simpler than the one we created in the previous section. Then, we set the newly created proxy as the prototype of the Map’s prototype.

Let’s now see this proxy in action on an instance of Map, for example, langsAndAuthors:

 const​ langsAndAuthors = ​new​ Map([
  [​'JavaScript'​, ​'Eich'​], [​'Java'​, ​'Gosling'​]]);
 
 console.log(langsAndAuthors.​get​(​'JavaScript'​));
 console.log(langsAndAuthors.JavaScript);

We created an instance of Map and directly called the dynamic property on it after making the traditional built-in call with get. The instance happily responds to both the calls:

 Eich
 Eich

Let’s verify that this solution works on a different instance of Map as well.

 const​ capitals = ​new​ Map([
  [​'USA'​, ​'Washington. D.C.'​],
  [​'UK'​, ​'London'​],
  [​'Trinidad & Tobago'​, ​'Port of Spain'​]]);
 
 console.log(capitals.UK);
 console.log(capitals[​'Trinidad & Tobago'​]);

This example uses countries and capitals instead of languages and authors, with one twist; there’s a country name with & in the middle. The proxy we created can handle that with no worries—remember to use [] to access the property with & since it does not conform to JavaScript property names syntax.

 London
 Port of Spain

Now we can declare our solution a great success.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset