Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I run a function with arguments from an object?

Tags:

javascript

I have a lot of functions like the following:

var runme = function(liked, disliked){ 
  console.log(`i like ${liked} but I dislike ${disliked}`) 
}

I have an object with I would like to use to fill in the arguments of the function

var obj = {liked: 'apple', disliked: 'pear'}

How can I run the function using the object to specify the arguments?

I tried using spread syntax:

runme(...obj)

But this produces:

TypeError: Found non-callable @@iterator

How can I run a function with parameters from an object?

I can't change the functions, as I am creating a wrapper that needs to be able to handle arbitrary functions.

Edit: I've edited the post to use 'liked' and 'disliked' instead of 'one' and 'two' as this better shows that ordering matters.

I can use any version of JavaScript, up to and including ES9.

like image 736
mikemaccana Avatar asked Feb 07 '26 01:02

mikemaccana


2 Answers

There is no general-purpose way to accomplish this.

If you can guarantee the provenance of the source then you could use an AST operation to build your wrappers during a transpilation or load phase.

If you cannot, then you're particularly out of luck, because the parameter names may be mangled, making an object-key-to-parameter-name transformation impossible.

like image 199
Dave Newton Avatar answered Feb 12 '26 05:02

Dave Newton


I can't change the functions, as I am creating a wrapper that needs to be able to handle arbitrary functions.

That's unfortunate, since making them accept a destructured parameter would be exactly what you need.

Unfortunately, the names of parameters are not available unless you parse the result of calling toString on the function, which is...fraught with peril. You basically need a full JavaScript parser (because parameter lists are complex these days, including possibly containing entire function definitions for default values) and minifiers and such may rename parameters (changing liked to _0, for instance).

You also can't count on the order of the properties in an object. (They do have an order, but not one that helps here...or almost anywhere else.)

You've said you need to handle functions whose parameters you don't know in advance, so my various ideas around wrapping functions with utilities that require passing in the names of the parameters won't work. (If anyone's curious, look at the revision list to see those.)

You can do this from toString if we can make several assumptions:

  1. The function parameter lists are simple. They don't include destructuring or default values.

  2. Comments are not used within the parameter lists.

  3. Your minifier does not rename function parameters.

  4. The functions are all traditional functions, methods, or arrow functions that do have () around the parameter list (so for instance, (x) => x * 2, not just x => x * 2).

  5. You don't mind that it'll be fairly inefficient (parsing each time).

That's a lot of assumptions and I don't recommend it. But if you can rely on them:

// LOTS of assumptions here!
function run(f, obj) {
  let params = /\(([\w\s,]*)\)/.exec(String(f));
  if (!params) {
    throw new Error("Couldn't parse function");
  }
  params = params[1].split(/\s*,\s*/).map(n => n.trim());
  return f.apply(this, params.map(param => obj[param]));
}

run(runme, obj);

Live Example:

// Traditional function
const runme = function(liked, disliked){ 
  console.log(`i like ${liked} but I hate ${disliked}`) 
}
// Traditional function with newlines
const runme2 = function(
    liked,
    disliked
  ){ 
  console.log(`i like ${liked} but I hate ${disliked}`) 
}
// Arrow function
const runme3 = (liked, disliked) => { 
  console.log(`i like ${liked} but I hate ${disliked}`) 
}
// Method
const {runme4} = {
  runme4(liked, disliked) { 
    console.log(`i like ${liked} but I hate ${disliked}`) 
  }
};

const obj = {liked: 'apple', disliked: 'pear'}

function run(f, obj) {
  let params = /\(([\w\s,]*)\)/.exec(String(f));
  if (!params) {
    throw new Error("Couldn't parse function");
  }
  params = params[1].split(/\s*,\s*/).map(n => n.trim());
  return f.apply(this, params.map(param => obj[param]));
}

run(runme, obj);
run(runme2, obj);
run(runme3, obj);
run(runme4, obj);

That works because Function.prototype.toString is standardized now, and even in resource-constrained environments it's required to include the parameter list (but may well not include the rest of the function implementation).

like image 24
T.J. Crowder Avatar answered Feb 12 '26 04:02

T.J. Crowder