Search code examples
javascriptevalwrapperarity

Is it possible to dynamically create functions with specific arguments without using eval or the Function constructor?


Very similar to this Python question but for JavaScript.

I'm using a library that relies on nodejs-depd which uses new Function() to dynamically wrap functions in deprecation messages:

function wrapfunction(fn, message) {
  if (typeof fn !== "function") {
    throw new TypeError("argument fn must be a function");
  }

  var args = createArgumentsString(fn.length);
  var stack = getStack();
  var site = callSiteLocation(stack[1]);

  site.name = fn.name;

  var deprecatedFn = new Function(
    "fn",
    "log",
    "deprecate",
    "message",
    "site",
    '"use strict"\n' +
      "return function (" +
      args +
      ") {" +
      "log.call(deprecate, message, site)\n" +
      "return fn.apply(this, arguments)\n" +
      "}"
  )(fn, log, this, message, site);

  return deprecatedFn;
}

This produces various legitimate concerns about security. But it also produces functions that match the original - if args are arg0, arg1, the new function will be

function (arg0, arg1) {
  log.call(deprecate, message, site)
  return fn.apply(this, arguments)
}

BTW, 'no, this isn't possible' is a fine answer. I just want to find out if this is possible.


Solution

  • Dynamically create a function with a specific parameter declaration

    It is not possible to dynamically create a function with a specific parameter declaration without eval/new Function, but it also not advisable to depend on that - the code (fn.toString()) is considered an implementation detail.

    Dynamically create a function with a specific arity

    Yes, it is possible to dynamically create a function with a specific arity, if all you care about in the "signature" is the arity of the function.

    The arity (.length) of a function is a non-writable property, but it's still configurable:

    function wrapfunction(fn, message) {
      if (typeof fn !== "function") {
        throw new TypeError("argument fn must be a function");
      }
    
      var stack = getStack();
      var site = callSiteLocation(stack[1]);
    
      site.name = fn.name;
      var deprecate = this;
    
      var deprecatedFnOld = function() {
         log.call(deprecate, message, site);
         return fn.apply(this, arguments);
      };
      Object.defineProperty(deprecatedFnOld, 'length', {value: fn.length});
      Object.defineProperty(deprecatedFnOld, 'name', {value: 'deprecated '+fn.name});
    
      return deprecatedFnOld;
    }