Jonathan Chan
Jonathan Chan

Reputation: 2399

Is it possible to reflect the arguments of a Javascript function?

Is it possible to get all of the arguments a Javascript function is written to accept? (I know that all Javascript function arguments are "optional")? If not, is it possible to get the number of arguments? For example, in PHP, one could use:

$class = new ReflectionClass('classNameHere');
$methods = $class->getMethods();
foreach ($methods as $method) {
    print_r($method->getParameters());
}

... or something like that, I haven't touched PHP in a while so the example above may not be correct.

Thanks in advance!

EDIT: Unfortunately, I have to be able to get the arguments outside of the body of the function... Sorry for the lack of clarification, but thanks for the current answers!

Upvotes: 29

Views: 17272

Answers (9)

Landan Parker
Landan Parker

Reputation: 1

Here is my method for this use-case:

Grabbing the top level parameters of functional code blocks ie: lambda expressions and functions, named and anonymous.

If this is what you're wanting to extract from:

({ device: { name, brand }, settings }, { brightness = 50, volume = 70 }) => {};

And you are looking for this: ['{ device: { name,brand },settings }', '{ brightness = 50,volume = 70 }']

Then you can utilize this:

function getFirstLayerParams(func) {
  const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm;
  const paramSectionRegex = /\(([^)]+)\)/;
  let fnString = func.toString();
  fnString = fnString.replace(STRIP_COMMENTS, '');
  const paramsMatch = fnString.toString().match(paramSectionRegex);
  const theParams = [];
  if (paramsMatch) {
    const paramsString = paramsMatch[1];
    const params = paramsString.split(',').map((param) => param.trim());
    let depth = 0;
    let currentParam = '';
    for (const param of params) {
      currentParam += param;
      depth += (param.match(/{/g) || []).length - (param.match(/}/g) || []).length;
      if (depth === 0 && currentParam.trim().length > 0) {
        theParams.push(currentParam.trim());
        currentParam = '';
      } else {
        currentParam += ',';
      }
    }
  }
  return theParams;
}

As of right now, this will not work if you include defaults of functions/expressions ie: errorHandler = () => console.error('Error occurred') } Of course, resolving this last bit might not be that difficult, as you can just walk the parameters, and as you encounter them, identify the content to be a function of some sort and go from there. I'm not doing that because I don't expect to ever have defaults of function bodies, and I don't want to pull my hair out right now.

This came about after doing research of my own in order to build my approach to lift required parameter signatures like we need in test frameworks such as RTL and Playwright, I saw that maybe, I could call 'use' for fixtures on behalf of the developer (and to enable other features of my own) to shift the requirement to only passing fixture dependency parameters.

const funcArgs = getFirstLayerParams(func);
const paramsResult = funcArgs[0];
const paramSection = `${paramsResult ?? '{}'},`;
const nestedFunctionParamResult = funcArgs[0] ?? '{}'
const useThisFunc = new Function(
      `const func = arguments[0]; return (${paramSection} use) => { use(func(${nestedFunctionParamResult}));};`,
    ).bind(null, func)();

This is the basic aspect. I added more, such as injecting other parameters that weren't specified in implementation by inserting right before the final closing curly brace.

Also, if you want to extract the function body, all you have to do is take this code, and find the last index of characters where the parameter section would stop, then slice whatever you need.

Upvotes: 0

args = f => f.toString ()
    .replace( /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,'')
    .replace(/(\r\n\t|\n|\r\t)/gm,"")
    .trim()
    .match (/(?:\w*?\s?function\*?\s?\*?\s*\w*)?\s*(?:\((.*?)\)|([^\s]+))/)
    .slice (1,3)
    .join ('').replace(/\s/g, '').
    split (/\s*,\s*/);

/*Test*/
console.log(args((a,b)=>a+b));
console.log(args(function(c,d){return c+d;}));
console.log(args(async function(a,b,c){/**/}));
console.log(args(function* (a,b,c,d){/**/}));
console.log(args(function name(s1,s2){}));
console.log(args(function name(/*comment 1*/ s3/*comment2*/,s4//
){}));
console.log(args(async function* name(/*comment1*/ s5/*comment2*/,s6){}));

console.log(args(async function * name(/*comment1*/ s7/*comment2*/,s8){}));

console.log(args(async function *name(/*comment1*/ s9/*comment2*/,s10){}));

Upvotes: 1

HBP
HBP

Reputation: 16033

This new version handles fat arrow functions as well...

args = f => f.toString ().replace (/[\r\n\s]+/g, ' ').
              match (/(?:function\s*\w*)?\s*(?:\((.*?)\)|([^\s]+))/).
              slice (1,3).
              join ('').
              split (/\s*,\s*/);

function ftest (a,
                 b,
                 c) { }

let aftest = (a,
                 b,
                 c) => a + b / c;

console.log ( args (ftest),  // = ["a", "b", "c"] 
              args (aftest), // = ["a", "b", "c"]
              args (args)    // = ["f"]
             );

Here is what I think you are looking for :

 function ftest (a,
                 b,
                 c) { }
 var args = ftest.toString ().
              replace (/[\r\n\s]+/g, ' ').
              match (/function\s*\w*\s*\((.*?)\)/)[1].split (/\s*,\s*/);

args will be an array of the names of the arguments of test i.e. ['a', 'b', 'c']

The value is args will be an array of the parameter names if the ftest is a function. The array will be empty if ftest has not parameters. The value of args will be null if ftest fails the regular expression match, i.e it is not a function.

Upvotes: 46

nucleartide
nucleartide

Reputation: 3988

HBP's answer is what most people are looking for, but if you're the one defining the function, you can also assign a property to the function object. For example,

a.arguments = ['foo', 'bar', 'baz']
function a(foo, bar, baz) {
  // do stuff
}

This is debatably more clear, but you'll have to write your arguments twice.

Upvotes: 2

Mostkaj
Mostkaj

Reputation: 1697

check only required chars. with func.toString().regex you checked full length.so if function is class with 500 lines of code...

function getParams(func){
    var str=func.toString();
    var len = str.indexOf("(");
    return str.substr(len+1,str.indexOf(")")-len -1).replace(/ /g,"").split(',')
}

Upvotes: 2

Etienne
Etienne

Reputation: 16867

it is possible get all the formal parameter name of a javascript:

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

function formalParameterList(fn) {
   var fnText,argDecl;
   var args=[];
   fnText = fn.toString().replace(STRIP_COMMENTS, '');
   argDecl = fnText.match(FN_ARGS); 

   var r = argDecl[1].split(FN_ARG_SPLIT);
   for(var a in r){
      var arg = r[a];
      arg.replace(FN_ARG, function(all, underscore, name){
         args.push(name);
      });
   }
   return args;
 }

this can be tested this way :

 var expect = require('expect.js');
 expect( formalParameterList(function() {} )).to.eql([]);
 expect( formalParameterList(function () {} )).to.eql([]);
 expect( formalParameterList(function /*  */ () {} )).to.eql([]);
 expect( formalParameterList(function (/* */) {} )).to.eql([]);
 expect( formalParameterList(function ( a,   b, c  ,d /* */, e) {} )).to.eql(['a','b','c','d','e']);

Note: This technique is use with the $injector of AngularJs and implemented in the annotate function. (see https://github.com/angular/angular.js/blob/master/src/auto/injector.js and the corresponding unit test in https://github.com/angular/angular.js/blob/master/auto/injectorSpec.js )

Upvotes: 18

Ray Toal
Ray Toal

Reputation: 88378

Now when you say outside the body of the function I can only imagine that you want to know what the names of the parameters are? Because as far as the values go, you already know what arguments you are passing. Other answers have said you can get the length of the function, which is the number of parameters it explicitly declares. Now if you want to know the names outside the function, how about the toString hack?

Consider

function f(oh, hi, there) {
    return hi + there / oh;
}

Then

alert(f);

What do you see? RIght, just regex them out! Okay, SORRY to bring this up. Perhaps it is not standard ECMAScript, but it, uh, works in Chrome....

Upvotes: 1

xudifsd
xudifsd

Reputation: 850

JavaScript is a dialects of ECMAScript, according to ECMAScript standard, a function is also a object, and when a function is called, function can access arguments object, this arguments is array-like object, it has length property, so you can use arguments.length to traverse all arguments passed to this function. visit http://interglacial.com/javascript_spec/a-13.html#a-13.2.1 for more details.

Upvotes: -1

wong2
wong2

Reputation: 35730

Suppose your function name is foo

Is it possible to get all of the arguments a Javascript function is written to accept?

arguments[0] to arguments[foo.length-1]

If not, is it possible to get the number of arguments?

foo.length would work

Upvotes: 12

Related Questions