When we compare ES6 to Groovy (a language I find very expressive, as seen on some playing with it years ago), we notice some missing features in ES6. These include features like methodMissing (which many other languages have) and Groovy’s clean method interception using invokeMethod. However, we can achieve both of these features in ES6 using proxies and a _get trap.
Note that I’ll be using the term “class” throughout this discussion, regardless of whether we are using ES6 class syntax or the traditional JavaScript style.
Proxies have always seemed powerful, but they presented a challenge: needing to create a separate proxy for every object that needed these features. Ideally, we want to implement this at the class level. Since everything in JavaScript is an object, we can achieve this by proxying the right object: the prototype. This technique is already used for implementing features like multiple inheritance. I’ve created an example demonstrating how to implement “method missing” using this approach.
My initial thought was to simply wrap MyClass.prototype in a proxy and reassign it:
| |
However, this doesn’t work with the ES6 class syntax because the constructor function’s prototype is neither writable nor configurable, preventing reassignment. To work around this, we can create a subclass using the older syntax, allowing us to reassign the prototype. For example, to add “method missing” to an Employee class, we define a subclass like this:
| |
Now, let’s define a function that takes a constructor function and wraps its prototype in a proxy. This proxy will provide the “method missing” functionality:
| |
This function accepts a second parameter, manageMissingItem, which defines the behavior when a missing method is called. Here’s how we can use it to create method aliases, treating calls to “tell” or “speak” as calls to “say”:
| |
You can find the complete code for this example on gist.
This technique works well when you have a limited and known set of potential missing methods, like in the example above. However, there’s a problem if you want to trap any missing method call.
In JavaScript, method invocation is a two-step process. First, the function for the method is retrieved (our get trap), and then it is invoked (with the correct this and parameters). This means that within our get trap, we can’t determine if the retrieved element is a function or a data field. Unlike Groovy, we cannot distinguish between methodMissing and propertyMissing. If we return a function for any missing retrieval, it leads to problems when the user expects data. Instead of receiving the expected string (or undefined), they get a function that won’t be invoked. This is because data access involves only retrieval, not invocation.
One potential trick is to return a function with a missingMethodReturn property. This would allow for checks like: let data = p.dataField; data = data.missingMethodReturn ? undefined : data;.