MethodMissing in JavaScript refers to a feature that allows developers to define a custom behavior when a method is called on an object that does not exist

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:

1
2
3
4
let aux = MyClass.prototype;
MyClass.prototype = new Proxy(aux, {
 //your get trap here
});

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Employee {
 //whatever
}

function EmployeeWithMethodMissing(name){
  //Employee.call(this); not allowed to call a constructor other than with new
  let emp = new Employee(name);
  Reflect.setPrototypeOf(emp, EmployeeWithMethodMissing.prototype);
  return emp;
 }
EmployeeWithMethodMissing.prototype = new Employee();
EmployeeWithMethodMissing.prototype.constructor = EmployeeWithMethodMissing;

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//manageMissingItem: function that will be called when an access to a missing property takes place
function addMethodMissingCapabilityToClass(classConstructor, manageMissingItem){
 let _proto = classConstructor.prototype;
 classConstructor.prototype = new Proxy(_proto, {
  get: function(target, key, receiver){
   //console.log("get trap invoked");
   
   if (key in target){
    return target[key];
   }
   else{
    //the problem here is that I can not know if they were asking for a method of for a data field
    //so this makes sense for a missing method, but for missing properties it does not
    return manageMissingItem(target, key);
   }
  }
 });
}

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”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
addMethodMissingCapabilityToClass(EmployeeWithMethodMissing, 
  //manageMissingItem function
  function(target, key){
   if (key === "tell" || key === "speak"){
    console.log(`method missing, but ${key} is synonymous with "say"`);
    return function(...args){
     console.log("calling the returned function with " + this.name);
     return this.say(...args);
    };
   }
   else{
    return undefined;
   }
  }
 );
 
 let e1 = new  EmployeeWithMethodMissing("Laurent");

 console.log(e1.doWork("task1", "task2"));
 console.log(e1.say("Hi"));
 console.log(e1.tell("Hi Again"));

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;.

Licensed under CC BY-NC-SA 4.0
Last updated on Jun 16, 2022 08:37 +0100