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

9

29

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!

Millrace answered 3/8, 2011 at 4:21 Comment(5)
debugging anything with getParameters() just makes me so mad. i mean, that's not helpful, i'm just saying is all. maybe there's a better way of solving your problem.Crumley
Sure there is. Through the arguments keyword (inside a function it is an array-like object were arguments[0] is the first argument, etc). This isn't an answer because I am too lazy to go find a good reference. However, this only exposes the values, not the names. Depending upon a particular "To String" implementation for a Function, the names can be extracted via that and parsing magic.Baldhead
@sudowned :/ I'm not debugging, reflection is an integral part in the design of my application...Millrace
@pst Unfortunately I have to be able to get the arguments outside of the body of the function - I'm updating the question.Millrace
Rather late, but apparently the term I was looking for was parameters. I found out a (rather convoluted) way to get those: jsbin.com/ucacit.Millrace
S
12

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

Schacker answered 3/8, 2011 at 4:24 Comment(3)
I think the .length property is the best you can do. After all, who cares what the names of the parameters are?Varela
Sorry for the poor wording, but this is what I intended to do. Thanks! :)Millrace
caveat: if the function is defined with the spread syntax, it length will return 0 (see e.g., (function(...args) { return args; }).length)Ruth
A
46

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.

Aviva answered 3/8, 2011 at 5:27 Comment(8)
One small flaw in your regex that the Angular one in @Etienne's answer addresses is the first \s+ will prevent matching anonymous functions with no space between the keyword and the brackets, i.e. function(x,y) { /* ... */ }. This could be avoided by changing it to a \s* instead.Indigestible
Thanks for that, in my own coding I always put in that space. I've updated the code.Aviva
The error that GregL pointed out in your first example also occurs (in a slightly different form) in your second. The regex should probably read /^\s*function(?:\s+\w*)?\s*\(([\s\S]*?)\)/. (I also made it allow for multi-line argument lists.)Spindell
Thanks for the report. Function.toString does not return the source as you typed it (at least on browsers I have tested). but returns a reformatted version so some of these changes may not be strictly speaking necessary. To make it absolutely foolproof you would need to cater of comments wherever whitespace is valid.Aviva
Why are you complicating the regex so much? Why not just do this: ftest.toString().match(/\([^\)]*\)/m)[0].match(/[^\n\r\s,\)\(]/g). It is basically matching everything between the first pair of round brackets (i.e. (<content>)) and then matching everything that is not a bracket, comma, or whitespace within those brackets.Kirchhoff
@nadeem Re my comment of 15 July '13 above. The value returned by the toString function applied to a function is not fully specified. It may be a "sanitized" version or the original source text. In the latter case, it is possible for comments to be present. The extra checking in the regular expression reduces the chances of an erroneous match in those cases.Aviva
Thanks for the answer. This doesn't work if you have a new-line in your signature though. The solution by @Etienne does https://mcmap.net/q/94069/-is-it-possible-to-reflect-the-arguments-of-a-javascript-functionFredrika
@Aviva how about arrow functions?Marrowfat
P
18

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 )

Peugia answered 1/12, 2012 at 14:59 Comment(2)
What is the purpose of capturing the parameter underscore separately and ignoring it?Molokai
Great solution. I don't get why if I grab functions from this I cannot apply to it, see getAllFunctions by @kooinc in #11279941 and the comment below.Skein
S
12

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

Schacker answered 3/8, 2011 at 4:24 Comment(3)
I think the .length property is the best you can do. After all, who cares what the names of the parameters are?Varela
Sorry for the poor wording, but this is what I intended to do. Thanks! :)Millrace
caveat: if the function is defined with the spread syntax, it length will return 0 (see e.g., (function(...args) { return args; }).length)Ruth
E
2

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(',')
}
Emerick answered 24/9, 2013 at 22:6 Comment(0)
N
2

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.

Nebiim answered 2/6, 2016 at 16:16 Comment(0)
V
1

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

Varela answered 3/8, 2011 at 5:27 Comment(0)
C
1

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){}));
Cutch answered 27/2, 2019 at 3:25 Comment(0)
A
0

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.

Appetizer answered 19/2, 2024 at 1:53 Comment(0)
E
-1

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.

Erigeron answered 3/8, 2011 at 4:58 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.