How to get function parameter names/values dynamically?

前端 未结 30 2445
说谎
说谎 2020-11-22 00:13

Is there a way to get the function parameter names of a function dynamically?

Let’s say my function looks like this:

function doSomething(param1, par         


        
相关标签:
30条回答
  • 2020-11-22 00:41

    You can also use "esprima" parser to avoid many issues with comments, whitespace and other things inside parameters list.

    function getParameters(yourFunction) {
        var i,
            // safetyValve is necessary, because sole "function () {...}"
            // is not a valid syntax
            parsed = esprima.parse("safetyValve = " + yourFunction.toString()),
            params = parsed.body[0].expression.right.params,
            ret = [];
    
        for (i = 0; i < params.length; i += 1) {
            // Handle default params. Exe: function defaults(a = 0,b = 2,c = 3){}
            if (params[i].type == 'AssignmentPattern') {
                ret.push(params[i].left.name)
            } else {
                ret.push(params[i].name);
            }
        }
    
        return ret;
    }
    

    It works even with code like this:

    getParameters(function (hello /*, foo ),* /bar* { */,world) {}); // ["hello", "world"]
    
    0 讨论(0)
  • 2020-11-22 00:41

    Wow so many answers already.. Im pretty sure this gets buried. Even so I figured this might be useful for some.

    I wasn't fully satisfied with the chosen answers as in ES6 it doesn't work well with default values. And it also does not provide the default value information. I also wanted a lightweight function that does not depend on an external lib.

    This function is very useful for debugging purposes, for example: logging called function with its params, default param values and arguments.

    I spent some time on this yesterday, cracking the right RegExp to solve this issue and this is what I came up with. It works very well and I'm very pleased with the outcome:

    const REGEX_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
    const REGEX_FUNCTION_PARAMS = /(?:\s*(?:function\s*[^(]*)?\s*)((?:[^'"]|(?:(?:(['"])(?:(?:.*?[^\\]\2)|\2))))*?)\s*(?=(?:=>)|{)/m
    const REGEX_PARAMETERS_VALUES = /\s*(\w+)\s*(?:=\s*((?:(?:(['"])(?:\3|(?:.*?[^\\]\3)))((\s*\+\s*)(?:(?:(['"])(?:\6|(?:.*?[^\\]\6)))|(?:[\w$]*)))*)|.*?))?\s*(?:,|$)/gm
    
    /**
     * Retrieve a function's parameter names and default values
     * Notes:
     *  - parameters with default values will not show up in transpiler code (Babel) because the parameter is removed from the function.
     *  - does NOT support inline arrow functions as default values
     *      to clarify: ( name = "string", add = defaultAddFunction )   - is ok
     *                  ( name = "string", add = ( a )=> a + 1 )        - is NOT ok
     *  - does NOT support default string value that are appended with a non-standard ( word characters or $ ) variable name
     *      to clarify: ( name = "string" + b )         - is ok
     *                  ( name = "string" + $b )        - is ok
     *                  ( name = "string" + b + "!" )   - is ok
     *                  ( name = "string" + λ )         - is NOT ok
     * @param {function} func
     * @returns {Array} - An array of the given function's parameter [key, default value] pairs.
     */
    function getParams(func) {
    
      let functionAsString = func.toString()
      let params = []
      let match
      functionAsString = functionAsString.replace(REGEX_COMMENTS, '')
      functionAsString = functionAsString.match(REGEX_FUNCTION_PARAMS)[1]
      if (functionAsString.charAt(0) === '(') functionAsString = functionAsString.slice(1, -1)
      while (match = REGEX_PARAMETERS_VALUES.exec(functionAsString)) params.push([match[1], match[2]])
      return params
    
    }
    
    
    
    // Lets run some tests!
    
    var defaultName = 'some name'
    
    function test1(param1, param2, param3) { return (param1) => param1 + param2 + param3 }
    function test2(param1, param2 = 4 * (5 / 3), param3) {}
    function test3(param1, param2 = "/root/" + defaultName + ".jpeg", param3) {}
    function test4(param1, param2 = (a) => a + 1) {}
    
    console.log(getParams(test1)) 
    console.log(getParams(test2))
    console.log(getParams(test3))
    console.log(getParams(test4))
    
    // [ [ 'param1', undefined ], [ 'param2', undefined ], [ 'param3', undefined ] ]
    // [ [ 'param1', undefined ], [ 'param2', '4 * (5 / 3)' ], [ 'param3', undefined ] ]
    // [ [ 'param1', undefined ], [ 'param2', '"/root/" + defaultName + ".jpeg"' ], [ 'param3', undefined ] ]
    // [ [ 'param1', undefined ], [ 'param2', '( a' ] ]
    // --> This last one fails because of the inlined arrow function!
    
    
    var arrowTest1 = (a = 1) => a + 4
    var arrowTest2 = a => b => a + b
    var arrowTest3 = (param1 = "/" + defaultName) => { return param1 + '...' }
    var arrowTest4 = (param1 = "/" + defaultName, param2 = 4, param3 = null) => { () => param3 ? param3 : param2 }
    
    console.log(getParams(arrowTest1))
    console.log(getParams(arrowTest2))
    console.log(getParams(arrowTest3))
    console.log(getParams(arrowTest4))
    
    // [ [ 'a', '1' ] ]
    // [ [ 'a', undefined ] ]
    // [ [ 'param1', '"/" + defaultName' ] ]
    // [ [ 'param1', '"/" + defaultName' ], [ 'param2', '4' ], [ 'param3', 'null' ] ]
    
    
    console.log(getParams((param1) => param1 + 1))
    console.log(getParams((param1 = 'default') => { return param1 + '.jpeg' }))
    
    // [ [ 'param1', undefined ] ]
    // [ [ 'param1', '\'default\'' ] ]

    As you can tell some of the parameter names disappear because the Babel transpiler removes them from the function. If you would run this in the latest NodeJS it works as expected (The commented results are from NodeJS).

    Another note, as stated in the comment is that is does not work with inlined arrow functions as a default value. This simply makes it far to complex to extract the values using a RegExp.

    Please let me know if this was useful for you! Would love to hear some feedback!

    0 讨论(0)
  • 2020-11-22 00:43

    Here is an updated solution that attempts to address all the edge cases mentioned above in a compact way:

    function $args(func) {  
        return (func + '')
          .replace(/[/][/].*$/mg,'') // strip single-line comments
          .replace(/\s+/g, '') // strip white space
          .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  
          .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters  
          .replace(/=[^,]+/g, '') // strip any ES6 defaults  
          .split(',').filter(Boolean); // split & filter [""]
    }  
    

    Abbreviated test output (full test cases are attached below):

    'function (a,b,c)...' // returns ["a","b","c"]
    'function ()...' // returns []
    'function named(a, b, c) ...' // returns ["a","b","c"]
    'function (a /* = 1 */, b /* = true */) ...' // returns ["a","b"]
    'function fprintf(handle, fmt /*, ...*/) ...' // returns ["handle","fmt"]
    'function( a, b = 1, c )...' // returns ["a","b","c"]
    'function (a=4*(5/3), b) ...' // returns ["a","b"]
    'function (a, // single-line comment xjunk) ...' // returns ["a","b"]
    'function (a /* fooled you...' // returns ["a","b"]
    'function (a /* function() yes */, \n /* no, */b)/* omg! */...' // returns ["a","b"]
    'function ( A, b \n,c ,d \n ) \n ...' // returns ["A","b","c","d"]
    'function (a,b)...' // returns ["a","b"]
    'function $args(func) ...' // returns ["func"]
    'null...' // returns ["null"]
    'function Object() ...' // returns []
    

    function $args(func) {  
        return (func + '')
          .replace(/[/][/].*$/mg,'') // strip single-line comments
          .replace(/\s+/g, '') // strip white space
          .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments  
          .split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters  
          .replace(/=[^,]+/g, '') // strip any ES6 defaults  
          .split(',').filter(Boolean); // split & filter [""]
    }  
    
    // test cases  
    document.getElementById('console_info').innerHTML = (
    [  
      // formatting -- typical  
      function(a,b,c){},  
      function(){},  
      function named(a, b,  c) {  
    /* multiline body */  
      },  
        
      // default values -- conventional  
      function(a /* = 1 */, b /* = true */) { a = a||1; b=b||true; },  
      function fprintf(handle, fmt /*, ...*/) { },  
      
      // default values -- ES6  
      "function( a, b = 1, c ){}",  
      "function (a=4*(5/3), b) {}",  
      
      // embedded comments -- sardonic  
      function(a, // single-line comment xjunk) {}
        b //,c,d
      ) // single-line comment
      {},  
      function(a /* fooled you{*/,b){},  
      function /* are you kidding me? (){} */(a /* function() yes */,  
       /* no, */b)/* omg! */{/*}}*/},  
      
      // formatting -- sardonic  
      function  (  A,  b  
    ,c  ,d  
      )  
      {  
      },  
      
      // by reference  
      this.jQuery || function (a,b){return new e.fn.init(a,b,h)},
      $args,  
      
      // inadvertent non-function values  
      null,  
      Object  
    ].map(function(f) {
        var abbr = (f + '').replace(/\n/g, '\\n').replace(/\s+|[{]+$/g, ' ').split("{", 1)[0] + "...";
        return "    '" + abbr + "' // returns " + JSON.stringify($args(f));
      }).join("\n") + "\n"); // output for copy and paste as a markdown snippet
    <pre id='console_info'></pre>

    0 讨论(0)
  • Since JavaScript is a scripting language, I feel that its introspection should support getting function parameter names. Punting on that functionality is a violation of first principles, so I decided to explore the issue further.

    That led me to this question but no built-in solutions. Which led me to this answer which explains that arguments is only deprecated outside the function, so we can no longer use myFunction.arguments or we get:

    TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
    

    Time to roll up our sleeves and get to work:

    ⭐ Retrieving function parameters requires a parser because complex expressions like 4*(5/3) can be used as default values. So Gaafar's answer or James Drew's answer are so far the best approaches.

    I tried the babylon and esprima parsers but unfortunately they can't parse standalone anonymous functions, as pointed out in Mateusz Charytoniuk's answer. I figured out another workaround though by surrounding the code in parentheses, so as not to change the logic:

    const ast = parser.parse("(\n" + func.toString() + "\n)")
    

    The newlines prevent issues with // (single-line comments).

    ⭐ If a parser is not available, the next-best option is to use a tried-and-true technique like Angular.js's dependency injector regular expressions. I combined a functional version of Lambder's answer with humbletim's answer and added an optional ARROW boolean for controlling whether ES6 fat arrow functions are allowed by the regular expressions.


    Here are two solutions I put together. Note that these have no logic to detect whether a function has valid syntax, they only extract the arguments. This is generally ok since we usually pass parsed functions to getArguments() so their syntax is already valid.

    I will try to curate these solutions as best I can, but without effort from the JavaScript maintainers, this will remain an open problem.

    Node.js version (not runnable until StackOverflow supports Node.js):

    const parserName = 'babylon';
    // const parserName = 'esprima';
    const parser = require(parserName);
    
    function getArguments(func) {
        const maybe = function (x) {
            return x || {}; // optionals support
        }
    
        try {
            const ast = parser.parse("(\n" + func.toString() + "\n)");
            const program = parserName == 'babylon' ? ast.program : ast;
    
            return program
                .body[0]
                .expression
                .params
                .map(function(node) {
                    return node.name || maybe(node.left).name || '...' + maybe(node.argument).name;
                });
        } catch (e) {
            return []; // could also return null
        }
    };
    
    ////////// TESTS //////////
    
    function logArgs(func) {
    	let object = {};
    
    	object[func] = getArguments(func);
    
    	console.log(object);
    // 	console.log(/*JSON.stringify(*/getArguments(func)/*)*/);
    }
    
    console.log('');
    console.log('////////// MISC //////////');
    
    logArgs((a, b) => {});
    logArgs((a, b = 1) => {});
    logArgs((a, b, ...args) => {});
    logArgs(function(a, b, ...args) {});
    logArgs(function(a, b = 1, c = 4 * (5 / 3), d = 2) {});
    logArgs(async function(a, b, ...args) {});
    logArgs(function async(a, b, ...args) {});
    
    console.log('');
    console.log('////////// FUNCTIONS //////////');
    
    logArgs(function(a, b, c) {});
    logArgs(function() {});
    logArgs(function named(a, b, c) {});
    logArgs(function(a /* = 1 */, b /* = true */) {});
    logArgs(function fprintf(handle, fmt /*, ...*/) {});
    logArgs(function(a, b = 1, c) {});
    logArgs(function(a = 4 * (5 / 3), b) {});
    // logArgs(function (a, // single-line comment xjunk) {});
    // logArgs(function (a /* fooled you {});
    // logArgs(function (a /* function() yes */, \n /* no, */b)/* omg! */ {});
    // logArgs(function ( A, b \n,c ,d \n ) \n {});
    logArgs(function(a, b) {});
    logArgs(function $args(func) {});
    logArgs(null);
    logArgs(function Object() {});
    
    console.log('');
    console.log('////////// STRINGS //////////');
    
    logArgs('function (a,b,c) {}');
    logArgs('function () {}');
    logArgs('function named(a, b, c) {}');
    logArgs('function (a /* = 1 */, b /* = true */) {}');
    logArgs('function fprintf(handle, fmt /*, ...*/) {}');
    logArgs('function( a, b = 1, c ) {}');
    logArgs('function (a=4*(5/3), b) {}');
    logArgs('function (a, // single-line comment xjunk) {}');
    logArgs('function (a /* fooled you {}');
    logArgs('function (a /* function() yes */, \n /* no, */b)/* omg! */ {}');
    logArgs('function ( A, b \n,c ,d \n ) \n {}');
    logArgs('function (a,b) {}');
    logArgs('function $args(func) {}');
    logArgs('null');
    logArgs('function Object() {}');

    Full working example:

    https://repl.it/repls/SandybrownPhonyAngles

    Browser version (note that it stops at the first complex default value):

    function getArguments(func) {
        const ARROW = true;
        const FUNC_ARGS = ARROW ? /^(function)?\s*[^\(]*\(\s*([^\)]*)\)/m : /^(function)\s*[^\(]*\(\s*([^\)]*)\)/m;
        const FUNC_ARG_SPLIT = /,/;
        const FUNC_ARG = /^\s*(_?)(.+?)\1\s*$/;
        const STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
    
        return ((func || '').toString().replace(STRIP_COMMENTS, '').match(FUNC_ARGS) || ['', '', ''])[2]
            .split(FUNC_ARG_SPLIT)
            .map(function(arg) {
                return arg.replace(FUNC_ARG, function(all, underscore, name) {
                    return name.split('=')[0].trim();
                });
            })
            .filter(String);
    }
    
    ////////// TESTS //////////
    
    function logArgs(func) {
    	let object = {};
    
    	object[func] = getArguments(func);
    
    	console.log(object);
    // 	console.log(/*JSON.stringify(*/getArguments(func)/*)*/);
    }
    
    console.log('');
    console.log('////////// MISC //////////');
    
    logArgs((a, b) => {});
    logArgs((a, b = 1) => {});
    logArgs((a, b, ...args) => {});
    logArgs(function(a, b, ...args) {});
    logArgs(function(a, b = 1, c = 4 * (5 / 3), d = 2) {});
    logArgs(async function(a, b, ...args) {});
    logArgs(function async(a, b, ...args) {});
    
    console.log('');
    console.log('////////// FUNCTIONS //////////');
    
    logArgs(function(a, b, c) {});
    logArgs(function() {});
    logArgs(function named(a, b, c) {});
    logArgs(function(a /* = 1 */, b /* = true */) {});
    logArgs(function fprintf(handle, fmt /*, ...*/) {});
    logArgs(function(a, b = 1, c) {});
    logArgs(function(a = 4 * (5 / 3), b) {});
    // logArgs(function (a, // single-line comment xjunk) {});
    // logArgs(function (a /* fooled you {});
    // logArgs(function (a /* function() yes */, \n /* no, */b)/* omg! */ {});
    // logArgs(function ( A, b \n,c ,d \n ) \n {});
    logArgs(function(a, b) {});
    logArgs(function $args(func) {});
    logArgs(null);
    logArgs(function Object() {});
    
    console.log('');
    console.log('////////// STRINGS //////////');
    
    logArgs('function (a,b,c) {}');
    logArgs('function () {}');
    logArgs('function named(a, b, c) {}');
    logArgs('function (a /* = 1 */, b /* = true */) {}');
    logArgs('function fprintf(handle, fmt /*, ...*/) {}');
    logArgs('function( a, b = 1, c ) {}');
    logArgs('function (a=4*(5/3), b) {}');
    logArgs('function (a, // single-line comment xjunk) {}');
    logArgs('function (a /* fooled you {}');
    logArgs('function (a /* function() yes */, \n /* no, */b)/* omg! */ {}');
    logArgs('function ( A, b \n,c ,d \n ) \n {}');
    logArgs('function (a,b) {}');
    logArgs('function $args(func) {}');
    logArgs('null');
    logArgs('function Object() {}');

    Full working example:

    https://repl.it/repls/StupendousShowyOffices

    0 讨论(0)
  • 2020-11-22 00:44

    I know this is an old question, but beginners have been copypasting this around as if this was good practice in any code. Most of the time, having to parse a function's string representation to use its parameter names just hides a flaw in the code's logic.

    Parameters of a function are actually stored in an array-like object called arguments, where the first argument is arguments[0], the second is arguments[1] and so on. Writing parameter names in the parentheses can be seen as a shorthand syntax. This:

    function doSomething(foo, bar) {
        console.log("does something");
    }
    

    ...is the same as:

    function doSomething() {
        var foo = arguments[0];
        var bar = arguments[1];
    
        console.log("does something");
    }
    

    The variables themselves are stored in the function's scope, not as properties in an object. There is no way to retrieve the parameter name through code as it is merely a symbol representing the variable in human-language.

    I always considered the string representation of a function as a tool for debugging purposes, especially because of this arguments array-like object. You are not required to give names to the arguments in the first place. If you try parsing a stringified function, it doesn't actually tell you about extra unnamed parameters it might take.

    Here's an even worse and more common situation. If a function has more than 3 or 4 arguments, it might be logical to pass it an object instead, which is easier to work with.

    function saySomething(obj) {
      if(obj.message) console.log((obj.sender || "Anon") + ": " + obj.message);
    }
    
    saySomething({sender: "user123", message: "Hello world"});
    

    In this case, the function itself will be able to read through the object it receives and look for its properties and get both their names and values, but trying to parse the string representation of the function would only give you "obj" for parameters, which isn't useful at all.

    0 讨论(0)
  • 2020-11-22 00:46
    function getArgs(args) {
        var argsObj = {};
    
        var argList = /\(([^)]*)/.exec(args.callee)[1];
        var argCnt = 0;
        var tokens;
    
        while (tokens = /\s*([^,]+)/g.exec(argList)) {
            argsObj[tokens[1]] = args[argCnt++];
        }
    
        return argsObj;
    }
    
    0 讨论(0)
提交回复
热议问题