如何动态获取函数参数名称/值?

IT技术 javascript reflection function-parameter
2021-01-09 08:26:59

有没有办法动态获取函数的函数参数名称?

假设我的函数如下所示:

function doSomething(param1, param2, .... paramN){
   // fill an array with the parameter name and value
   // some other code 
}

现在,我如何从函数内部将参数名称及其值的列表放入数组中?

6个回答

以下函数将返回传入的任何函数的参数名称的数组。

var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var ARGUMENT_NAMES = /([^\s,]+)/g;
function getParamNames(func) {
  var fnStr = func.toString().replace(STRIP_COMMENTS, '');
  var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
  if(result === null)
     result = [];
  return result;
}

用法示例:

getParamNames(getParamNames) // returns ['func']
getParamNames(function (a,b,c,d){}) // returns ['a','b','c','d']
getParamNames(function (a,/*b,c,*/d){}) // returns ['a','d']
getParamNames(function (){}) // returns []

编辑

随着 ES6 的发明,这个函数可以被默认参数触发。这是一个在大多数情况下应该有效的快速技巧:

var STRIP_COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg;

我说大多数情况是因为有些事情会绊倒它

function (a=4*(5/3), b) {} // returns ['a']

编辑:我还注意到 vikasde 也希望数组中的参数值。这已经在名为 arguments 的局部变量中提供。

摘自https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments

参数对象不是数组。它类似于数组,但除了长度之外没有任何数组属性。例如,它没有 pop 方法。但是它可以转换为一个真正的数组:

var args = Array.prototype.slice.call(arguments);

如果 Array 泛型可用,则可以改用以下内容:

var args = Array.slice(arguments);
ES6 箭头函数只有一个参数,如 (a => a*10),它无法提供所需的输出。
2021-03-13 08:26:59
请注意,此解决方案可能会因注释和空格而失败 - 例如:var fn = function(a /* fooled you)*/,b){};将导致 ["a", "/*", "fooled", "you"]
2021-03-14 08:26:59
编译正则表达式是有成本的,因此您希望避免多次编译复杂的正则表达式。这就是为什么它在函数之外完成
2021-03-18 08:26:59
当没有任何参数时,我修改了函数以返回一个空数组(而不是 null)
2021-03-24 08:26:59
更正:打算使用 perl 允许的 /s 修饰符修改正则表达式。也可以匹配换行符。这对于 /* */ 中的多行注释是必需的。原来 Javascript 正则表达式不允许 /s 修饰符。使用 [/s/S] 的原始正则表达式匹配换行符。SOOO,请无视之前的评论。
2021-03-30 08:26:59

下面是取自 AngularJS 的代码,它使用了依赖注入机制的技术。

这是来自http://docs.angularjs.org/tutorial/step_05的解释

在构建控制器时,Angular 的依赖注入器会为您的控制器提供服务。依赖注入器还负责创建服务可能具有的任何传递依赖(服务通常依赖于其他服务)。

请注意,参数的名称很重要,因为注入器使用这些名称来查找依赖项。

/**
 * @ngdoc overview
 * @name AUTO
 * @description
 *
 * Implicit module which gets automatically added to each {@link AUTO.$injector $injector}.
 */

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

  if (typeof fn == 'function') {
    if (!($inject = fn.$inject)) {
      $inject = [];
      fnText = fn.toString().replace(STRIP_COMMENTS, '');
      argDecl = fnText.match(FN_ARGS);
      forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
        arg.replace(FN_ARG, function(all, underscore, name){
          $inject.push(name);
        });
      });
      fn.$inject = $inject;
    }
  } else if (isArray(fn)) {
    last = fn.length - 1;
    assertArgFn(fn[last], 'fn')
    $inject = fn.slice(0, last);
  } else {
    assertArgFn(fn, 'fn', true);
  }
  return $inject;
}
我确实在互联网上搜索了这个话题,因为我很好奇 Angular 是如何做到的……现在我知道了,而且我知道的太多了!
2021-03-18 08:26:59
显然,@apaidnerd 带着恶魔的鲜血和撒旦的后裔。正则表达式?!如果在 JS 中有一个内置的方式会很酷,不是吗。
2021-03-21 08:26:59
为了节省人们的时间,您可以通过 angular 获取此功能 annotate = angular.injector.$$annotate
2021-03-21 08:26:59
@apaidnerd,太真实了!只是想 - 这到底是如何实施的?实际上我考虑过使用 functionName.toString() 但我希望更优雅(也许更快)
2021-03-24 08:26:59
@sasha.sochka,来到这里想知道完全相同的事情,在意识到没有内置的方式来使用 javascript 获取参数名称之后
2021-04-01 08:26:59

这是一个更新的解决方案,试图以紧凑的方式解决上述所有边缘情况:

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 [""]
}  

缩写的测试输出(完整的测试用例附在下面):

'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 []

这将解构对象(如({ a, b, c })拆分为解构内部的所有参数。为了保持解构的对象完好无损,将最后一个更改.split为: .split(/,(?![^{]*})/g)
2021-03-23 08:26:59
当存在单行注释时,这会中断。尝试这个: return (func+'') .replace(/[/][/].*$/mg,'') // strip single-line comments (line-ending sensitive, so goes first) .replace(/\s+/g,'') // remove whitespace
2021-03-29 08:26:59
当函数具有自定义 .toString() 实现时,您可能应该替换func + ''Function.toString.call(func)以抵御这种情况。
2021-03-30 08:26:59
当存在包含“//”或“/*”的默认字符串值时,这也不起作用
2021-03-30 08:26:59
粗箭头 => .split(/\)[\{=]/, 1)[0]
2021-04-08 08:26:59

不太容易出现空格和注释的解决方案是:

var fn = function(/* whoa) */ hi, you){};

fn.toString()
  .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg,'')
  .match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
  .split(/,/)

["hi", "you"]
@AlexMills 我注意到的一件事是箭头函数的规范说它们不应该被视为“函数”。这意味着它不适合匹配数组函数。'this' 的设置方式不同,也不应该将它们作为函数调用。这是我艰难地学到的东西。($myService) => $myService.doSomething() 看起来很酷,但它滥用了数组函数。
2021-03-16 08:26:59

这里的很多答案都使用正则表达式,这很好,但它不能很好地处理语言的新增功能(如箭头函数和类)。另外值得注意的是,如果你在缩小代码上使用这些函数中的任何一个,它就会去🔥。它将使用任何缩小的名称。Angular 通过允许您在将参数注册到 DI 容器时传入与参数顺序匹配的有序字符串数组来解决这个问题。继续解决方案:

var esprima = require('esprima');
var _ = require('lodash');

const parseFunctionArguments = (func) => {
    // allows us to access properties that may or may not exist without throwing 
    // TypeError: Cannot set property 'x' of undefined
    const maybe = (x) => (x || {});

    // handle conversion to string and then to JSON AST
    const functionAsString = func.toString();
    const tree = esprima.parse(functionAsString);
    console.log(JSON.stringify(tree, null, 4))
    // We need to figure out where the main params are. Stupid arrow functions 👊
    const isArrowExpression = (maybe(_.first(tree.body)).type == 'ExpressionStatement');
    const params = isArrowExpression ? maybe(maybe(_.first(tree.body)).expression).params 
                                     : maybe(_.first(tree.body)).params;

    // extract out the param names from the JSON AST
    return _.map(params, 'name');
};

这处理了原始解析问题和更多的函数类型(例如箭头函数)。以下是它可以和不能按原样处理什么的想法:

// I usually use mocha as the test runner and chai as the assertion library
describe('Extracts argument names from function signature. 💪', () => {
    const test = (func) => {
        const expectation = ['it', 'parses', 'me'];
        const result = parseFunctionArguments(toBeParsed);
        result.should.equal(expectation);
    } 

    it('Parses a function declaration.', () => {
        function toBeParsed(it, parses, me){};
        test(toBeParsed);
    });

    it('Parses a functional expression.', () => {
        const toBeParsed = function(it, parses, me){};
        test(toBeParsed);
    });

    it('Parses an arrow function', () => {
        const toBeParsed = (it, parses, me) => {};
        test(toBeParsed);
    });

    // ================= cases not currently handled ========================

    // It blows up on this type of messing. TBH if you do this it deserves to 
    // fail 😋 On a tech note the params are pulled down in the function similar 
    // to how destructuring is handled by the ast.
    it('Parses complex default params', () => {
        function toBeParsed(it=4*(5/3), parses, me) {}
        test(toBeParsed);
    });

    // This passes back ['_ref'] as the params of the function. The _ref is a 
    // pointer to an VariableDeclarator where the ✨🦄 happens.
    it('Parses object destructuring param definitions.' () => {
        function toBeParsed ({it, parses, me}){}
        test(toBeParsed);
    });

    it('Parses object destructuring param definitions.' () => {
        function toBeParsed ([it, parses, me]){}
        test(toBeParsed);
    });

    // Classes while similar from an end result point of view to function
    // declarations are handled completely differently in the JS AST. 
    it('Parses a class constructor when passed through', () => {
        class ToBeParsed {
            constructor(it, parses, me) {}
        }
        test(ToBeParsed);
    });
});

取决于你想将它用于 ES6 代理的什么,解构可能是你最好的选择。例如,如果您想将它用于依赖项注入(使用参数的名称),那么您可以按如下方式进行:

class GuiceJs {
    constructor() {
        this.modules = {}
    }
    resolve(name) {
        return this.getInjector()(this.modules[name]);
    }
    addModule(name, module) {
        this.modules[name] = module;
    }
    getInjector() {
        var container = this;

        return (klass) => {
            console.log(klass);
            var paramParser = new Proxy({}, {
                // The `get` handler is invoked whenever a get-call for
                // `injector.*` is made. We make a call to an external service
                // to actually hand back in the configured service. The proxy
                // allows us to bypass parsing the function params using
                // taditional regex or even the newer parser.
                get: (target, name) => container.resolve(name),

                // You shouldn't be able to set values on the injector.
                set: (target, name, value) => {
                    throw new Error(`Don't try to set ${name}! 😑`);
                }
            })
            return new klass(paramParser);
        }
    }
}

它不是最先进的解析器,但它提供了一个想法,如果您想使用 args 解析器进行简单的 DI,您可以如何使用代理来处理它。然而,这种方法有一个小小的警告。我们需要使用解构赋值而不是普通参数。当我们传入注入器代理时,解构与在对象上调用 getter 相同。

class App {
   constructor({tweeter, timeline}) {
        this.tweeter = tweeter;
        this.timeline = timeline;
    }
}

class HttpClient {}

class TwitterApi {
    constructor({client}) {
        this.client = client;
    }
}

class Timeline {
    constructor({api}) {
        this.api = api;
    }
}

class Tweeter {
    constructor({api}) {
        this.api = api;
    }
}

// Ok so now for the business end of the injector!
const di = new GuiceJs();

di.addModule('client', HttpClient);
di.addModule('api', TwitterApi);
di.addModule('tweeter', Tweeter);
di.addModule('timeline', Timeline);
di.addModule('app', App);

var app = di.resolve('app');
console.log(JSON.stringify(app, null, 4));

这将输出以下内容:

{
    "tweeter": {
        "api": {
            "client": {}
        }
    },
    "timeline": {
        "api": {
            "client": {}
        }
    }
}

它连接了整个应用程序。最好的一点是该应用程序易于测试(您可以实例化每个类并传入模拟/存根/等)。此外,如果您需要更换实现,您可以从一个地方完成。由于 JS 代理对象,这一切都是可能的。

注意:在它准备好用于生产之前需要做很多工作,但它确实给出了它的外观的想法。

答案有点晚了,但它可能会帮助其他有同样想法的人。👍