有没有办法动态获取函数的函数参数名称?
假设我的函数如下所示:
function doSomething(param1, param2, .... paramN){
// fill an array with the parameter name and value
// some other code
}
现在,我如何从函数内部将参数名称及其值的列表放入数组中?
有没有办法动态获取函数的函数参数名称?
假设我的函数如下所示:
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 的局部变量中提供。
参数对象不是数组。它类似于数组,但除了长度之外没有任何数组属性。例如,它没有 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 []
不太容易出现空格和注释的解决方案是:
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 代理对象,这一切都是可能的。
注意:在它准备好用于生产之前需要做很多工作,但它确实给出了它的外观的想法。
答案有点晚了,但它可能会帮助其他有同样想法的人。👍