有没有办法动态获取函数的函数参数名称?
假设我的函数看起来像这样:
function doSomething(param1, param2, .... paramN){ // fill an array with the parameter name and value // some other code }
现在,我如何从函数内部获取参数名称及其值的列表到数组中?
以下函数将返回传入的任何函数的参数名称数组.
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:
arguments对象不是Array.它类似于Array,但除了length之外没有任何Array属性.例如,它没有pop方法.但是它可以转换为真正的数组:
var args = Array.prototype.slice.call(arguments);
如果Array泛型可用,则可以使用以下代码:
var args = Array.slice(arguments);
下面是从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; }
这是一个更新的解决方案,试图以紧凑的方式解决上面提到的所有边缘情况:
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 []
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
不太容易出现空格和注释的解决方案是:
var fn = function(/* whoa) */ hi, you){}; fn.toString() .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg,'') .match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1] .split(/,/) ["hi", "you"]
这里的很多答案都使用了正则表达式,这很好但是它并没有很好地处理语言的新增功能(比如箭头函数和类).另外值得注意的是,如果你在缩小代码上使用这些函数中的任何一个,它就会发生.它将使用任何缩小的名称.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代理对象,这一切都是可能的.
注意:在准备好用于生产之前,需要做很多工作,但它确实会让人知道它的外观.
答案有点晚,但它可能有助于其他想到同样事情的人.
我知道这是一个古老的问题,但是初学者一直在进行复制,好像这是任何代码中的好习惯.大多数情况下,必须解析函数的字符串表示以使用其参数名称只是隐藏了代码逻辑中的缺陷.
函数的参数实际上存储在一个类似于数组的对象中arguments
,其中第一个参数是arguments[0]
,第二个是arguments[1]
,依此类推.在括号中写入参数名称可以看作是一种简写语法.这个:
function doSomething(foo, bar) { console.log("does something"); }
...是相同的:
function doSomething() { var foo = arguments[0]; var bar = arguments[1]; console.log("does something"); }
变量本身存储在函数的作用域中,而不是作为对象中的属性.无法通过代码检索参数名称,因为它只是表示人类语言变量的符号.
我总是将函数的字符串表示视为用于调试目的的工具,尤其是因为这个arguments
类似于数组的对象.您不需要首先为参数命名.如果您尝试解析字符串化函数,它实际上并不会告诉您可能需要的额外未命名参数.
这是一个更糟糕,更常见的情况.如果函数具有3个或4个以上的参数,则将其传递给对象可能是合乎逻辑的,这更容易使用.
function saySomething(obj) { if(obj.message) console.log((obj.sender || "Anon") + ": " + obj.message); } saySomething({sender: "user123", message: "Hello world"});
在这种情况下,函数本身将能够读取它接收的对象并查找其属性并获取它们的名称和值,但是尝试解析函数的字符串表示只会给出参数的"obj",这根本没用.
由于JavaScript是一种脚本语言,我觉得它的内省应该支持获取函数参数名称.对该功能进行修改违反了第一原则,因此我决定进一步探讨该问题.
这让我想到了这个问题,但没有内置的解决方案.这让我得到了这个解释,这个解释arguments
只是在函数之外被弃用,所以我们不能再使用myFunction.arguments
或得到:
TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
是时候卷起袖子开始工作了:
⭐检索函数参数需要解析器,因为复杂的表达式4*(5/3)
可以用作默认值.因此,Gaafar的答案或James Drew的答案是迄今为止最好的方法.
我尝试了巴比伦和esprima解析器但不幸的是他们无法解析独立的匿名函数,正如Mateusz Charytoniuk的回答所指出的那样.我想通过围绕括号中的代码找出另一种解决方法,以免改变逻辑:
const ast = parser.parse("(\n" + func.toString() + "\n)")
换行符可防止//
(单行注释)出现问题.
⭐如果解析器不可用,那么下一个最佳选择是使用像Angular.js的依赖注入器正则表达式这样经过验证的技术.我将Lambder的答案的功能版本与humbletim的答案结合起来,并添加了一个可选的ARROW
布尔值来控制正则表达式是否允许ES6胖箭头函数.
这是我放在一起的两个解决方案.请注意,它们没有逻辑来检测函数是否具有有效语法,它们只提取参数.这通常是好的,因为我们通常将解析的函数传递给getArguments()
它们,因此它们的语法已经有效.
我将尝试尽可能地策划这些解决方案,但是如果没有JavaScript维护者的努力,这将仍然是一个悬而未决的问题.
Node.js版本(在StackOverflow支持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() {}');
(function(a,b,c){}).toString().replace(/.*\(|\).*/ig,"").split(',')
=> ["a","b","c"]
您还可以使用"esprima"解析器来避免参数列表中的注释,空格和其他内容的许多问题.
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; }
它甚至可以使用这样的代码:
getParameters(function (hello /*, foo ),* /bar* { */,world) {}); // ["hello", "world"]
我之前尝试过这样做,但从来没有找到一种完成它的实用方法.我最终传入了一个对象,然后循环遍历它.
//define like function test(args) { for(var item in args) { alert(item); alert(args[item]); } } //then used like test({ name:"Joe", age:40, admin:bool });
我已经在这里阅读了大多数答案,并且我想添加我的单行代码。
new RegExp('(?:'+Function.name+'\\s*|^)\\((.*?)\\)').exec(Function.toString().replace(/\n/g, ''))[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, '')
要么
function getParameters(func) { return new RegExp('(?:'+func.name+'\\s*|^)\\s*\\((.*?)\\)').exec(func.toString().replace(/\n/g, ''))[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, ''); }
或ECMA6中的单线功能
var getParameters = func => new RegExp('(?:'+func.name+'\\s*|^)\\s*\\((.*?)\\)').exec(func.toString().replace(/\n/g, ''))[1].replace(/\/\*.*?\*\//g, '').replace(/ /g, '');
__
假设您有一个功能
function foo(abc, def, ghi, jkl) { //code }
下面的代码将返回 "abc,def,ghi,jkl"
该代码还将与Camilo Martin提供的功能一起使用:
function ( A, b ,c ,d ){}
另外,还有Bubersson对Jack Allan的回答的评论:
function(a /* fooled you)*/,b){}
__
说明new RegExp('(?:'+Function.name+'\\s*|^)\\s*\\((.*?)\\)')
这就产生了一个正则表达式用new RegExp('(?:'+Function.name+'\\s*|^)\\s*\\((.*?)\\)')
。我必须使用,new RegExp
因为我正在向Function.name
RegExp中注入变量(,即目标函数的名称)。
示例如果函数名称为“ foo”(function foo()
),则RegExp将为/foo\s*\((.*?)\)/
。
Function.toString().replace(/\n/g, '')
然后,它将整个函数转换为字符串,并删除所有换行符。删除换行符有助于Camilo Martin提供的功能设置。
.exec(...)[1]
这就是RegExp.prototype.exec
功能。它基本上将正则指数(new RegExp()
)与字符串(Function.toString()
)相匹配。然后,[1]
它将返回在正指数()中找到的第一个捕获组(.*?)
。
.replace(/\/\*.*?\*\//g, '').replace(/ /g, '')
这将删除所有的注释中/*
和*/
,并删除所有空格。
现在,这还支持阅读和理解arrow(=>
)函数,例如f = (a, b) => void 0;
,该函数Function.toString()
将返回(a, b) => void 0
而不是常规函数function f(a, b) { return void 0; }
。原始正则表达式可能会引起混乱,但现在可以解决了。
变化是从new RegExp(Function.name+'\\s*\\((.*?)\\)')
(/Function\s*\((.*?)\)/
)到new RegExp('(?:'+Function.name+'\\s*|^)\\((.*?)\\)')
(/(?:Function\s*|^)\((.*?)\)/
)
如果要将所有参数放入一个Array中,而不是用逗号分隔的String中,请最后添加.split(',')
。